b8586537f8814eb06e22126302babd7019b07d98
141 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c9e679169 |
docs: rewrite README into a proper user-facing intro
The old README was mostly a phase-status table — useful for tracking
progress but unhelpful for a first-time reader landing on the repo.
Restructured around the three things a new user actually needs:
1. **What it is.** One-paragraph pitch + a feature checklist that
mirrors the SPEC §13.3 status without burying the reader in phase
numbers. Architecture details + the deferred §13.1 work stay in
`SPEC.md`, linked from the intro.
2. **How to install.** Plugin-manager snippets for lazy.nvim,
vim-plug, dein, and a plain-Vim packpath path. Plus a
"try it without touching your config" pointer to
`./start-nvim.sh` / `./start-vim.sh` so curious users can kick the
tyres in 5 seconds.
3. **How to use.** A compact `setup({})` example covering wiki_root,
multi-wiki, mappings groups, folding strategy, and diagnostic
severity. Then command and keymap cheatsheets that distinguish
"vimwiki-compat surface" from "things the LSP server returns",
plus the migrating-from-vimwiki notes (two behaviour deltas the
user might trip on).
Kept:
- The repository layout diagram (updated to reflect the renamed
ftplugin/syntax files and the new `scripts/` test harness).
- The implementation-status tables, now under `Development` so
contributors find them but new users aren't drowned in them.
- The dual MIT/Apache license footer.
No behaviour changes; documentation-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a229977b77 |
ci(keymaps): pin Neovim 0.11 from upstream release tarball
CI log for the keymaps job showed Lua crashing with `attempt to call field 'get_clients' (a nil value)` plus a flood of `W18: Invalid character in group name` on the `@vimwiki*` highlight links. Both signal that apt's `neovim` package is too old (Ubuntu LTS ships 0.6/0.7) — `vim.lsp.get_clients` only landed in 0.10, and Tree-sitter-style `@group.modifier` highlight names only became valid sometime in the 0.8-ish era. Switched the installer to pull the official static tarball (`neovim/neovim/releases/v0.11.3/nvim-linux-x86_64.tar.gz`) and symlink it into `/usr/local/bin/nvim`. Vim from apt is kept — it only runs the pure-VimL harness so its age doesn't matter. Also added a defensive fallback in the harness: `get_lsp_clients = vim.lsp.get_clients or vim.lsp.get_active_clients`, plus a `server_capabilities` nil guard. Won't matter on the pinned 0.11 runner, but means a future regression that drops the deps will fail with a clear `lsp.attached` FAIL message instead of a Lua stack trace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a489bea480 |
test(keymaps): hard timeouts + serialise the CI job behind test
User reported the editor-keymaps CI job hung for 30 minutes before "Has been cancelled" — twice in a row. Two changes to keep that from happening again: 1. Wrap both harness invocations with `timeout` (60s Neovim, 30s Vim). A misbehaving LSP attach, a stuck `vim.defer_fn`, or ex-mode hung on stdin can no longer hold the runner indefinitely; `timeout` kills the process and the wrapper exits non-zero so CI shows the real failure shape. 2. `</dev/null` on `vim -e -s` so ex-mode doesn't sit waiting for input on CI's non-TTY stdin (the local zsh run inherits the user's tty, masking this). 3. CI: the keymaps job now has `needs: test` and `timeout-minutes: 10`. `needs: test` serialises behind the cargo jobs so we don't fight for the single Gitea runner, and the workflow-level timeout is a belt to the script-level `timeout`'s braces — if for some reason both fail the same way, the runner reclaims itself in 10 minutes instead of 30. Local timing: `test-keymaps.sh` 10.4s, `test-keymaps-vim.sh` 0.1s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5fdd7a842e |
test(keymaps): Vim path harness + fix 25 broken one-liner functions
User asked about the Vim path of the keymap suite. Building it
surfaced a real bug: 25 of our autoload functions were written as
`function! foo() abort | call bar() | endfunction` one-liners, which
isn't valid Vim syntax — `function!` requires a multi-line body, and
Vim parses the `|` after `abort` as an unexpected trailing character
(E488). Most invocations of the buggy autoload functions errored
out the moment Vim tried to parse them, which is why the user saw
broken keymaps and confusing diagnostics.
Rewrote all 25 one-liners (`autoload/nuwiki/commands.vim`) into the
standard three-line form. Affected groups: `diary_*`, `toggle_list_item`,
`cycle_list_item`, `reject_list_item`, `heading_add`, `heading_remove`,
`toc_generate`, `links_generate`, `export_current` / `_all` /
`_all_force` / `_rss`, and the §13.1 deferred stubs (`list_change_lvl`,
`list_remove_done`, `table_*`, `colorize`, `paste_link`, `paste_url`).
Added `scripts/test-keymaps-vim.{sh,vim}` — a Vim-side counterpart of
the Neovim harness covering the pure-VimL bindings (header nav,
link nav, `o`/`O` bullet continuation, `<CR>` wrap step). The
LSP-roundtrip bindings (`<C-Space>`, `=`, `-`, …) stay on the Neovim
side because vim-lsp's async layer uses timers that don't fire
inside `vim -e -s` headless mode — their server-side codepath is
already exercised by the Neovim harness and the cargo test suite.
Vim harness covers 12 cases: filetype + 2 command-presence smoke
tests, 4 header-nav (`]]`/`[[`/`]=`/`]u`), `<Tab>` link-nav, `<CR>`
wrap-on-first-press, and 3 bullet-continuation flows.
CI: extended the existing `keymaps` job to install both nvim + vim
and run both harnesses. Verified locally:
Neovim harness: 19 passed, 0 failed
Vim harness: 12 passed, 0 failed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3b6693129c |
test(keymaps): headless Neovim harness for buffer-local bindings + CI
`scripts/test-keymaps.sh` boots `nvim --headless` against a scratch wiki, sources `scripts/test-keymaps.lua`, and asserts the post-state of each buffer-local mapping registered by `ftplugin/vimwiki.vim` + `lua/nuwiki/keymaps.lua`. The harness covers the bindings that actually have observable effects on the buffer or cursor: - **Lists** (LSP round-trip): `<C-Space>` / `<C-@>` / `<Nul>` toggle, `gln` cycle, `glx` reject, plus a `[X]→[ ]` round-trip. - **Tasks**: `gnt` jumps to the next unfinished `[ ]`. - **Headings** (LSP round-trip): `=` adds a level, `-` removes one, `=` on h6 clamps without growing. - **Header nav** (pure Lua): `]]` next, `[[` prev, `]u` parent. - **Link nav** (pure Lua): `<Tab>` to the next `[[…]]`. - **`<CR>` two-step**: first press wraps a bare word as `[[word]]` and stops (matches vimwiki's review-then-follow). - **Bullet continuation**: `o` / `O` keep the marker, and `o` on a `[ ]` line carries the checkbox forward. Each case sets buffer lines, places the cursor (1-based), fires `nvim_feedkeys` with `replace_termcodes`, waits long enough for the LSP `executeCommand` round-trip (800 ms by default; 100 ms for the pure-client maps), and asserts either the resulting lines, a specific line, the cursor's new line, or the buffer's new name. Output goes via `$NUWIKI_KEYMAP_RESULTS` (env-var IPC keeps it robust to `--clean -u` argument parsing). Exit code mirrors the harness: 0 on green, 1 on any FAIL. Local run yields: SUMMARY: 19 passed, 0 failed CI: new `keymaps` job in `.gitea/workflows/ci.yaml` installs Neovim and runs the script, so a regression in `lua/nuwiki/keymaps.lua`, `autoload/nuwiki/commands.vim`, or the LSP command surface is caught on every push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4905858d5e |
fix(follow-link): two-step <CR> — first wraps, second follows
User feedback: the smart `<CR>` should give the user a chance to
review/edit the freshly-created wikilink before committing to the
follow. Matches upstream vimwiki's flow.
`follow_link_or_create` (Lua + VimL) now:
1. Cursor already inside `[[…]]` → follow immediately.
2. Cursor on a bare word → wrap it as `[[word]]`, place cursor
inside, and STOP. A second `<CR>` then follows.
3. Neither — fall through to plain definition request so users
can still chord follow without a target word.
Verified: cursor on `MyPage rest` →
1st `<CR>` → `[[MyPage]] rest` (buffer unchanged)
2nd `<CR>` → opens `MyPage.wiki` (the synthesised future page).
381 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
347e2f02f7 |
fix(follow-link): create missing page + wrap word on <CR>
Two missing pieces in the previous "follow creates page" fix:
1. **`wiki_root` never reached the server.** The Lua glue (and the
Vim autoload glue) was sending the config under `settings` — that
field is for `workspace/didChangeConfiguration` notifications, not
for `initialize`. The server's `Config::from_init_params` reads
`initialization_options`, so `wiki_root` arrived as `None`, the
`Wiki` aggregate was empty, and `wiki_for_uri` returned `None` →
`resolve_target_uri` bailed before reaching my synthesise path,
leaving `<CR>` with "No Location Found".
Both glues now send the same payload under both keys:
- `lua/nuwiki/lsp.lua` → adds `init_options = init_options()`
alongside the existing `settings = …`.
- `autoload/nuwiki/lsp.vim` → adds `initialization_options` to
the `lsp#register_server` call.
The server keeps reading from `initializationOptions` at startup
*and* honouring `didChangeConfiguration` later (Phase 11 plumbing).
2. **No-wiki fallback in the server.** Even with the glue fix above,
users who launch nuwiki on an ad-hoc `.wiki` file outside any
workspace folder would still get an empty `wikis` list. The
`LinkKind::Wiki` arm now falls back to a new
`synthesise_page_uri_next_to(source_uri, path)` which builds the
future page next to the current file. `<CR>` on `[[NewPage]]`
opens `./NewPage.wiki`, save creates it.
3. **`<CR>` on a plain word now wraps + follows.** Mirrors vimwiki's
`:VimwikiFollowLink`: when the cursor isn't already inside
`[[…]]`, the new `nuwiki.commands.follow_link_or_create` /
`nuwiki#commands#follow_link_or_create` first wraps the word
under cursor as `[[word]]`, places the cursor inside the link,
then dispatches `vim.lsp.buf.definition()` /
`:LspDefinition`. Both editor paths now bind `<CR>` /
`<S-CR>` / `<C-CR>` / `<C-S-CR>` to this smart variant.
Verified end-to-end:
- `[[NewPage]]` with the rebuilt binary: definition returns
`file:///tmp/wiki/NewPage.wiki` as a Location, so the editor
opens an empty buffer for it.
- A bare `MyPage` word becomes `[[MyPage]]` in the buffer before
the follow request fires.
Total 381 tests still pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9d86218b13 |
fix(keymap): C-Space toggle fires across all terminal byte spellings
User reported Ctrl+Space didn't toggle list items. Three things contributed: 1. **Terminal byte ambiguity.** Different terminals encode Ctrl+Space differently — GUI / kitty protocol sends `<C-Space>`, xterm and most legacy terms send NUL which Vim sees as `<C-@>` (or, in some Neovim builds, `<Nul>` in keymap.lhs). The previous keymap registered only `<C-Space>` + `<C-@>`. Added the `<Nul>` alias too (both Lua and VimL paths) so the binding fires regardless of what byte the user's terminal produces. 2. **Deprecated `vim.lsp.util.make_position_params`.** Neovim 0.12 requires `position_encoding` explicitly. The old no-arg call now prints a deprecation warning, and in stricter setups returns nil — which made the command's `arguments` malformed and the `executeCommand` request silently fail. `position_params()` now pulls `offset_encoding` from the resolved client and passes both args, with a `pcall`-guarded fallback to the no-arg form for Neovim < 0.10. 3. **Deprecated `client.request(...)` dot-call.** Same Neovim 0.12 change — `Client:request()` is the method form. Use the colon call now; both arities are preserved for back-compat. Verified end-to-end: feeding all three of `<C-Space>` / `<C-@>` / `<Nul>` via `nvim_feedkeys` toggles the checkbox; `:messages` no longer carries the deprecation warnings. 381 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
047c9a3cf3 |
fix(vim): follow-link creates missing pages + safe LSP foldexpr
Two user-visible bugs: 1. `<CR>` on a wikilink whose target wasn't indexed yet (typical for "follow this link → page doesn't exist → I want a fresh buffer for it") did nothing. Cause: `Backend::resolve_target_uri` returned `None` once `WorkspaceIndex::resolve` came up empty, so `goto_definition` had no Location to hand back to the client and `vim.lsp.buf.definition()` / `:LspDefinition` silently no-op'd. Vimwiki's behaviour is "open a fresh buffer for the future page". `Backend::resolve_target_uri` now synthesises a `<wiki_root>/<path><file_extension>` URI when the lookup misses, for both `LinkKind::Wiki` and the `LinkKind::Interwiki` fallback. The editor opens an empty buffer; saving writes the file. New helper `synthesise_page_uri` does the path walk so subdirectory wikilinks like `[[notes/Daily]]` get the right output path. 2. Folding occasionally surfaced an `E5108` from inside `vim.lsp.foldexpr()` — happens when the function fires before the LSP client has finished attaching to the buffer, or when the server's `foldingRange` response hasn't yet arrived. The error gets shouted once per visible line. `lua/nuwiki/folding.lua` now exports `lsp_expr` which `pcall`s `vim.lsp.foldexpr` and falls back to the regex implementation (`M.expr`) on error or when the LSP function isn't available. `ftplugin.lua` points `foldexpr` at the wrapper instead of `vim.lsp.foldexpr` directly. `foldtext` is set in both branches now so collapsed folds keep the nicer summary line. Tests: 4 new in `phase19_followlink_creates.rs` covering indexed resolution, missing-page fallback, the canonical `<root>/<page>.wiki` shape, and the slash-bearing subdirectory link. Total 381 tests pass; fmt + clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3a8eae9d0 |
fix(vim): vim-lsp detection + drop hang-prone echo lines
`autoload/nuwiki/lsp.vim` probed vim-lsp via `exists('*lsp#register_server')`
but autoload functions aren't loaded until first call — that check
always returned false on the dev script's setup, so the plugin
printed "no supported LSP client found" even after vim-lsp had been
cloned + added to the runtimepath.
vim-lsp's `plugin/lsp.vim` sets `g:lsp_loaded = 1` only when its
hard requirements (`json_encode`, `timers`, `lambda`) are met, so
that's the reliable presence flag. Switched to `exists('g:lsp_loaded')`.
Also replaced `echo` with `echomsg` in the generated `start-vim.sh`
vimrc — the two startup status lines were pushing past `'cmdheight'`
and triggering "Press ENTER or type command to continue", which made
non-interactive invocations exit before the buffer was usable.
`echomsg` writes to `:messages` history without the prompt.
Verified with `vim --clean -u $DEV/vimrc index.wiki`:
- `g:lsp_loaded = 1`
- `filetype = vimwiki`
- `:VimwikiTOC` defined (buffer-local), `b:did_ftplugin = 1`
- No "press ENTER" hang.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f1bf70d448 |
fix(vim): gate Neovim TS-style highlight groups + correct bin path lookup
Plain Vim spat a wall of `W18: Invalid character in group name` on
every buffer load because `syntax/vimwiki.vim` declared the
LSP-token highlight defaults with Tree-sitter-style names
(`@vimwikiHeading.level1`, `@vimwikiBold`, …). Those work on Neovim
but Vim rejects `@` and `.` in group names.
- `syntax/vimwiki.vim` now guards the `@vimwiki*` block with
`has('nvim')`. The static regex fallback below it (which uses
Vim-compatible `nuwiki*` group names) keeps highlighting working
on plain Vim.
While reproducing, also noticed `autoload/nuwiki/lsp.vim`'s
`s:bin_path` reported the binary at `/home/gfranco/bin/nuwiki-ls`
(only two dirs under `$HOME`) instead of
`/home/gfranco/git-repositories/nuwiki/bin/nuwiki-ls`. Cause:
`expand('<sfile>:p')` inside a function resolves to the *calling*
script at invocation time, not this autoload file. Captured the
plugin root via `<sfile>` at script-load time into a new
`s:plugin_root` and used it in `bin_path()`.
Verified with `vim -u .vimrc index.wiki`:
- `:messages` is empty of W18 warnings.
- The "binary not found" path (when no built binary exists) now
prints the correct repo-relative path.
Total 377 Rust tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d58a34a2d7 |
feat(vim): close keymap gap with Neovim (47 → 63 maps)
The previous commit registered the parity surface on Neovim but only ported ~75% to plain Vim. Filling the remaining 16: autoload/nuwiki/commands.vim — new functions: - `wiki_ui_select` — Vim has no `vim.ui.select`, so route through `inputlist()` after fetching the wiki list via the LSP. - `open_below_with_bullet` / `open_above_with_bullet` — port of the Lua `o` / `O` continuation that preserves the list marker (and a `[ ]` checkbox when present) on the new line. - `next_header` / `prev_header` / `next_sibling_header` / `prev_sibling_header` / `parent_header` — port of the Lua heading jumper. Pure VimL, no LSP round-trip. ftplugin/vimwiki.vim (Vim path) new mappings: - `<Leader>ws` → `wiki_ui_select` - `]=` / `[=` → next / prev sibling header - `]u` / `[u` → parent header - `o` / `O` → bullet continuation - `gl<Space>` / `gL<Space>` → :VimwikiRemove*CB stubs - `gq1` / `gw1` → :VimwikiTableAlignQ1/W1 stubs - x-mode variants for `+`, `<Leader>wc`, `gln`, `glp`, `glx` Replaced the regex-based `]]` / `[[` (which just searched `^\s*=\+\s`) with calls into the new heading nav so they skip non-heading `=` runs and respect heading levels. Verified end-to-end: `silent map <buffer>` reports 63 maps on Vim, matching Neovim's 63. All 377 Rust tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9e6faa5554 |
feat(mappings): vimwiki-parity default keymaps for Vim + Neovim
The Phase 19 keymap layer only covered the high-value subset and used `g=`/`g-` instead of vimwiki's `=`/`-` for header levels. Users reporting "mappings don't seem to be working" were either on the Vim path (which registered zero keymaps) or hitting bindings that don't exist (e.g. `<Tab>`, `<S-CR>`, `gll`, `gqq`). Cross-referenced upstream vimwiki/ftplugin/vimwiki.vim and rewired both `lua/nuwiki/keymaps.lua` and `ftplugin/vimwiki.vim` (Vim path) to match its default surface. Mappings backed by commands that exist on the server dispatch directly; §13.1-deferred mappings register with the same lhs but stub out to a notification — so users get parity *signalling*, not silence. New on Neovim (and matched on Vim where possible): Links group: - `<S-CR>` / `<C-CR>` / `<C-S-CR>` — follow in split / vsplit / tab - `<Tab>` / `<S-Tab>` — jump to next / prev `[[…]]` (pure Lua search) - `+` (n + x) — :VimwikiNormalizeLink stub - `<Leader>wc` (n + x) — :VimwikiColorize stub Lists group: - `<C-@>` (n + x) — terminal-alt for `<C-Space>` - `gln` / `glp` increment + decrement (deferred backward; same fn for now) - `glh` / `gll` / `gLh` / `gLl` — list level stubs - `glr` / `gLr` — list renumber stubs - `gl<Space>` / `gL<Space>` — checkbox remove stubs - `o` / `O` — open below/above and continue the bullet (pure Lua) - visual-mode variants of `<C-Space>`, `gln`, `glp`, `glx` Header group (key group, now buffer-local): - `=` / `-` — promote / demote (was `g=`/`g-`, matched to vimwiki) - `]]` / `[[` — next / prev header (pure Lua) - `]=` / `[=` — next / prev sibling header (pure Lua) - `]u` / `[u` — parent header (pure Lua) Table group: `gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`, `<A-Right>` stubs. Wiki prefix: `<Leader>w<Leader>m` (vimwiki's tomorrow), `<Leader>w<Leader>i` (rebuild diary index). Config schema (`config.options.mappings`) updates to vimwiki's group names — `links` / `lists` / `headers` / `table_editing` / `diary` / `html_export` / `text_objects` / `wiki_prefix`. Old names (`list_editing` / `header_nav`) are retired; rename only — keeps the opt-out shape intact. Vim path: same set ported to buffer-local VimL maps. `g:nuwiki_no_default_mappings` opts out the whole layer for users who prefer their own bindings. Lua side uses the Neovim 0.11+ `vim.keymap.set` API; both call straight into the same `nuwiki#commands#…` autoload (Vim) / `nuwiki.commands` (Lua) layer that drives the LSP. Total 377 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04df170791 |
feat(vim): :Vimwiki* / :Nuwiki* commands for plain Vim via vim-lsp
The Phase 19 ftplugin only registered the command surface on Neovim
and finished early for plain Vim, leaving `start-vim.sh` users with
only commentstring + suffixesadd — exactly the "no commands" report.
`autoload/nuwiki/commands.vim` now ships the Vim-side dispatch:
- `s:exec` sends `workspace/executeCommand` through vim-lsp's
`lsp#send_request`. Optional callback for commands that return data.
- `s:open_uri_from` reads `{ uri }` out of the LSP response and runs
`:edit` (or `:tabedit` for the tab variants).
- `s:results_to_qf` lifts `checkLinks` / `findOrphans` / `tags.search`
arrays into the quickfix list and opens `:copen` — same UX as the
Neovim path.
- `s:open_browser` fires `open` / `xdg-open` after `export.browse`.
- §13.1 deferred commands stub out with a "not yet implemented"
notification so users get the same signal as on Neovim.
`ftplugin/vimwiki.vim` defines the same `:Vimwiki*` / `:Nuwiki*`
command set on the plain-Vim path, each delegating to its
`nuwiki#commands#…` autoload counterpart. `:VimwikiFollowLink` /
`:VimwikiBacklinks` map straight to vim-lsp's `:LspDefinition` /
`:LspReferences`. coc.nvim users can still use `:CocCommand` directly
and ignore the aliases entirely.
Verified with `vim -e -s -u .vimrc index.wiki`:
filetype=vimwiki, did_ftplugin=1,
:VimwikiTOC, :NuwikiIndex, :VimwikiToggleListItem all defined.
Total 377 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
09b9bd98a3 |
fix(ftplugin): load on .wiki buffers without manual filetype override
Three reasons the plugin wasn't activating on `nvim --clean -u init.lua
foo.wiki`:
1. **File-name mismatch.** Vim's filetype-plugin runtime looks for
`ftplugin/<filetype>.vim` matching the *filetype name*. Our
filetype is `vimwiki` (per `ftdetect`) but the file was named
`ftplugin/nuwiki.vim` — so `:runtime! ftplugin/vimwiki.vim` from
the built-in FileType autocmd never found it, and none of the
`:Vimwiki*` / `:Nuwiki*` commands got defined. Same wart on the
syntax side. Renamed:
- `ftplugin/nuwiki.vim` → `ftplugin/vimwiki.vim`
- `syntax/nuwiki.vim` → `syntax/vimwiki.vim`
2. **`setfiletype` deferred to Neovim's bundled rule.** Neovim ships
a default `*.wiki → mediawiki` rule via `vim.filetype.add`, and
`:setfiletype vimwiki` is a no-op once a filetype has been set.
- `ftdetect/nuwiki.vim` now uses `set filetype=vimwiki` (force).
- `lua/nuwiki/init.lua`'s `setup()` also calls
`vim.filetype.add({ extension = { wiki = 'vimwiki', ... } })`
so the modern table-based detection picks us before the
bundled rule fires. Honours `config.options.file_extension`
so a user with a custom extension gets covered automatically.
3. **`foldmethod`/`foldexpr` are window-local options.**
`ftplugin.lua` was setting them with `nvim_set_option_value({ buf
= bufnr })`, which threw `'buf' cannot be passed for window-local
option 'foldmethod'` and aborted the rest of the per-buffer
attach. Switched to `vim.opt_local.*` (window-aware), applied on
the current window when the ftplugin fires, and re-applied via a
`BufWinEnter` autocmd so `:split` / `:vsplit` keep the fold mode.
Verified end-to-end with:
nvim --clean -u init.lua sample.wiki
→ filetype = vimwiki
→ :VimwikiTOC, :NuwikiIndex, … all defined
→ foldmethod = expr, foldexpr = v:lua.vim.lsp.foldexpr()
→ ftdetect, plugin, ftplugin, syntax all in :scriptnames
Total 377 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
cf94f99d3c |
dev: minimal-config launchers for nvim/vim plugin testing
Two scripts at the repo root that spin up an isolated editor session against this checkout, so changes to the plugin or LSP can be smoke- tested without polluting the user's real (n)vim install: - start-nvim.sh — builds nuwiki-ls in release mode, symlinks it into `bin/`, seeds a scratch wiki under `$XDG_CACHE_HOME/nuwiki-dev/wiki`, generates a minimal `init.lua` that prepends this repo to `runtimepath`, points `vim.env.XDG_*` at the dev cache so the host's Neovim state is untouched, and execs `nvim --clean -u <init>`. - start-vim.sh — same shape for Vim. Clones vim-lsp + async.vim into the dev cache on first run so the LSP path is actually exercisable (the plugin's autoload layer prefers vim-lsp). Opt-out via `NUWIKI_DEV_NO_LSP=1` for syntax-only testing. Both scripts: - Are idempotent (skip cargo build / git clone / wiki seed when their outputs are already in place). - Pass through extra args to the editor; default to opening the scratch wiki's index page. - Accept `NUWIKI_DEV_WIKI=<path>` to point at a custom sandbox. - Seed `index.wiki` + `Notes.wiki` with smoke-test content covering wikilinks, anchors, checkboxes, tags, and the `:Vimwiki*` command surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
63e5b6d514 |
phase 19: editor glue v2
Server-side:
- New `folding.rs` module with `folding_ranges(ast, total_lines)`.
Emits one fold per top-level heading (start → line before the
next same-or-higher-level heading, or EOF) plus one per top-level
list block. Nested headings fold inside their parent thanks to the
level-aware end-line computation; sublists fold implicitly via
their parent item's source span. `FoldingRangeKind::Region` is set
on every fold so collapsing UIs render them as section folds.
- `Backend::folding_range` handler wires the module into
`textDocument/foldingRange`; `ServerCapabilities.folding_range_provider`
advertises it.
- `folding::line_count` exposed for the handler and tests; treats a
trailing newline as a virtual empty line (the line LSP positions
use for EOF).
Editor glue (Neovim primary, Vim minimal):
- `lua/nuwiki/commands.lua` — async `workspace/executeCommand`
wrappers for every server command shipped through Phase 18. The
open-* family handles `{ uri }` responses by opening the file
(with `tab` / `split` variants). `check_links` and `find_orphans`
hand results to `setqflist` + `:copen` for quickfix-style review.
§13.1-deferred commands (`list.changeSymbol`, table rewriters,
`link.pasteWikilink/pasteUrl`, `colorize`) stub out with
`vim.notify` so users get a clear "not yet implemented" signal
instead of an LSP "unknown command" error.
- `lua/nuwiki/keymaps.lua` — buffer-local default mappings. Subgroups
(`list_editing`, `header_nav`, `diary`, `html_export`,
`text_objects`) flip independently via the new
`mappings.<group>` config. Heading promote/demote uses `g=`/`g-`
to avoid clobbering Vim's built-in `=`/`-` operators.
- `lua/nuwiki/textobjects.lua` — `ah`/`ih` (around/inside heading)
using a buffer scan for the heading-block boundary. The four
remaining text objects from SPEC §12.10 wait until §13.1 lands
the table/list rewriters they share infrastructure with.
- `lua/nuwiki/folding.lua` — pure regex `foldexpr` + `foldtext`
fallback for clients without `foldingRange`. Same heading-block
model as the server.
- `lua/nuwiki/ftplugin.lua` — single per-buffer attach entry point.
`folding = 'lsp'` (default) uses Neovim 0.11+'s `vim.lsp.foldexpr`,
falling back to the regex on older versions; `'expr'` forces the
fallback; `'off'` skips folding setup.
- `lua/nuwiki/config.lua` — extends defaults with `mappings = {...}`
(P10 keymap layer) and `folding` (P14 resolved).
- `ftplugin/nuwiki.vim` — declares every `:Vimwiki*` / `:Nuwiki*`
command from SPEC §12.10 by inlining `lua require(...).fn()`
bodies (no script-local function indirection so commands stay
callable after re-source). Plain-Vim users get only the buffer
options; they're expected to drive the LSP via vim-lsp / coc's
built-in commands.
Health check (§12.10 additions):
- `:checkhealth nuwiki` now reports the count of `executeCommand`
entries the server advertises, whether `foldingRange` capability
was negotiated, and whether the configured HTML output directory
exists + is writable. Default keymap subgroup status is also
surfaced.
Tests: 8 new in `phase19_folding.rs` covering empty docs, single
heading → EOF, sibling headings each getting their own fold,
nested heading bounded by parent, top-level list folds, single-line
heading non-fold, fold-kind classification, and `line_count`
semantics. Total 377 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
106268d488 |
phase 18: multi-wiki resolver + picker commands
The Phase 11 plumbing already landed the data structures (`Wiki`
aggregate, per-wiki `WorkspaceIndex`, `wikis = [...]` config shape
with v1.0 desugaring). Phase 18 closes the user-visible gap: cross-
wiki link resolution + the picker command surface.
Cross-wiki resolution:
- `Backend::wikis_snapshot` — clone the Vec under the read lock
so commands can drop it before awaiting.
- `Backend::wiki_by_index` / `wiki_by_name` — pair with the
parser's `wiki<N>:` / `wn.<Name>:` interwiki shapes.
- `Backend::resolve_target_uri(target, source_uri)` — single entry
point for cross-wiki page lookup:
- `Wiki` / `AnchorOnly` → source wiki's index (Phase 8
behaviour preserved).
- `Interwiki` → routes to the wiki referenced by
`wiki_index` / `wiki_name`, then resolves the page in that
wiki's `pages_by_name`. Returns `None` when no wiki matches
or the page isn't indexed.
- `Backend::heading_range_for(target_uri, anchor)` — anchor lookup
on the target wiki's index. Phase 8's `goto_definition` was
computing this against the *source* wiki's index, which broke for
interwiki anchors; both `goto_definition` and `hover` are
refactored onto the new pair.
Commands:
- `nuwiki.wiki.listAll` — returns `[{ id, name, root, syntax,
file_extension }]`. Drives the picker UI.
- `nuwiki.wiki.select` — alias of `listAll` per SPEC §12.9
(selection is client-side; the server just exposes the list).
- `nuwiki.wiki.openIndex` — args `{ wiki? }` accepting an index id
(number), name (string), or numeric string. Returns
`{ uri, name, tab: false }` for `<root>/index<file_extension>`.
- `nuwiki.wiki.tabOpenIndex` — same payload but `tab: true` so
the client knows to open in a new tab/split.
- `nuwiki.wiki.gotoPage` — args `{ page, wiki? }`. Prefers an
indexed URI when the page is already known; otherwise builds
`<root>/<page><file_extension>` directly. Drives the
`:VimwikiGoto {name}` compat command.
Tests: 12 new in `phase18_multi_wiki.rs` covering parser shapes for
`wiki<N>:` / `wn.<Name>:` (and that plain `[[Home]]` stays
`LinkKind::Wiki`), multi-wiki config loading + v1.0 single-root
desugaring, `WikiId` sequential assignment, longest-prefix URI
routing for nested roots, the cross-wiki resolution semantics
(target wiki's index wins when both wikis define the same page
name), missing-wiki fallback, the COMMANDS list completeness, and a
link-kind matrix covering all nine kinds. Total 369 tests pass;
fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d1b807b7d1 |
phase 17: html export commands + extended template vars
HtmlRenderer (nuwiki-core):
- `with_var(k, v)` / `with_vars(map)` add arbitrary `{{key}}`
substitution alongside the existing `{{title}}` / `{{content}}`.
Substitution order: content → title → user vars, so a rendered body
that happens to contain a literal `{{title}}` isn't double-processed.
WikiConfig (nuwiki-lsp/config):
- New `html: HtmlConfig` field with per-wiki export options matching
vimwiki's `g:vimwiki_*` keys: `html_path`, `template_path`,
`template_default`, `template_ext`, `template_date_format`,
`css_name`, `auto_export`, `html_filename_parameterization`,
`exclude_files`. Defaults: `<root>/_html` and `<root>/_templates`.
- `RawWiki` accepts all of the above through `initializationOptions`
(and `workspace/didChangeConfiguration` reload).
New `export` module (pure helpers):
- `output_path_for` — page name → on-disk HTML path. Slugifies only
the final segment when `html_filename_parameterization = true` so
the `diary/` subdir survives.
- `template_for` — primary/fallback path lookup tuple.
- `fallback_template` — minimal in-memory page used when neither
template file exists.
- `format_date` — strftime subset (`%Y`/`%m`/`%d`/`%%`) so we don't
depend on `chrono`/`time`.
- `build_toc_html` — nested `<ul class="toc">` from the doc's
headings, with HTML escaping.
- `compute_root_path` — `../` prefix per directory depth for
stylesheet hrefs in nested pages.
- `is_excluded` — glob matcher supporting `*`, `**`, `?`. Used by
the `allToHtml*` walker against `exclude_files`.
- `render_page_html` — wires the above into HtmlRenderer with
`{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}` populated.
- `DEFAULT_CSS` — minimal stylesheet bundled for first-time exports.
New commands:
- `nuwiki.export.currentToHtml` — render the current buffer; honours
`%nohtml` and `%template`; ensures CSS exists.
- `nuwiki.export.allToHtml` — walk the wiki root, skip pages matched
by `exclude_files` and `%nohtml`, and only re-export pages whose
source is newer than the existing HTML (incremental behaviour
matching `:VimwikiAll2HTML`).
- `nuwiki.export.allToHtmlForce` — same but unconditional re-export.
- `nuwiki.export.browse` — alias of currentToHtml that returns the
`file://` URL of the rendered page in the response so the client
can open it.
- `nuwiki.export.rss` — write `<html_path>/rss.xml` listing the
wiki's diary entries newest-first.
- `did_save` handler — runs the auto-export equivalent when the
resolved wiki has `auto_export = true`.
LSP capability:
- `text_document_sync` switched from `Kind(FULL)` to `Options(...)`
so the server can register a `save` notification (otherwise
clients never push `did_save` events).
Tests: 33 new in `phase17_html_export.rs` covering HtmlConfig
defaults + JSON-shape config, the `format_date` subset (including
the `%%` escape and pass-through for unsupported specifiers),
root-path computation, output-path slugification rules, the glob
matcher (single-star, globstar, question-mark, non-match),
template_for primary/fallback selection, render_page_html for the
title/content/date/root_path triad and the override-by-extras flow,
and side-effecting disk paths (write_page creates parents, picks up
on-disk template, RSS is newest-first, ensure_css is idempotent).
Total 357 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ae7d8c7971 |
spec: track pending implementation in §13
Backfill scope reminder for the audit that surfaced the Phase 14 deferred surface. §13 consolidates the items still owed from earlier phases so a future "what's left" check doesn't have to re-derive scope from the per-phase prose. - §13.1 lists the 11 deferred Phase 14 commands (list/table/link rewriters + colorize), each with an implementation sketch and a suggested cluster (list rewriters, table rewriters, link helpers, Phase-17-blocked). - §13.2 documents the deliberately-deferred `will_rename` / `will_delete` file-operation variants from the Phase 13 backfill. - §13.3 reminds readers that Phases 17–19 are roadmap, not missing-from-earlier-phases. - §13.4 clarifies that §9 syntax checklist `[ ]` rows are parser coverage rather than missing features. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ad1b55a401 |
backfill: tag commands + file-operations capability
SPEC audit of Phases 12–13 surfaced two gaps. Closing them here so the
v1.1 command surface and capability advertisement match what §12.3 and
§12.4 promise.
Phase 12 — tag commands (§12.3):
- `nuwiki.tags.search` — case-insensitive substring filter over the
workspace tag index. Returns `[{ name, uri, range, scope, page }]`
where `scope` is one of `file` / `heading` / `standalone`.
- `nuwiki.tags.generateLinks` — generates/refreshes a tagged-pages
section. Two modes:
- `{ uri, tag }` → `= Tag: <name> =` + flat list of `[[Page]]`
entries for every page tagged with `<name>`.
- `{ uri }` (no tag) → `= Tags =` umbrella section with `== <tag>
==` subgroups, mirroring `:VimwikiGenerateTagLinks` (no arg).
Idempotent: re-runs replace the existing section by
case-insensitive heading match.
- `nuwiki.tags.rebuild` — synchronous full re-walk of the wiki root.
Drops index entries for pages whose files no longer exist on disk,
re-parses every `.wiki`, and returns `{ pages, files_seen,
stale_removed }`. Useful for cleaning up after out-of-band edits
the LSP didn't see.
Ops layer:
- `tag_hits(index, query) → Vec<TagHit>` — pure filter, sorted by
(name, page, line).
- `tag_pages_snapshot(index) → BTreeMap<tag, [pages]>` — taken under
the read lock so the dispatcher can release the lock before
building the edit.
- `build_tag_links_text` / `tag_links_edit` — text-level rendering
and the WorkspaceEdit producer.
Phase 13 — file-operations capability (§12.4):
- `ServerCapabilities.workspace.file_operations` now advertises
`did_rename` + `did_delete` with filters scoped to `**/*.wiki`
and `**/*.md`. (will_* deferred — those return a WorkspaceEdit
and we'd want to wire them into the rename pipeline before
promising support.)
- `did_rename_files` handler — moves the live DocumentState forward
to the new URI when the file was open, then refreshes the index
(remove old URI, upsert new via read_or_open).
- `did_delete_files` handler — drops both DocumentState and index
entry so backlink lookups don't surface dangling source URIs.
Tests: 15 new in `phase17_backfill.rs` covering tag hit filtering &
ordering, snapshot dedup, single-tag and full-index render shapes,
edit replace-vs-insert flow, JSON shape of `TagHit`, and the COMMANDS
list. Total 324 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e75ad6ca89 |
phase 16: diary path conventions + nav commands
Hand-rolled date primitive (no chrono/time dep) plus the diary
command surface that closes the `:VimwikiMakeDiaryNote` ergonomics gap.
nuwiki-core/src/date.rs:
- `DiaryDate { year, month, day }` with strict `YYYY-MM-DD` parsing
(rejects missing zero-padding, alternate separators, whitespace,
impossible calendar dates including the leap-year exceptions).
- `next_day` / `prev_day` via Howard Hinnant's days-from-civil
algorithm — proleptic Gregorian, walks across month/year/leap-day
boundaries.
- `today_utc()` from `SystemTime::now()`. UTC-only by design: a
local-tz implementation would need a tz database we don't ship.
- `Ord` by epoch days, `Display`/`format` round-trip with `parse`.
WikiConfig (P15 resolution):
- `diary_rel_path: String` (default `"diary"`) — mirrors vimwiki's
`g:vimwiki_diary_rel_path`.
- `diary_index: String` (default `"diary"`) — the stem inside the
diary subdir.
- `diary_dir()`, `diary_index_path()`, `diary_path_for(date)` —
centralise the path math so commands stay declarative.
- `RawWiki` (init-options wire format) accepts both fields, so users
with `g:vimwiki_diary_rel_path = "journal"` migrate cleanly.
WorkspaceIndex:
- New `diary_rel_path: Option<String>` field set by `Wiki::new`.
- `IndexedPage.diary_date: Option<DiaryDate>` — populated in `upsert`
when the URI is under `<root>/<diary_rel_path>/` and the stem
parses. `diary_date_for_uri` is the standalone classifier.
- The classifier *requires* the URI to be inside the diary subdir
when a root is known; with no root it accepts any file whose stem
is a date (kept for the ad-hoc test/scratch case).
nuwiki-lsp/src/diary.rs:
- `uri_for_date` / `index_uri` — bridge from `DiaryDate` and config
to `file://` URIs.
- `list_entries(index) → Vec<DiaryEntry>` — ascending, plus
`list_entries_filtered(year, month)` for calendar hooks.
- `next_entry` / `prev_entry` — strict less-than/greater-than
comparison so navigating from a non-diary page (uses today as
pivot) still moves to the right entry.
- `build_index_body` — newest-first, grouped under `== YYYY ==` /
`=== Month ===` subheadings, emits `- [[diary/YYYY-MM-DD]]`. Matches
`:VimwikiDiaryGenerateLinks`.
- `DiaryEntry` JSON shape: `{ date: "YYYY-MM-DD", uri }`. Manual
serde impl so DiaryDate stays serde-free in nuwiki-core.
Commands (9 new):
- `nuwiki.diary.openToday` / `openYesterday` / `openTomorrow` →
`{ uri, date }`.
- `nuwiki.diary.openIndex` → `{ uri }`.
- `nuwiki.diary.generateIndex` → WorkspaceEdit. Three paths:
live document in `backend.documents` → full-doc replace; on-disk
index → read + full-doc replace; missing → CreateFile + insert.
- `nuwiki.diary.next` / `prev` → pivots on the current page's
`diary_date`, falls back to today.
- `nuwiki.diary.listEntries` → optional `year` / `month` filter.
- `nuwiki.diary.openForDate` → strict `YYYY-MM-DD` arg.
All commands accept an optional `uri` to scope to a particular wiki
when multi-wiki lands; the dispatcher falls back to `default_wiki()`.
Tests: 35 new in `phase16_diary.rs` (date parser exhaustively,
calendar arithmetic across boundaries + leaps, path conventions,
upsert classification including the "date filename outside diary dir"
no-op case, list/filter/nav, index body grouping, serde shape, command
list completeness). Total 309 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ce3ac8c4f8 |
phase 15: link health + TOC/links/orphans
New diagnostics module hosts the `nuwiki.link` source: walks every
`WikiLinkNode` and emits a diagnostic per broken target. Severity is
gated by `cfg.link_severity` (off | hint | warn | error). Wired into
`collect_diagnostics` so didOpen/didChange publish link diagnostics
alongside parse errors.
Classification (`BrokenLinkKind`):
- `Wiki` target missing from the workspace index → MissingPage.
- `Wiki`/`AnchorOnly` target with an anchor that matches neither a
heading nor a `:tag:` on the resolved page → MissingAnchor.
- `file:` / `local:` target whose resolved path isn't on disk →
MissingFile. Absolute paths are used as-is; relative paths resolve
against the source URI's parent directory, falling back to the wiki
root.
- Interwiki/Diary/Raw/external — never diagnosed.
`classify_link` works off a `&WikiLinkNode`; `classify_outgoing` does
the same job from a cached `IndexedPage.outgoing` entry so the
workspace-wide checker doesn't need to re-parse every page.
New commands:
- `nuwiki.toc.generate` — generate `= Contents =` heading + nested
list of `[[#anchor|Title]]` entries from current headings. Replaces
any existing h1 "Contents" + its immediate following list
case-insensitively; inserts at line 0 otherwise. Skips the TOC's
own heading so re-generation is idempotent.
- `nuwiki.links.generate` — equivalent of `:VimwikiGenerateLinks`.
Flat alphabetical list under `= Generated Links =`, excludes the
current page.
- `nuwiki.workspace.checkLinks` — returns `Vec<BrokenLinkEntry>` with
`{ uri, range, kind, message }` per broken link across the wiki.
Open documents use live text for range conversion; closed pages
fall back to the stored span coords. Accepts an optional `uri` to
pick a specific wiki; defaults to the first registered one.
- `nuwiki.workspace.findOrphans` — returns `Vec<{ uri, name }>` for
every indexed page with no incoming links. Sorted alphabetically.
Pure ops (`commands::ops`):
- `build_toc_text(items, heading_name)` — formats the TOC body with
2-space indent per level, anchors via `index::slugify`.
- `build_links_text(pages, heading_name, exclude)` — formats the flat
list.
- `find_section_range(ast, heading_name)` — locates an existing h1
section (heading + immediate following list) by case-insensitive
title match.
- `toc_edit` / `links_edit` — full edit producers.
- `collect_workspace_broken_links` / `find_orphans`.
Plumbing:
- `collect_diagnostics` gains a `uri: Option<&Url>` parameter so
link-health can resolve `file:` / `local:` relatives. `ast_diagnostics`
back-compat wrapper preserved.
- `Backend::default_wiki` is no longer `#[allow(dead_code)]`.
Tests: 31 new in `phase15_link_health.rs` covering severity mapping,
classification on every kind/anchor case, TOC nesting, links exclusion,
section-range case-insensitivity, idempotent re-generation, orphan
detection, and the COMMANDS list. The Phase 11 stub test was rewritten
into a real "no index = no diagnostics" assertion. All 274 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f1d046fad1 |
phase 14: list/heading edit commands + CommandOutcome split
Six new executeCommand entries — the cheap surgical edits that account for the bulk of vimwiki users' daily keymap traffic. Heavier commands (table.align/moveColumn/insert, list.changeSymbol/changeLevel/renumber/ removeDone, link.normalize, colorize) are deferred for a follow-up since each needs its own structural rewriter. CommandOutcome split: - `Edit(WorkspaceEdit)` — server applies via apply_edit. - `Value(Value)` — returned verbatim to the client (used by read-only queries like nextTask). - execute_command handler in lib.rs dispatches both cases. New commands: - nuwiki.list.toggleCheckbox — [ ] ↔ [X]; mid-states snap to [X]; rejected reverts to [ ]. - nuwiki.list.cycleCheckbox — [ ] → [.] → [o] → [O] → [X] → [ ]; rejected resets to [ ]. - nuwiki.list.rejectCheckbox — toggle the [-] rejected marker. - nuwiki.list.nextTask — returns Location of the first unfinished task after the cursor; wraps to the start of the doc when none found below. Only items with checkboxes count. - nuwiki.heading.addLevel — = → ==; clamps at 6. - nuwiki.heading.removeLevel — == → =; clamps at 1 (no-op on h1). Pure ops: - ops::toggle_state / cycle_state / reject_state — checkbox state transitions. - ops::checkbox_edit / next_task / heading_change_level — end-to-end edit producers. Tests drive these directly without spinning up a Backend. - ops::rewrite_heading_with_level — text-level rewrite preserving the leading whitespace that marks a centered heading (lives outside the heading span, so the edit doesn't touch it). - ops::find_list_item_at / find_checkbox_span / item_in_list — AST + source-text helpers, pub for downstream tooling. Tests (21 new): - State transitions: round-trip, progression, dash toggle. - checkbox_edit: empty → done, partial → cycle, reject path, no-op on plain item, no-op off list, nested-sublist resolution. - heading_change_level: h2→h3, level-6 cap, h3→h2, h1 no-op, off-heading no-op, span boundary semantics. - next_task: first-unfinished, wrap-around, all-done returns None, plain items ignored. - COMMANDS list ↔ handler match. All 211 prior tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cb4bfae5e7 |
phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
Server-side rename with cross-document link rewriting (P11's
read_or_open delivers on its promise) and the first executeCommand
entry. Plus the routing infrastructure that Phases 14-17 will hang
their own commands off of.
commands.rs:
- commands::execute(backend, name, args) dispatches by command name
and returns the WorkspaceEdit to apply.
- COMMANDS const slice is the source of truth for what's advertised
in ExecuteCommandOptions.commands.
- First entry: nuwiki.file.delete with args { uri: <Url> } returning
a WorkspaceEdit with a DeleteFile op.
rename.rs:
- compute_rename(backend, target_uri, new_name, utf8):
1. Resolve the wiki via wiki_for_uri.
2. Build new URI from wiki.root + new_name + extension.
3. Snapshot the index's backlinks for the old page name before
releasing the read lock so we don't hold it across awaits.
4. For each backlink, read_or_open the source (live state or
disk + reparse) and validate the cached span against current
text.
5. Rewrite the [[...]] segment with rewrite_wikilink_target,
preserving anchor and description.
6. Add the RenameFile op so the builder emits it before the text
edits in documentChanges ordering.
- build_new_uri handles subdirectory paths and avoids double-
appending the file extension.
- rewrite_wikilink_target is pub so other tooling can call it.
lib.rs:
- Capabilities: rename_provider + execute_command_provider listing
commands::COMMANDS.
- New rename handler: cursor-on-wikilink renames the linked page,
else the buffer's file. Delegates to rename::compute_rename.
- New execute_command handler dispatches via commands::execute, then
pushes the resulting WorkspaceEdit through client.apply_edit. On
unknown command or bad args, logs and returns null.
- read_or_open loses its allow(dead_code) attribute now that Phase
13 calls it.
- rename_target_uri helper shares index-resolution with goto_def.
Tests (11 new in phase13_rename_commands.rs):
- rewrite_wikilink_target: plain, with description, with anchor,
with both, subdirectory path, non-wikilink-text rejection.
- build_new_uri: under root, with subdirectory, extension dedup,
empty path segments skipped.
- nuwiki.file.delete builder smoke test (DocumentChanges + DeleteFile).
All 211 v1.0+Phase 11+12 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8b29d4e9b9 |
phase 12: tags — lexer, AST, index, anchor resolution, ws/symbol
Vimwiki `:tag1:tag2:` syntax across the full stack. P11 resolved as
strict-vimwiki — no markdown `#tag` alias.
AST (nuwiki-core):
- `TagScope { File, Heading(usize), Standalone }` mirrors vimwiki's
three placement-determined meanings: lines 0-1 → File; one or two
lines below the most recent heading → Heading(idx); otherwise
Standalone.
- `TagNode { span, tags: Vec<String>, scope }` + `BlockNode::Tag`.
- `PageMetadata.tags: Vec<String>` accumulates the file-scope tag
names — convenience projection of the Tag blocks whose scope is
File. New field is additive (Default derive) so existing consumers
using `..Default::default()` keep working.
- `Visitor::visit_tag` lands with a default body and `walk_block`
dispatches the new arm.
Lexer (`syntax/vimwiki/lexer.rs`):
- `VimwikiTokenKind::Tag(Vec<String>)` block-level token.
- `try_lex_tag` runs in the normal-line dispatcher right after heading
recognition; demands the *whole trimmed line* match `:name:name…:`,
with non-empty whitespace-free names between adjacent colons. Empty
segments (`::tag::`), whitespace inside a name, or single-colon
lines are all rejected. Doesn't fight `Term::` (definition-list
marker) because that pattern requires text before the `::`.
- Indentation is tolerated; the span covers the trimmed run.
Parser (`syntax/vimwiki/parser.rs`):
- `ParseState` gains `headings_seen` + `last_heading_line`.
- `parse_heading` bumps both on every closed heading so the tag
parser can detect the heading window.
- `parse_tag` reads the Tag token, computes the scope via
`scope_for_tag(tag_line)`, eats the trailing newline, and emits the
TagNode.
- `parse_document` post-processes each block: when a Tag with
`scope == File` is emitted, its names are pushed (de-duped) into
`metadata.tags`.
Renderer (`render/html.rs`):
- `render_tag` emits `<div class="tags"><span class="tag"
id="tag-name">name</span>...</div>`. The `id` is what makes
`[[Page#tag-name]]` jump to the rendered tag.
Semantic tokens (`semantic_tokens.rs`):
- New `TOKEN_TAG = 19` / `vimwikiTag` so editors highlight tag lines
distinctly. `emit_block` adds the Tag arm.
Workspace index (`index.rs`):
- `IndexedPage.tags: Vec<TagInfo>` records each tag occurrence on a
page with its span + scope.
- `WorkspaceIndex.tags_by_name: HashMap<String, Vec<TagOccurrence>>`
is the reverse map for cross-page anchor resolution + the Phase 13
`nuwiki.tags.search` command.
- `upsert` populates both; `remove` scrubs the reverse map and drops
empty entries; `tags_for(name)` is the public lookup.
LSP handlers (`lib.rs`):
- `heading_range_in` falls back to tag matching when the anchor
isn't a heading slug — so `[[Page#some-tag]]` lands on the matching
TagNode. Heading anchors win over tag anchors when both match (the
vimwiki precedence).
- `workspace/symbol` includes tags alongside headings, named `:tag:`
and kinded `PROPERTY` so editor UIs can distinguish them.
Tests (20 new):
- Syntax (15 in `vimwiki_tags.rs`):
lexer recognition + rejection of `::tag::`, whitespace-in-name,
Term::-conflict; parser scope paths (file, heading-1-below,
heading-2-below, standalone, no-heading, line-1 priority);
metadata dedup; renderer HTML + escaping.
- LSP index (5 in `tags_index_and_lsp.rs`):
per-page tag population, cross-page `tags_by_name` aggregation,
re-upsert replaces stale tags, `remove` cleans the reverse map,
heading-scope tag indexed but not in file metadata.
All 191 v1.0 + Phase 11 tests still pass — backward compatible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ea574f23f8 |
phase 11: plumbing prereqs — Wiki, edits, config, snapshot, diags
Cross-cutting infrastructure that every v1.1 phase depends on. Lands as a numbered phase before any user-visible feature so Phases 12–19 stay mechanical. 5 pieces: - `config.rs` — `Config` + `WikiConfig` + `DiagnosticConfig` + `LinkSeverity`. Serde-deserialised from `initialization_options`. v1.0 single-wiki shape (`wiki_root` + `file_extension` + `syntax`) desugars to one-entry `wikis = [...]` per P16. `~/...` expansion without pulling in `dirs`. Fallbacks: workspace_folders[0] → deprecated root_uri → empty list. - `wiki.rs` — `Wiki` aggregate (id + WikiConfig + Arc<RwLock<Index>>). `build_wikis(&[WikiConfig])` materialises the list; `resolve_uri_to_wiki` picks the longest-prefix root match so nested wikis route correctly. Single-entry today, multi-entry once Phase 18 lands. - `edits.rs` — `WorkspaceEditBuilder` + `text_edit_replace/insert/delete` + `op_rename/delete/create`. Handles the UTF-8/UTF-16 conversion at the span boundary so each Phase 13+ command stays a 3-liner. Builder picks `documentChanges` when file ops are present (LSP requires it), `changes` map otherwise (works for LSP 3.15- clients). File ops precede content edits in the output so created/renamed files exist when edits apply. `DeleteFile` is built without `annotation_id` — lsp-types 0.94.x ships it that way; later versions added the field. - `Backend` refactor — `index: Arc<RwLock<WorkspaceIndex>>` replaced by `wikis: Arc<RwLock<Vec<Wiki>>>` + `config: Arc<RwLock<Config>>`. New `wiki(id)` / `default_wiki()` / `wiki_for_uri(uri)` helpers. Every v1.0 handler (definition, references, hover, completion, workspace/symbol, update_document) goes through `wiki_for_uri`. `initialize` builds wikis from `Config::from_init_params`; `initialized` spawns one indexer task per wiki with the existing `window/workDoneProgress` wrapper. New `did_change_configuration` handler re-applies settings (rebuild semantics revisited in Phase 18). The old `workspace_root_from_params` helper is gone. - `read_or_open` + `DocSnapshot` (scaffolded, `#[allow(dead_code)]` until Phase 13's `workspace/rename` consumes it). Returns the live document when held in the documents map, otherwise async-reads from disk and re-parses so cross-document operations get accurate text and spans instead of trusting stale `WorkspaceIndex` data. - `collect_diagnostics` composable collector. Same `ErrorNode` walk today; takes `Option<&WorkspaceIndex>` + `page_name` + `&DiagnosticConfig` so Phase 15 can drop a link-health source in without further refactor. `ast_diagnostics` kept as a thin wrapper for the v1.0 test surface. New deps: `serde` + `serde_json` (already transitive through tower-lsp; elevated to direct deps for clarity and stability). `tokio` gains the `fs` feature for `read_or_open`'s async disk read. Tests (19 new in `phase11_plumbing.rs`): - Edits: UTF-8 passthrough + UTF-16 conversion (`héllo`), insert/delete shape, builder mode selection, file-op accumulation, `op_create` round-trip - Config: defaults, v1.0 desugar, explicit list overrides legacy, empty JSON, invalid JSON preserves prior state - Wiki: sequential id assignment, longest-prefix resolution, no-match, `Wiki::contains` - Diagnostics: error-node composition, link-health stub returns empty in Phase 11 All 172 v1.0 tests still pass — backward compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cb244a8080 |
spec: add Phase 11 plumbing prereqs; resolve P14; renumber 12+
Architecture-review follow-through. The v1.1 plan as originally written started directly at Tags, which would have meant retrofitting five cross-cutting pieces into Phases 12–19 ad hoc. This commit lifts that infrastructure into its own numbered phase up front so the rest of v1.1 stays mechanical. Phase table (§10): inserts new "Phase 11 — Plumbing prerequisites"; Tags becomes 12, Workspace edits 13, …, Editor glue v2 19. v1.1 now has 9 phases instead of 8. §12.2 new "Plumbing prerequisites" subsection covers: - Server config receipt (initializationOptions + didChangeConfiguration) - WorkspaceEditBuilder + span helpers (text_edit_*, op_*) - Closed-doc loader (read_or_open, returns DocSnapshot) - Diagnostic source chain (collect_diagnostics with composable sources) - Wiki aggregate (lands single-entry, multi-entry config in Phase 18) Plus explicit "what does *not* change" and a tests-list. §11 decisions: - P14 (folding) resolved: LSP textDocument/foldingRange (primary) + foldexpr fallback in ftplugin. Server already has the AST + heading boundaries; duplicating that in VimL would be lossy. - P10/P11/P12/P13/P15/P16/P17 phase references bumped by one. - P18 added: server-config transport (initializationOptions + didChangeConfiguration, both). Targeted notes in the renumbered subsections: - §12.4 (Workspace edits): closed-doc rewrites use the Phase 11 loader and re-parse on demand — no trust in stale index spans. - §12.7 (Diary): roll-our-own YYYY-MM-DD parser; no chrono dep. - §12.9 (Multi-wiki): data structures already land in Phase 11, so Phase 18 is a config-shape change, not a refactor. Subsections 12.3 → 12.13 renumbered; phase labels in headers updated to match the new §10 table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f435679814 |
spec: define v1.1 — full vimwiki replacement (Phases 11–18)
v1.0 shipped parsing + highlighting + navigation; the gap-analysis vs
github.com/vimwiki/vimwiki shows nuwiki covers ~95% of the syntax but
0% of the daily-authoring command surface (≈40 :Vimwiki* commands and
≈60 keymaps), the diary, tags, link health, HTML export commands, and
multi-wiki. v1.1 closes that gap without architectural changes — every
new editing operation lands as an LSP executeCommand returning a
WorkspaceEdit, so nuwiki-core stays the single source of truth.
Spec additions:
- Status line: v1.0 shipped; v1.1 in design.
- §1: v1.1 scope summarised; multi-wiki moved out of "non-goals (v1.0)"
and into v1.1.
- §10: 8 new phase rows (11–18) under a v1.1 divider.
- 11 Tags, 12 Workspace edits + executeCommand, 13 List/table edit
commands, 14 Link health + TOC/index generation, 15 Diary,
16 HTML export commands, 17 Multi-wiki, 18 Editor glue v2.
- §11: 8 new pending decisions (P10–P17): command namespace, tag
syntax, list-edit transport, multi-wiki config shape, folding
mechanism, diary path scheme, v1.0 config back-compat, markdown
syntax inclusion.
- §12: new top-level v1.1 plan with 12 subsections covering stability
commitment, tags spec (AST + index), workspace-edit semantics,
~15 nuwiki.* edit commands, link-health diagnostics, diary path
conventions + commands, HTML export commands + per-wiki options,
multi-wiki schema, full editor-glue v2 (:Vimwiki* compatibility
table, default keymaps, text objects, folding, insert-mode behaviour,
health-check additions), updated user-config schema example,
§9 syntax checklist additions (tags, hex colour, table alignment,
multi-line lists), CI/CD notes.
v1.0 stability is preserved: AST + Visitor + LSP capabilities +
HtmlRenderer output + v1.0 config keys are all backward-compatible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
02291c2744 |
phase 10: release pipeline (Gitea release on v* tag)
P9 resolved (SPEC §11): stay manual. CI builds the four Linux targets
via cross; macOS + Windows binaries are produced ad hoc by maintainers
and uploaded to the same Gitea release.
§8.2/§8.4 updated: crates.io publish is deferred for now (workflow
ships the Gitea release only; can be re-enabled with a one-line
`cargo publish -p nuwiki-core` job once we're ready to publish). The
CARGO_REGISTRY_TOKEN row in §8.4 is struck through accordingly.
release.yaml:
- Trigger: `push` on tags matching `v*`.
- build matrix (4 jobs): x86_64/aarch64 × gnu/musl. Each pins
Rust 1.83, caches `~/.cargo/{bin,registry,git}` + `target` keyed
on target + Cargo.lock hash, installs `cross 0.2.5` if not in
cache, runs `cross build --release -p nuwiki-ls`, packages as
`nuwiki-ls-{version}-{target}.tar.gz`, uploads as an artifact.
- release job: `needs: build`. Downloads all build artifacts via
download-artifact@v4 with `merge-multiple: true`; installs jq +
curl if missing; POSTs a release-create payload to
`/api/v1/repos/$REPO/releases` using `RELEASE_TOKEN`, then streams
each `.tar.gz` to `/releases/{id}/assets?name=…`. Up-front check
fails fast if the secret is unset.
README + SPEC top-line status: "All phases (0–10) complete." 172
tests still green locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ee61d40872 |
phase 9: editor glue — plugin, LSP wiring, install, health, docs
P5 + P6 resolved (SPEC §11): Neovim 0.11+ and Vim 9.1+.
Universal Vim entry:
- plugin/nuwiki.vim dispatches Vim vs Neovim. Neovim path waits for
the user's `require('nuwiki').setup()` call; Vim path starts the
LSP client on `FileType vimwiki` and exposes `:NuwikiInstall`.
- ftdetect/nuwiki.vim associates `*.wiki` with the `vimwiki` filetype.
- ftplugin/nuwiki.vim sets commentstring + comments + formatoptions +
suffixesadd + iskeyword, with a clean `b:undo_ftplugin`.
- syntax/nuwiki.vim ships default `@vimwiki*` highlight links for the
semantic-token legend (with `level1..6` + `centered` modifiers),
plus a minimal regex fallback so static highlighting works before
the LSP attaches.
Lua glue (Neovim 0.11+):
- init.lua exposes setup() / install() / health() — wiring only per
SPEC §6.2.
- config.lua holds the §7.5 schema (wiki_root / file_extension /
syntax / log_level) and a tbl_deep_extend apply().
- lsp.lua registers via `vim.lsp.config{} + vim.lsp.enable` on 0.11+;
falls back to a FileType autocmd calling `vim.lsp.start` on older
builds. Root dir walks up for `.git` / `.nuwiki`, then uses
wiki_root.
- install.lua does target-triple detection (Linux gnu/musl x x86_64/
aarch64, macOS arm/x86, Windows), curl+tar download from the
release URL, cp/chmod into bin/, with a cargo fallback. Honours
`g:nuwiki_build_from_source`.
- health.lua implements `:checkhealth nuwiki` per §7.6.
VimL glue (Vim 9.1+):
- autoload/nuwiki/lsp.vim follows §7.4 preference order:
vim-lsp (calls `lsp#register_server`) → coc.nvim (prints the
coc-settings.json snippet) → error.
- scripts/download_bin.vim mirrors the Lua installer for Dein /
vim-plug build hooks — same target triples, same download →
fallback → cargo build chain.
Help: doc/nuwiki.txt with tags for install (lazy.nvim / vim-plug /
Dein), config options, health, LSP feature list, `:NuwikiInstall`.
Rust workspace unchanged this phase; 172 tests still green locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4d461d2425 |
phase 8: navigation — definition, refs, hover, completion, ws/symbol
P8 resolved (SPEC §11): lazy + background indexing with progress. Initial scan runs in a tokio task spawned from `initialized`; the server stays responsive immediately and nav features answer with partial data until the index is complete. WorkspaceIndex (`index.rs`): - Per-URI `IndexedPage` keeps name, title, headings, outgoing links. - Reverse maps: `pages_by_name`, `backlinks: HashMap<page, …>`. - `upsert` scrubs stale entries first so re-parses don't leak. - `resolve` handles Wiki + AnchorOnly link kinds against the index; other kinds (Interwiki/Diary/File/Local/Raw) fall through. - `slugify` for anchors (lowercases, collapses runs, drops trailing dashes, preserves Unicode), `page_name_from_uri` for workspace- rooted names, `walk_wiki_files` for the background scanner. Backend wiring (`lib.rs`): - `initialize` captures workspace root from `workspaceFolders` or the deprecated `root_uri`; advertises definition/references/hover/ completion (trigger `[`)/workspace_symbol providers. - `initialized` spawns the indexer, wrapped in `window/workDoneProgress` begin/end notifications. - `did_open`/`did_change` synchronously update the index alongside publishing diagnostics; `did_close` keeps the indexed projection (file may still exist on disk and other pages link to it) and only clears diagnostics + drops the live document state. Position lookup (`nav.rs`): - `lsp_to_byte_pos` translates between LSP encoding (UTF-8 or UTF-16) and our byte columns. - `find_inline_at` descends blocks → inlines → inline containers, returning the most specific inline whose span covers the cursor. Handlers: - definition: wikilinks resolve to a target URI; anchored links return the range at the matching heading. - references: backlinks of the cursor's link target, or of the current page when the cursor isn't on a link. Ranges fall back to byte-column LSP positions when the source doc isn't open. - hover: markdown preview with page title + outline (first 8 headings), or a "not in index" fallback. - completion: fires only inside an unterminated `[[ … ` on the current line; suggests every indexed page name. - workspace/symbol: case-insensitive substring over every indexed heading. Tests (20 new): slugify (basic / punctuation / Unicode / trailing dash), page-name derivation under various roots, upsert / replace / remove, resolve for Wiki + AnchorOnly, sorted `page_names`, UTF-8 passthrough + UTF-16 conversion, position lookup hits and misses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6efe154f1a |
phase 7: semantic tokens for syntax highlighting
`textDocument/semanticTokens/full` + `/range` per SPEC §6.9. Token
type scheme settled in P7: custom `vimwiki*` types
(`vimwikiHeading`, `vimwikiBold`, `vimwikiCodeBlock`, `vimwikiKeyword`,
`vimwikiWikilink`, `vimwikiTransclusion`, …) plus `level1`..`level6`
and `centered` modifiers. Default highlight groups for these arrive
in Phase 9.
Pipeline (`semantic_tokens.rs`):
- `legend()` builds the `SemanticTokensLegend` advertised in
`initialize` capabilities.
- `LineIndex` precomputes byte offsets per line so multi-line span
splitting and UTF-16 conversion stay linear.
- `collect_tokens` walks the AST. Blocks that are visually distinct
(heading, preformatted, math-block, comment) emit one token over
their span; multi-line ones get split per line because the LSP wire
format can't represent a token that crosses a newline. Container
blocks (paragraph, list item, table cell, blockquote) recurse into
inlines instead of wrapping them, so we end up with a non-overlapping
tile of inline tokens. Headings shadow nested inlines (one heading
token covers the line, no Bold-inside-Heading).
- `encode` produces the LSP delta-quintuple stream
`(deltaLine, deltaStart, length, type, mods)`. UTF-16 mode converts
byte offsets/lengths to code-unit counts on the fly (BMP + surrogate
pairs).
- `build_data` is the entry point used by both handlers; the `range`
variant filters tokens by overlap in the same encoding the client
requested.
Capability declared as `SemanticTokensOptions { full = true,
range = true, legend = … }`. Both handlers reuse the document store.
P7 resolved in SPEC §11 — custom vimwiki types with rationale and a
note that Phase 9 will ship default highlight-group definitions in
`syntax/nuwiki.vim` and the Lua glue.
Tests (21 new): legend integrity, per-construct emission for every
inline + block kind, heading shadowing of inner inlines, multi-line
code/math block splitting, sort order, delta encoding (same line and
crossing lines), UTF-16 length conversion for é, range filter,
empty-doc edge case, end-to-end `build_data` parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c44db37bab |
phase 6: LSP foundation — didOpen/didChange, diagnostics, outline
`tower-lsp` server (`nuwiki-lsp::run` + `run_stdio`) wired up by the `nuwiki-ls` binary. Backend holds an `Arc<DashMap<Url, DocumentState>>` per SPEC §6.10 plus an `Arc<SyntaxRegistry>` pre-loaded with `VimwikiSyntax`. Re-parse is full per change; incremental parsing deferred post-v1 per spec. Capabilities declared: - `text_document_sync = FULL` - `document_symbol_provider = true` - `position_encoding = UTF-8` when the client opts into LSP 3.17+ encodings (SPEC §6.11). Otherwise we keep the legacy UTF-16 default and translate spans through a per-line UTF-16 code-unit walk that handles BMP and surrogate pairs alike. Handlers: `initialize` (negotiate encoding), `initialized` (log it), `shutdown`, `did_open` / `did_change` (full re-parse + `publishDiagnostics`), `did_close` (drop state, clear diagnostics), `document_symbol` (nested outline). Diagnostics: every `BlockNode::Error` (from §6.7's resilient parser) emits one `DiagnosticSeverity::ERROR` with `source = "nuwiki"`. The walker descends into Blockquotes and ListItems so errors nested inside those don't slip through. Document outline: nested `DocumentSymbol` tree built recursively from heading levels (`= H1 =` → root, `== H2 ==` → child of preceding H1, etc.). Titles flatten inline markers via `inline_to_text` so `*world*` shows up as `world` in the outline. Pure helpers (`to_lsp_position`, `to_lsp_range`, `ast_diagnostics`, `headings_to_symbols`) are `pub` so the integration tests can verify conversion correctness without spinning up the async server. End-to- end LSP message exercises will land in Phase 8 alongside navigation. Tests (11): UTF-8 passthrough, UTF-16 conversion for é + 🦀, single and multi-error diagnostic emission, blockquote descent, flat outline, nested outline by levels, inline-marker stripping in titles, empty- heading fallback, registry-dispatch parity. Dep pin: `idna_adapter` held at 1.2.0 — 1.2.2 needs Cargo's edition2024 feature (Rust 1.85), our MSRV is 1.83. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f00b0780b8 |
phase 5: renderer trait + HtmlRenderer
Writer-based `Renderer` trait per SPEC §6.8: object-safe
`fn render(&self, doc, w: &mut dyn Write) -> io::Result<()>` plus
a `render_to_string` convenience. Object safety preserves the option
of `Box<dyn Renderer>` in the LSP layer.
`HtmlRenderer` builder accepts a link-resolution callback (via
`with_link_resolver`) and an optional enclosing template (via
`with_template`). The default resolver mirrors §9: Wiki paths become
`path.html`, interwiki links land in sibling directories
(`../wiki<N>/` / `../wn-<Name>/`), diary entries under `diary/`,
file/local schemes use literal paths, anchor-only collapses to `#`.
Renderer covers every AST node:
- block: heading (centered class), paragraph, hr, blockquote,
preformatted (`language-<lang>` class), math block (with
`\begin{env}` when an environment is set), lists (ordered/unordered
+ every checkbox state, plus recursive sublists), definition list
(dt/dd), table (thead/tbody driven by `is_header`, col/row span as
CSS class hooks), comment (as HTML comment with `--` neutralised),
error node (as `.parse-error` span)
- inline: strong/em + bold-italic, `<del>`, code (escaped),
`<sup>`/`<sub>`, math-inline wrapped in `\(...\)`, keyword spans
with per-keyword class, color spans, wikilink/external/raw URL
anchors, transclusion as `<img>` with sorted attrs
HTML escaping is byte-level (`<>&"'`). Template substitution replaces
`{{content}}` first so a body containing the literal `{{title}}`
isn't double-substituted.
Tests (31 new) cover every block + inline construct, every LinkKind
through the default resolver, HTML escaping, custom resolver
override, template with/without title metadata, and an end-to-end
smoke test on a multi-construct document.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
294963b517 |
phase 4: vimwiki parser + SyntaxPlugin glue
Hand-rolled recursive-descent parser over the token stream. Resilient per SPEC §6.7: never aborts the document — anything the dispatcher can't claim becomes BlockNode::Error and the cursor advances, and unterminated wikilinks / transclusions / delimiters fall back to literal text. Block coverage: every variant from §6.4 — Heading (with inline content between markers), Paragraph (with soft-break across single newlines), HorizontalRule, Blockquote (both `>` and 4-space styles), Preformatted, MathBlock, List (with checkbox + indent-driven sublists), DefinitionList, Table (header separator promotes the preceding row, plus colspan / rowspan cells), Comment (single + multiline), and page-level placeholders threaded into PageMetadata. Inline pairing: left-to-right scan with "find next matching delim", recursing on the slice between. `_*x*_` becomes Italic(Bold(...)), mirroring SPEC §6.7 precedence. URLs inside `[[ ]]` route to ExternalLinkNode; everything else to WikiLinkNode with full LinkTarget classification (Wiki / Interwiki numbered + named / Diary / File / Local / AnchorOnly / directory / root-relative / filesystem-absolute). VimwikiSyntax registers as id="vimwiki", extensions=[".wiki"] and chains lexer + parser inside SyntaxPlugin::parse. SPEC §4 updated to record the hand-rolled choice with rationale — span-bearing tokens fit awkwardly into winnow/nom combinator style; resilience and recovery are explicit in code instead. Tests (41 new parser cases) cover every AST construct, inline precedence including nested bold/italic, every LinkKind, transclusion alt + attrs, resilience on unterminated links, and end-to-end SyntaxRegistry dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
942dbe2aa8 |
phase 3: vimwiki lexer
Two-pass, hand-rolled lexer per SPEC.md §6.6. Block pass walks the
source line by line; recognised constructs emit structural tokens, and
text-bearing line content is fed to the inline pass. Multi-line fences
(`{{{ }}}`, `{{$ }}$`, `%%+ +%%`) flip a `BlockMode` so subsequent
lines are captured raw until the matching closer.
VimwikiToken covers the full §9 feature checklist:
- headings (1–6, centered), horizontal rule, blockquotes (`>` and
4-space), all list-marker variants, checkboxes, definition `::`,
tables (separators, header row, col/row span)
- preformatted fences (with language + key=val attrs), math blocks
(with `%env%`), single- and multi-line comments, all four
page placeholders (%title / %nohtml / %template / %date)
- inline: bold/italic/strike/super/sub delimiters, inline code, inline
math, all six keywords (word-bounded), wikilinks (with description
separator), transclusions (with attr separator), raw URLs across
http(s)/ftp/mailto/file schemes — trailing sentence punctuation
stripped from URL spans
Spans are byte-accurate per SPEC §6.5 (0-indexed line + byte column +
absolute byte offset). The lexer is permissive: every delimiter is
emitted, the parser will pair them and fall back to literal text on
mismatches.
Tests (41 cases) cover one example per construct in §9, plus
word-boundary keyword detection, list-vs-bold disambiguation,
indent-vs-list disambiguation, and a span-correctness check.
README now carries a per-phase status table; SPEC top-line status
advanced to Phase 3 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8f35c0a80c |
phase 2: syntax plugin interface and registry
Add the Lexer/Parser/SyntaxPlugin traits and the SyntaxRegistry per SPEC.md §6.3. - `Lexer<Token>` and `Parser<Token>` are typed building blocks: each syntax owns its own token alphabet (vimwiki today, markdown later). - `TokenStream<T>` is a `Vec<T>` newtype so the eager-vs-lazy choice (§6.6) can flip later without breaking callers. - `SyntaxPlugin` is type-erased: it exposes id / display_name / file_extensions / parse(text) so the registry can hold heterogeneous plugins behind a single trait object. Implementations chain their Lexer + Parser inside parse(). - `SyntaxPlugin: Send + Sync` so the LSP server can stash plugins in an Arc and share them across handler tasks. - `SyntaxRegistry` stores `Arc<dyn SyntaxPlugin>`, supports lookup by id and by file extension, iteration in registration order. Tests cover TokenStream round-trip, full lex→parse chain through a mock plugin, registry lookup hits and misses, and a compile-time assert_send_sync::<SyntaxRegistry>(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dc2a952e67 |
phase 1: core AST, spans, and Visitor
Define every node type in SPEC.md §6.4 — Document, 11 block variants, 15 inline variants (including the 4 link-related ones), plus ListItem / DefinitionItem / TableRow / TableCell, ListSymbol, CheckboxState, Keyword, LinkTarget, LinkKind. Every node carries a `Span` (line + column + byte offset, 0-indexed) per §6.5. Visitor: open-recursion trait with default `visit_*` bodies that delegate to `walk_*` free functions. Override at any granularity; call `walk_*` from your override to keep descending. P4 resolved by defining the trait now so Phase 5 (renderer) and Phase 7 (semantic tokens) can plug in without refactoring traversal. P3 resolved: AST string fields are owned `String`. Cow / Arc<str> can be revisited if the parser grows a zero-copy mode. Tests: visitor smoke test builds a heading + paragraph + list + table by hand and asserts exact visit counts (4 blocks, 12 inlines, 9 text leaves, 1 bold, 1 external link, 1 transclusion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cf336ee839 |
phase 0: scaffold workspace, plugin layout, and CI
Lay down the empty repo skeleton defined in SPEC.md §5 so subsequent phases have somewhere to land: - Cargo workspace (resolver v2, edition 2021, MSRV 1.83) with nuwiki-core, nuwiki-lsp, nuwiki-ls — dep direction matches §6.2. - Vim/Neovim plugin directory layout (plugin, lua, autoload, ftdetect, ftplugin, syntax, doc, scripts) with header-only stubs. - .gitea/workflows/ci.yaml running fmt, clippy -D warnings, and tests pinned to Rust 1.83. - README, dual MIT/Apache-2.0 license texts, .gitignore. Verified: cargo check / fmt --check / clippy / test all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cf751383e1 | initial setup |