Commit Graph

227 Commits

Author SHA1 Message Date
gffranco 02172d9e25 fix(config): accept integer 0/1 for bool fields in RawWiki deserialization
CI / cargo fmt --check (push) Successful in 31s
CI / cargo clippy (push) Successful in 39s
CI / cargo test (push) Successful in 36s
CI / editor keymaps (push) Successful in 1m25s
VimL dicts serialise boolean-like settings such as `auto_export = 1`
as the JSON number `1`, not `true`.  serde's default `Option<bool>`
deserializer rejects that, causing `serde_json::from_value::<InitOptions>`
to return `Err` and `.ok()` to silently discard the entire wikis list.
The server then fell back to `wiki_root` and could not resolve per-wiki
links, marking every cross-wiki link as broken.

Add an `opt_bool_or_int` serde helper that accepts JSON bool, `null`,
or integers (0 = false, non-zero = true), and apply it to `auto_export`,
`html_filename_parameterization`, `listsyms_propagate`, and `auto_toc`
in `RawWiki`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:03:36 -03:00
gffranco 8d11f7daef fix(lsp): send g:nuwiki_wikis to the server so per-wiki roots are known
CI / cargo fmt --check (push) Successful in 34s
CI / cargo clippy (push) Successful in 19s
CI / cargo test (push) Successful in 39s
CI / editor keymaps (push) Successful in 1m46s
The server received only wiki_root (the parent directory) and had no
knowledge of the per-wiki sub-roots configured in g:nuwiki_wikis / the
setup() wikis list. It therefore resolved all links relative to the
parent root, causing every link in a sub-wiki to appear broken.

Vim path (autoload/nuwiki/lsp.vim):
  s:settings() now includes a 'wikis' key when g:nuwiki_wikis is set,
  so the initialization_options and settings payloads carry the full
  per-wiki config (root, diary_rel_path, file_extension, …).

Lua path (lua/nuwiki/lsp.lua):
  Add resolved_opts() which merges vim.g.nuwiki_wikis into
  config.options.wikis when the user configures via VimL rather than
  setup({wikis=…}). init_options(), server_settings(), and the
  pre-0.11 root_dir_for() fallback all use resolved_opts() so the
  server receives the correct per-wiki roots in every code path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:45:04 -03:00
gffranco b3e2a72023 fix(lsp): add coc.nvim dispatch path to all Vim-side LSP calls
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 37s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m18s
Commands were hardwired to vim-lsp (lsp#send_request, :LspDefinition,
:LspReferences, :LspRename). Users running coc.nvim got "vim-lsp not
loaded" on every command despite the plugin correctly detecting coc.

Changes:

autoload/nuwiki/commands.vim
- Add s:has_coc() (checks CocActionAsync exists)
- Add s:coc_wrap() adapter — converts coc's (err, result) callback into
  the vim-lsp notification shape {response:{result:…}} so all existing
  on_notification handlers work unchanged
- s:exec(): try vim-lsp first, fall back to CocActionAsync('runCommand',
  …) with arguments spread as positional params, then error if neither
- Add s:jump_definition() helper — :LspDefinition vs CocActionAsync
- Add nuwiki#commands#backlinks() — :LspReferences vs CocActionAsync
- badd_link(): coc path uses CocAction('getDefinition') to resolve the
  target URI without navigating
- follow_link_or_create(): replace inline :LspDefinition checks with
  s:jump_definition()
- rename_file(): :LspRename vs CocActionAsync('rename')

ftplugin/vimwiki.vim
- VimwikiFollowLink / NuwikiFollowLink: dispatch through
  nuwiki#commands#follow_link_or_create() instead of :LspDefinition
- VimwikiBacklinks / NuwikiBacklinks / VWB: dispatch through
  nuwiki#commands#backlinks() instead of :LspReferences

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:39:27 -03:00
gffranco b7d2a6c61a fix(install): resolve plugin root at source time to fix NuwikiInstall E484
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 22s
CI / cargo test (push) Successful in 45s
CI / editor keymaps (push) Successful in 1m24s
<sfile> inside a command! body is evaluated at execution time, not at
definition time. When :NuwikiInstall is run interactively there is no
sourcing context, so <sfile> expands to nothing and the path collapses
to ./scripts/download_bin.vim — producing E484.

Fix: capture the path in s:plugin_root immediately while the script is
being sourced, then reference that variable from the command body.

Also hoist :NuwikiInstall above the early `finish` for Neovim so the
command is available in both editors (Neovim routes to
require('nuwiki.install').install(), Vim sources the download script).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:21:50 -03:00
gffranco 2c0cefb5c5 fix(mappings): respect g:nuwiki_wikis in global entry-point helpers
CI / cargo fmt --check (push) Successful in 49s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 36s
CI / editor keymaps (push) Successful in 1m20s
The global <Leader>ww / diary helpers read g:nuwiki_wiki_root directly,
so they always opened the root-level index.wiki even when the user had
configured per-wiki roots via g:nuwiki_wikis (e.g. personal_wiki.root =
'~/.vimwiki/personal_wiki').

Vim path (autoload): replace the three open_*_path functions with a
shared s:wiki_cfg(n) helper that checks g:nuwiki_wikis[n] first, then
falls back to the scalar g:nuwiki_* vars and built-in defaults.

Lua path (init.lua): replace the separate _wiki_index_path /_diary_path
locals with a unified _wiki_cfg() that checks setup() opts.wikis, then
vim.g.nuwiki_wikis (for users who configure via VimL rather than
setup()), then scalar opts / g: vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:17:30 -03:00
gffranco ba5c0002ab fix(compat): add vimwiki#vars shim for vimwiki-sync and vim-zettel
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 25s
CI / editor keymaps (push) Successful in 1m19s
Third-party plugins such as vimwiki-sync and vim-zettel call
vimwiki#vars#get_wikilocal() — a function only provided by the original
vimwiki plugin. Without it, the after/ftplugin loaded by vimwiki-sync
errors on every wiki buffer open:

  E117: Unknown function: vimwiki#vars#get_wikilocal
  E121: Undefined variable: g:zettel_dir

Add autoload/vimwiki/vars.vim with lightweight stubs for the two keys
vimwiki-sync uses ('path', 'is_temporary_wiki'), reading values from
nuwiki's own g:nuwiki_wiki_root / g:nuwiki_file_extension config so
users don't have to duplicate settings. get_global() is stubbed as a
no-op for completeness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:42:09 -03:00
gffranco 9354e1c176 fix(mappings): add global entry-point keymaps and correct <Leader>ww/<Leader>wt targets
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 31s
CI / cargo test (push) Successful in 53s
CI / editor keymaps (push) Successful in 1m30s
Two bugs prevented keymaps from working after a Dein install:

1. <Leader>ww and <Leader>wt both called diary_today() instead of
   wiki_index() / wiki_tab_index(). Fixed in keymaps.lua (Neovim)
   and ftplugin/vimwiki.vim (Vim).

2. All keymaps were buffer-local (<buffer> / buffer=bufnr), so they
   only activated inside an already-open .wiki file. Users had no way
   to reach the wiki from any other buffer, making the plugin appear
   broken on a fresh Vim start.

   Fix: register the eight <Leader>w* entry-point mappings globally —
   in setup() for Neovim (lua/nuwiki/init.lua) and in plugin/nuwiki.vim
   for plain Vim. To avoid a chicken-and-egg LSP dependency, the global
   mappings open files directly from config (wiki_root, diary_rel_path,
   file_extension); the LSP auto-starts via the FileType autocmd once
   the vimwiki buffer loads. Three new autoload helpers
   (open_wiki_path, open_diary_path, open_diary_index_path) provide the
   LSP-free path for the Vim side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:34:41 -03:00
gffranco a8b89a5303 fix(vim): correct broken symlink, missing commands, and bad VimwikiGoBackLink
CI / cargo fmt --check (push) Successful in 40s
CI / cargo clippy (push) Successful in 49s
CI / cargo test (push) Successful in 30s
CI / editor keymaps (push) Successful in 1m16s
- README: fix plain-Vim install symlink (ln -s target/… → ln -s ../target/…
  so the relative path resolves correctly from inside bin/)
- ftplugin: add missing VimwikiUISelect command on the Vim path
- ftplugin: add missing -count attribute on NuwikiTabIndex (Vim path)
- ftplugin: fix VimwikiGoBackLink (Neovim path) — `normal!` with no
  argument causes E471; use `execute "normal! \<C-o>"` instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:40:20 -03:00
gffranco 8cff0c2d68 ci(release): add libc6-dev-arm64-cross for aarch64-gnu builds
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 22s
CI / cargo test (push) Successful in 35s
CI / editor keymaps (push) Successful in 2m4s
Release / build x86_64-unknown-linux-gnu (push) Successful in 34s
Release / build aarch64-unknown-linux-musl (push) Successful in 55s
Release / build aarch64-unknown-linux-gnu (push) Successful in 1m50s
Release / build x86_64-unknown-linux-musl (push) Successful in 47s
Release / gitea release (push) Successful in 37s
gcc-aarch64-linux-gnu ships the cross compiler and binutils only;
linking glibc binaries also requires the target libc crt/dev files
(Scrt1.o, crti.o), which live in libc6-dev-arm64-cross.

Without it, the aarch64-unknown-linux-gnu build failed with:
  ld: cannot find Scrt1.o: No such file or directory
  ld: cannot find crti.o: No such file or directory

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:27:57 +00:00
gffranco f2fd7d6376 ci(release): pin upload/download-artifact to v3 for Gitea Actions
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 19s
CI / cargo test (push) Successful in 31s
Release / build x86_64-unknown-linux-gnu (push) Successful in 1m34s
Release / build aarch64-unknown-linux-musl (push) Successful in 55s
Release / build aarch64-unknown-linux-gnu (push) Failing after 57s
Release / build x86_64-unknown-linux-musl (push) Successful in 1m16s
Release / gitea release (push) Has been skipped
CI / editor keymaps (push) Successful in 1m24s
actions/upload-artifact@v4 and download-artifact@v4 require the
GitHub-hosted artifact backend, which Gitea Actions does not
implement (GHESNotSupportedError on v2.0.0+). Downgrade to v3.

v3's download-artifact has no merge-multiple flag and nests each
artifact under artifacts/<artifact-name>/, so the upload loop now
recurses to find the archives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:55:31 +00:00
gffranco 82ff454e0f ci(release): drop cross, use rustup targets + native linkers
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Failing after 16m25s
Release / build x86_64-unknown-linux-gnu (push) Failing after 56s
Release / build aarch64-unknown-linux-musl (push) Failing after 56s
Release / build aarch64-unknown-linux-gnu (push) Failing after 1m43s
Release / build x86_64-unknown-linux-musl (push) Failing after 1m31s
Release / gitea release (push) Has been skipped
CI / editor keymaps (push) Has been cancelled
cross spawns Docker containers per target, but the Gitea Actions
runner doesn't expose /var/run/docker.sock, so every cross-compile
step died with "Cannot connect to the Docker daemon".

Replace with rustup-installed targets plus per-target linker setup:
  - x86_64-gnu: native (no extra deps)
  - aarch64-gnu: gcc-aarch64-linux-gnu from apt
  - x86_64-musl: musl-tools from apt
  - aarch64-musl: rust-lld + link-self-contained (rust-std bundles
    musl libc.a + crt, no apt deps needed)

Verified aarch64-musl path locally: produces a statically linked
ARM aarch64 ELF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:29:36 +00:00
gffranco 3ef70e4b7c test(lsp): update find_section_range test for any-level heading match
CI / cargo fmt --check (push) Successful in 56s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m2s
CI / editor keymaps (push) Successful in 2m4s
The prior commit (e849ac9) intentionally removed the h1-only
restriction from find_section_range so VimwikiGenerateTagLinks stays
idempotent under non-h1 tag headings. The accompanying test still
asserted the old h1-only behavior and now fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:34:01 +00:00
gffranco ab6b5f6d73 style(lsp): apply rustfmt to commands.rs
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 24s
CI / cargo test (push) Failing after 31s
CI / editor keymaps (push) Has been skipped
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:27:22 +00:00
gffranco e849ac9d1f Fix duplicate tag links for VimwikiGenerateTagLinks by extending section range detection and allowing any heading level
Release / build aarch64-unknown-linux-gnu (push) Failing after 32s
Release / build aarch64-unknown-linux-musl (push) Failing after 16m23s
Release / build x86_64-unknown-linux-gnu (push) Failing after 55s
Release / build x86_64-unknown-linux-musl (push) Failing after 55s
Release / gitea release (push) Has been skipped
CI / cargo fmt --check (push) Failing after 37s
CI / cargo clippy (push) Successful in 26s
CI / cargo test (push) Failing after 33s
CI / editor keymaps (push) Has been skipped
2026-05-14 16:11:29 -03:00
gffranco 8fa85988d8 style(lists): apply rustfmt to checkbox propagation
CI / cargo fmt --check (push) Successful in 18s
CI / cargo clippy (push) Successful in 35s
CI / cargo test (push) Successful in 43s
CI / editor keymaps (push) Successful in 1m16s
CI's cargo fmt --check flagged a few lines from the propagation patch
that exceeded the wrapping threshold. No behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:24:35 +00:00
gffranco 7a0cec4920 fix(lists): skip own indent in smart_return when auto-indent is active
CI / cargo fmt --check (push) Failing after 22s
CI / cargo clippy (push) Successful in 1m0s
CI / cargo test (push) Successful in 1m30s
CI / editor keymaps (push) Successful in 1m50s
nvim defaults autoindent=on, so the indent inserted by Vim/Nvim after
the <CR> we return was being doubled by our own indent prefix —
pressing Enter on a sub-item produced a deeper-nested item instead of
a same-level one. Detect any auto-indent mechanism (autoindent,
smartindent, cindent, indentexpr) and let it own the indentation;
otherwise add it ourselves so vim's noautoindent default still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:01:21 +00:00
gffranco bcb32777fb chore(dev): always rebuild release binary in start scripts
ensure_binary used to short-circuit whenever bin/nuwiki-ls existed,
which left vim/nvim running an outdated LSP after every source change.
Now always invoke cargo build (incremental, so a no-op when nothing
changed) and refresh the symlink + log the build timestamp. Add
NUWIKI_DEV_SKIP_BUILD=1 for the rare opt-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:00:57 +00:00
gffranco 801896926d fix(lists): propagate checkbox state to ancestor list items on toggle
Toggling/cycling/rejecting a nested checkbox now recomputes each
ancestor's progress marker (vimwiki's listsyms_propagate). Mirrors
vimwiki's averaging — rejected items count as 100% unless every child
is rejected, in which case the parent itself becomes rejected. Walks
up while each ancestor has its own checkbox.

Also tightens find_checkbox_span to scan only the marker line, since a
parent item's span covers its nested sublist and was matching child
markers as if they belonged to the parent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:00:46 +00:00
gffranco 695702555f fix(tables/lua): switch smart_tab/return to <Cmd>, drop vim.schedule
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m19s
CI / editor keymaps (push) Successful in 1m57s
The keymap harness caught a regression in 5b6f789: returning '' from
the <expr> mapping and deferring cursor placement to vim.schedule
breaks any keypress immediately following Tab/<CR>. nvim_feedkeys with
the 'x' flag drains all queued keys synchronously, so the user's next
key lands at the pre-jump cursor position — and the scheduled
callback's startinsert leaks insert mode into the next test.

Mirror what the VimL side already does: have <expr> return
"<Cmd>lua require('nuwiki.commands')._helper(args)<CR>" so the
align + cursor-jump runs synchronously inside the same keypress. No
schedule, no mode juggling.

  cr.adds_new_table_row              ✓
  tab.next_cell_in_table             ✓
  tab.from_first_cell_moves_to_second ✓
  shift_tab.prev_cell_in_table       ✓

39/39 in scripts/test-keymaps.sh (was 35/4-fail).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:39:05 +00:00
gffranco f4e086f981 refactor(tests): group test files by feature, drop phase/cluster naming
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m21s
CI / editor keymaps (push) Failing after 1m31s
Restructures the integration test layout so each file is named after
what it covers, not the implementation phase it landed in.

nuwiki-core (renames only):
  vimwiki_lexer            → lexer
  vimwiki_parser           → parser
  vimwiki_tags             → tags
  vimwiki_table_alignment  → table_alignment
  diary_period             → diary
  list_continuation        → lists
  transclusion_attrs       → transclusion
  + table colspan/rowspan tests moved here from parity_cluster_1

nuwiki-lsp (renames + merges + one split):
  cluster_a_list_rewriters       → commands_lists
  cluster_b_table_rewriters      → commands_tables
  cluster_c_link_helpers +
    phase19_followlink_creates   → commands_links
  phase13_rename_commands        → commands_files
  phase17_colorize               → commands_colorize
  phase17_html_export            → html_export
  phase15_link_health            → link_health
  phase18_multi_wiki             → multi_wiki
  phase19_folding                → folding
  nav                            → navigation
  lsp_helpers                    → helpers
  phase16_diary +
    diary_frequency              → diary
  phase11_plumbing +
    parity_cluster_1 (config)    → index_and_config
  tags_index_and_lsp +
    phase17_backfill             → commands_tags
  phase14_edit_commands          → split into
    commands_checkboxes,
    commands_headings,
    commands_tasks

Net: 30 → 28 integration-test files. 456 → 455 tests (the one
removed test was a dummy `paragraph_render_is_unchanged` whose only
purpose was to keep a `ParagraphNode` import alive in
parity_cluster_1; the import is exercised elsewhere now).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:30:55 +00:00
gffranco 5b6f789c8d fix(tables): auto-realign on edit + stop dragging trailing | onto new row
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m37s
CI / editor keymaps (push) Failing after 1m46s
Two bugs in the insert-mode table editing path:

1. <Tab> past the last cell (and <CR> on a table row) used <CR>+typed
   keystrokes to create a new row, which split the current line at the
   cursor — pulling the trailing | down onto the next line and mangling
   the formatting. Both paths now go through helpers that append() a
   fresh row beside the current one without touching it.

2. The table never realigned to new content. Ported the LSP-side
   render_aligned_table algorithm to Lua + VimL so smart_tab,
   smart_shift_tab, and smart_return tighten column widths locally on
   every navigation. No vim-lsp / nuwiki server roundtrip required.

Neovim side schedules the work via vim.schedule (textlock-safe);
plain Vim hands off via <Cmd>:call …<CR> to keep insert mode and
avoid the cmdline-mode flash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:10:05 +00:00
gffranco 37af0803d8 docs: rewrite README + :help nuwiki for users
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m34s
CI / editor keymaps (push) Successful in 2m4s
Both files now describe what the plugin DOES, with no references to
internal phase work or SPEC.md (which is dev-only).

README.md
  * Reorganised the feature list by area (syntax, editor intelligence,
    editing, diary, other) and surfaced everything actually shipped:
    text objects, smart `<CR>` / `<Tab>` in insert, table alignment
    markers, colspan/rowspan, transclusion attributes, diary
    frequencies, list continuation, etc.
  * Updated the keymaps table to include the full insert-mode bindings,
    list-rewriter shortcuts (`gLh`/`gLl`, `glr`/`gLr`, `gl<Space>`),
    and all five text-object pairs (was "ah/ih + planned follow-ups").
  * Expanded the configuration example with the per-wiki keys (diary,
    HTML export, listsyms, nested_syntaxes) and the new Vim globals
    (`g:nuwiki_no_folding`, `g:nuwiki_mouse_mappings`).
  * Dropped the architecture / repository-layout / phase-log sections.
    Contributors who care about those still find them in the codebase.
  * Removed the "two known behavior deltas" note — the gln/glp claim
    is no longer accurate (both bindings ship as separate keymaps),
    and the lazy-index caveat moved into the Migrating section.

doc/nuwiki.txt
  * Grew from 7 sections to 14 covering installation, configuration
    (top-level + per-wiki + Vim globals), commands, keymaps (by area),
    text objects, insert-mode bindings, diary (with frequency formats),
    tables (with alignment / span syntax), HTML export, folding, health
    checks, and migrating from vimwiki.
  * Every public command has a help anchor.
  * Removed the §6.9 spec reference and the phase 9 caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:46:16 +00:00
gffranco ae96562969 parity(vim): port text objects + folding, fix smart_return textlock
CI / cargo fmt --check (push) Successful in 53s
CI / cargo clippy (push) Successful in 1m27s
CI / cargo test (push) Successful in 1m24s
CI / editor keymaps (push) Successful in 2m27s
Close the remaining Vim-vs-Neovim functional gaps:

  * Text objects — new `autoload/nuwiki/textobjects.vim` mirroring
    `lua/nuwiki/textobjects.lua`. All five pairs (ah/ih, aH/iH,
    al/il, a\/i\, ac/ic) wired in the Vim path of `ftplugin/vimwiki.vim`
    via the classic `:<C-u>call` idiom that works cleanly in both
    operator-pending and visual modes.

  * Folding — new `autoload/nuwiki/folding.vim` providing a regex
    `foldexpr` over headings, plus a tidy `foldtext`. Wired in the
    Vim path; opts out via `let g:nuwiki_no_folding = 1`.

  * `smart_return` was using `setline()` / `append()` inside an
    `<expr>` callback — fine on Neovim but plain Vim's stricter
    textlock raised E565. Rewrote as pure keystrokes (matches the
    Lua version): table rows insert via `<CR>...<Esc>0li`, empty
    list lines break via `<Esc>0DA<CR>`.

  * Function-name parity: added
    `nuwiki#commands#heading_add_level`,
    `nuwiki#commands#heading_remove_level`,
    `nuwiki#commands#table_move_column_{left,right}` as thin
    aliases over the original short names so the public
    `nuwiki#commands#*` surface matches `require('nuwiki.commands').*`.

Test harness:
  * `scripts/test-keymaps-vim.vim` extended from 12 to 30 cases:
    4 smart_return, 3 smart_tab/<S-Tab>, 3 named-command exists,
    4 text-object helpers, 1 `dah` end-to-end, 2 folding, and the
    original 12 pure-VimL cases. (Visual-mode mark inspection in
    `vim -e -s` stays unreliable, so text objects are exercised
    at the helper level plus one operator-pending end-to-end.)

Gates: 456 Rust / 39 Neovim / 30 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:30:15 +00:00
gffranco c8a6fc1172 parity(8b): multi-line list item continuation
Lines indented strictly past a list item's marker now attach to that
item rather than becoming a sibling paragraph. Matches upstream
vimwiki + Markdown convention.

Before:
  - first item
    continuing here          ←  rendered as a separate <p>
  - second

Now:
  - first item continuing here  ← one <li>, joined with a space
  - second

The detection lives in `parse_list_item`: after the item's first
line, we loop over subsequent lines and treat each as continuation
when its leading whitespace exceeds the marker's column. Two
flavours need handling because the lexer emits different tokens
based on indent width:

  * 1..3 leading spaces → `Text(s)` with the whitespace embedded;
    we trim it off the first text token before re-running
    `parse_inline_seq`.
  * 4+ leading spaces  → `BlockquoteIndent` + bare `Text`; we
    advance past the indent token and use the text as-is.

Continuation also works inside nested lists (`level + 1` threshold
adapts to the marker's own indent) and is bounded by blank lines /
unindented content / sibling-or-shallower list markers.

Tests: 6 new in `crates/nuwiki-core/tests/list_continuation.rs`
covering single-line continuation, multi-line continuation, blank-
line termination, unindented termination, nested-list continuation,
and the HTML-renderer regression (single `<ul>` with two `<li>`s,
no orphan `<p>`).

Gates: 456 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:15:58 +00:00
gffranco d8fa59a63a parity(4): diary weekly / monthly / yearly frequency support
vimwiki's `diary_frequency` config has been honoured by the existing
`nuwiki.diary.openToday` / `openYesterday` / `openTomorrow` commands
end-to-end. Setting `diary_frequency = 'weekly'` (or `monthly` /
`yearly`) now creates and addresses entries at that cadence — file
stems follow the canonical formats:

  daily    YYYY-MM-DD     2026-05-12.wiki
  weekly   YYYY-Www       2026-W19.wiki      (ISO 8601 week)
  monthly  YYYY-MM        2026-05.wiki
  yearly   YYYY           2026.wiki

Core additions (nuwiki_core::date):
  - DiaryFrequency enum + permissive `parse` (unknown → Daily)
  - DiaryPeriod enum unifying Day / Week / Month / Year
  - format / parse / next / prev / today_utc + first_day, with
    proper ISO-week math (Thursday rule, year-boundary handling)

LSP wiring:
  - WikiConfig::frequency() and diary_path_for_period()
  - crate::diary::uri_for_period
  - Index now records `diary_period` for any of the four flavours;
    `diary_date` is preserved (filtered to Day-only) for back-compat
    with existing daily-only callers
  - `nuwiki.diary.next` / `nuwiki.diary.prev` now navigate at the
    *same flavour* as the current entry (or at the wiki's configured
    frequency when off a diary page), via new
    `crate::diary::next_period` / `prev_period` helpers
  - `nuwiki.diary.openToday` returns the period stem + frequency in
    its response payload alongside the URI

Tests:
  - 12 in crates/nuwiki-core/tests/diary_period.rs covering the date
    math (ISO week boundaries, format round-trips, next/prev across
    year edges, etc.)
  - 9 in crates/nuwiki-lsp/tests/diary_frequency.rs covering
    WikiConfig path computation, index recognition for all four
    stems, period navigation, and diary-dir filtering

Gates: 450 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:09:14 +00:00
gffranco bcef714805 parity(8a): transclusion attr quote-stripping
`{{url|alt|key="quoted value"}}` syntax was already parsed end-to-end
through to `TransclusionNode::attrs`, but the value retained the
surrounding `"…"` (or `'…'`) characters verbatim. The HTML renderer
then escaped them into `&quot;`, producing `style="&quot;border: 1px&quot;"`
instead of clean `style="border: 1px"`.

Extracted a small `insert_attr` helper that splits on `=`, trims, and
strips matching single- or double-quote pairs from the value before
inserting. Unquoted values pass through unchanged.

Tests: 4 new in crates/nuwiki-core/tests/transclusion_attrs.rs
covering double-quote stripping, single-quote stripping, unquoted
pass-through, and the clean-HTML rendering shape (no `&quot;`).

Note: this commit covers the "transclusion attrs" half of the
original Cluster 8 audit. The "multi-line list-item continuation
paragraphs" half requires lexer + parser changes (continuation lines
indented past the marker should attach to the prior list item
instead of becoming a sibling paragraph) and is deferred.

Gates: 429 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:51:54 +00:00
gffranco 214d54e3b9 parity(7): table cell alignment markers (|:--|--:|:--:|)
The Markdown-style alignment anchors on a table's header-separator
row now propagate from the lexer through the parser into the AST,
and the HTML renderer emits a `style="text-align: …;"` attribute on
each affected `<th>` / `<td>`.

  |:--|   → left
  |--:|   → right
  |:--:|  → center
  |---|   → default (no style attr)

Encoding choice: rather than threading the source string into the
parser, the lexer now extracts per-cell alignment when it recognises
the separator row and carries the `Vec<TableAlign>` inline on the
`TableHeaderRow` token. The parser unpacks it into `TableNode`'s
new `alignments: Vec<TableAlign>` field. Cells past the end of the
vector render with the default alignment.

`TableAlign` lives in `nuwiki_core::ast::block` alongside the table
nodes and is re-exported via `nuwiki_core::ast`.

Tests: 4 new in crates/nuwiki-core/tests/vimwiki_table_alignment.rs
covering plain dashes, all three anchor flavours, the HTML output
shape, and the no-style-attr case. The existing
`vimwiki_lexer::table_header_separator_row` was updated to assert
the new payload shape.

Gates: 425 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:45:43 +00:00
gffranco 2562a046db parity(6): insert-mode <Tab>/<S-Tab> table cell nav
`<Tab>` / `<S-Tab>` in insert mode navigate between table cells when
the cursor is on a `|…|` row, otherwise they pass through to their
default insert-mode behaviour (literal tab / shift-tab). Same
`<expr>`-mapping idiom as Cluster 5's smart_return.

  <Tab>    next cell; creates a fresh row below if past the last cell
  <S-Tab>  previous cell; no-op past the first

Cursor lands immediately after the destination cell's leading `|`,
matching upstream vimwiki's `vimwiki#tbl#go_*_cell` behaviour.

Both Lua and VimL counterparts wired into ftplugin's existing
table_editing block (gated alongside `gqq`/`<A-Left>`).

Tests: 4 new in scripts/test-keymaps.lua — next cell, new-row
overflow, previous cell, and pass-through outside a table.

Gates: 421 Rust / 39 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:37:53 +00:00
gffranco e347f605ff parity(5): smart <CR> in insert mode (VimwikiReturn)
Bind `<CR>` in insert mode as an `<expr>` map to a `smart_return`
helper. Behaviour matches upstream vimwiki's `:VimwikiReturn`:

  - On a list line with content → continue the list with the same
    marker on a new line (preserving leading checkbox `[ ]` if any).
  - On an empty list line (marker only) → clear the marker and break
    out of the list with a plain newline.
  - Inside a `|…|` table row → insert a fresh empty row below with
    the same column count, cursor lands inside the first cell.
  - Otherwise → plain `<CR>`.

Implementation note: the helper is `<expr>`-bound, which means
textlock is active and the buffer can't be mutated from inside the
callback. The function returns key sequences only — including
`<Esc>0DA<CR>` for the empty-list break and `<Esc>0li` for the
table-row cursor jump — so every effect flows through Vim's normal
keystroke pipeline and stays undo-coherent.

Both Lua and VimL counterparts shipped.

Tests: 4 new in scripts/test-keymaps.lua covering list continuation,
checkbox preservation, empty-marker break-out, and new table row.

Gates: 421 Rust / 35 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:34:36 +00:00
gffranco fd4d902fde parity(3): text objects — aH/iH, al/il, a\/i\, ac/ic
Five operator-pending + visual text-object pairs, matching upstream
vimwiki:

  ah / ih   heading section only (stops at any next heading)
  aH / iH   heading section + sub-tree (stops at same-or-shallower)
  al / il   list item (line, with / without marker prefix + checkbox)
  a\ / i\   table cell (with / without surrounding `|` separators)
  ac / ic   table column (with / without the header separator row)

Pre-existing `ah` used aH semantics — fixed to match vimwiki: `ah`
now stops at the next heading regardless of level. `aH` is the new
descendants-inclusive variant.

Implementation notes:
- Pure regex; no LSP round-trip. Each helper computes a `(line, col)`
  rectangle and drives the selection with feedkeys.
- Operator-pending and visual need different feed sequences. In visual
  the previous anchor is sticky, so bounce through `<Esc>` first. In
  operator-pending an `<Esc>` would CANCEL the pending operator —
  feed the visual keys directly so Vim treats them as the motion.
- Re-exported helpers (`_heading_block`, `_cell_ranges`,
  `_current_cell_for_line`, `_table_bounds`) keep the integration
  surface testable without going through Vim's visual-mode pipeline,
  whose `'<`/`'>` mark semantics fight headless test harnesses.

Tests: 6 new in scripts/test-keymaps.lua — five pure-helper cases
plus one end-to-end `dah` deletion to verify the operator-pending
wiring.

Gates: 421 Rust / 31 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:23:44 +00:00
gffranco 46ae618b5f parity(2): insert-mode list bindings (<C-D>/<C-T>/<C-L><C-?>)
Match upstream vimwiki's insert-mode list editing:
  <C-D>      list_change_level(-1)   dedent current item
  <C-T>      list_change_level(+1)   indent current item
  <C-L><C-J> list_cycle_symbol(+1)   cycle marker forward
  <C-L><C-K> list_cycle_symbol(-1)   cycle marker backward
  <C-L><C-M> list_toggle_or_add_chk  toggle if has checkbox, else add `[ ]`

Cycle order matches vimwiki's canonical run:
  - → * → # → 1. → 1) → a) → A) → i) → I) → (wrap)

Both Lua and VimL paths wired. Lua callbacks fire directly (no `<C-o>`
dance) since list_change_level / changeSymbol mutate the buffer via
the LSP applyEdit pipeline, which works cleanly in insert mode. VimL
keeps the `<C-o>:call …<CR>` idiom since that's the standard there.

Harness adds 5 cases covering the new insert-mode bindings plus one
direct LSP roundtrip for changeSymbol — surfaced a long-standing
stale-binary footgun in test-keymaps.sh (rebuilt only when bin was
missing; now rebuilds every run since incremental cargo is fast).
Harness also captures server log_messages and dumps them on failure
so future swallowed-error bugs (Err → log + Ok(None)) are visible.

Gates: 421 Rust / 25 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:11:53 +00:00
gffranco ee0309b3c2 parity(1): vimwiki per-wiki config keys, table colspan/rowspan,
named link commands, mouse maps

Parity audit Cluster 1 — small wins.

WikiConfig (server) extended with 14 vimwiki globals:
- `index` (`g:vimwiki_index`), wired into `:VimwikiIndex` so users
  with a custom index page stem (e.g. `home`) get the right file.
- `diary_frequency`, `diary_start_week_day`, `diary_caption_level`,
  `diary_sort`, `diary_header` — diary keys (weekly/monthly/yearly
  semantics land in Cluster 4; the keys parse now so config
  migration doesn't need to wait).
- `maxhi`, `listsyms`, `listsyms_propagate`, `list_margin`,
  `links_space_char`, `nested_syntaxes`, `auto_toc` — list +
  highlight knobs the renderer/handlers can consult.

All keys threaded through `RawWiki` → `WikiConfig::from(RawWiki)`,
defaults centralised in `wiki_defaults()` so `empty()`, `from_root()`,
the legacy single-wiki path, and the `RawWiki` impl stay in sync.

Lua front-door (`lua/nuwiki/config.lua`) now documents the multi-wiki
`wikis = {…}` shape with every accepted per-wiki key — users who
prefer Lua tables no longer have to round-trip through
`init_options` raw JSON.

HTML renderer (`crates/nuwiki-core/src/render/html.rs`):
- New `table_spans()` pre-pass resolves vimwiki's `>` (col_span) and
  `\\/` (row_span) continuation markers into proper HTML
  `colspan="N"` / `rowspan="N"` attributes on the lead cell. Continuation
  cells emit nothing, matching what browsers expect.
- The old behaviour (emitting `class="col-span"` / `class="row-span"`
  as visual markers) is gone — replaced with real merging so the
  rendered HTML matches the source semantics.

Named ex-commands:
- `:VimwikiNextLink` / `:VimwikiPrevLink` — were keymapped only
  (`<Tab>`/`<S-Tab>`); now have explicit `:` commands in both editor
  paths, dispatching to new `nuwiki.commands.link_next` / `link_prev`
  pure-Vim/Lua helpers.
- `:VimwikiBaddLink` — adds the link target's file to the buffer
  list without switching focus. Goes through
  `textDocument/definition` and runs `:badd <fname>` on the resolved
  URI.

Mouse maps (opt-in via `mappings.mouse = true` in Lua or
`g:nuwiki_mouse_mappings = 1` in Vim):
- `<2-LeftMouse>` follows, `<S-2-LeftMouse>` / `<C-2-LeftMouse>`
  follow in split/vsplit, `<MiddleMouse>` adds to buflist,
  `<RightMouse>` goes back. Mirrors upstream vimwiki's defaults.

Tests: 7 new in `parity_cluster_1.rs` covering the per-wiki config
defaults + raw JSON parse, the legacy-single-wiki path, colspan
folding into a lead cell with no leftover class markers, rowspan
across rows, no-attrs when the table has no spans, and a sanity
paragraph render. Total 421 Rust tests pass; clippy clean; both
keymap harnesses still green (20 nvim + 12 vim).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:10:27 +00:00
gffranco de8b8fa30d feat(13.1): colorize + color_dic → ColorNode renderer
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 1m24s
CI / cargo test (push) Successful in 1m24s
CI / editor keymaps (push) Successful in 1m45s
Closes the last pending §13.1 entries:

- **`nuwiki.colorize`** (client-side, Lua + VimL) — wraps the word
  at cursor (or the visual selection on Neovim) in an inline
  `<span style="color:%c">…</span>` template. Matches vimwiki's
  default `color_tag_template`. Bound to `<Leader>wc` in both
  normal and visual mode on both editor paths; the previous
  "deferred" stub is gone.

- **`color_dic` → renderer** — `HtmlConfig` gains a
  `color_dic: HashMap<String, String>` field reading the same key
  out of `initializationOptions` / `didChangeConfiguration`. The
  HTML export path threads it into `HtmlRenderer::with_colors`;
  `ColorNode` now picks `style="color:<value>"` when the colour
  name is in the dict, falling back to the existing
  `class="color-<name>"` when it isn't.

SPEC §13.1 is fully checked off — the table moved from "deferred
sketch" to a per-command status table marking all 11 commands done,
plus a note on the `color_dic` follow-up landing alongside. README's
"Phase 14 list & table edit commands" row now reads  without the
deferred-subset caveat, and the §13.1-blocked text-objects note in
the keymaps section is rephrased as "planned follow-ups".

Tests: 4 new in `phase17_colorize.rs` covering the renderer
fallback when the dict is empty, the inline-style emission for a
listed name, the fallback path for an unlisted name in a populated
dict, and the `initializationOptions` JSON shape. Total 414 Rust
tests pass; clippy clean; keymap harness still 20/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:45:39 +00:00
gffranco 4245424d2f feat(13.1-B): table rewriters — insert, align, moveColumn
Closes the table-rewriter cluster from SPEC §13.1.

Server-side (ops module):
- `render_blank_table(cols, rows)` — pure templating: header row,
  separator `|--|--|--|`, then `rows` empty data rows. Wired through
  `table_insert` which dispatches a `TextEdit` at the cursor.
- `table_align_edit` — locates the `TableNode` containing the cursor
  line, computes max width per column across every row, re-emits the
  table with each cell space-padded to its column's width. The
  parser drops `|---|---|` separator rows but sets
  `TableNode.has_header`; the renderer re-inserts a padded separator
  after the header so the result still parses.
- `table_move_column_edit` — column-under-cursor swap with the
  left or right neighbour (`dir = "left" | "right"`). Cursor column
  is computed by counting pipe separators before the cursor's byte
  offset on the row's line. Column widths swap alongside the data so
  the post-swap table stays aligned. No-ops cleanly when the swap
  would fall off either edge.

Editor glue:
- `lua/nuwiki/commands.lua` / `autoload/nuwiki/commands.vim` — the
  three "not yet implemented" stubs are replaced with real LSP
  dispatchers. `table_insert` accepts optional `cols`/`rows`
  arguments (defaults: 3×2).
- Default keymaps `gqq` / `gq1` / `gww` / `gw1` now call
  `table_align`; `<A-Left>` / `<A-Right>` call
  `table_move_column_left` / `_right`. No more "deferred"
  notifications on the table keys.

Tests: 8 new in `cluster_b_table_rewriters.rs` — blank-table shape
(3×2 + 1×1), column-width alignment with separator-row repad, swap-
right shape, swap-left clamp at column 0, no-table-found fallbacks,
COMMANDS list completeness. Total 410 Rust tests pass; clippy clean;
Neovim keymap harness still 20/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:41:51 +00:00
gffranco 9d34c8baa2 feat(13.1-A): list rewriters — removeDone, renumber, changeSymbol/Level
Closes the four-command list-rewriter cluster from SPEC §13.1.

Server-side (commands.rs ops module):
- `parse_symbol` / `render_marker` — round-trip between
  `:VimwikiListChangeSymbol` arg shapes (`-`, `1.`, `a)`, `i)`,
  `Dash`, `Numeric`, …) and the rendered marker text. `render_marker`
  knows how to spell `a/b/…/z/aa` and `i/ii/iv/v/…/MMM` for the
  alphabetic + roman variants.
- `remove_done_edit` — walks every list (recursively through
  blockquotes and sublists), emits delete TextEdits for items whose
  checkbox is `Done` or `Rejected`. Item span gets extended through
  the trailing newline so we don't leave behind empty lines. Accepts
  an optional `position` to scope the operation to the item under
  cursor + its descendants.
- `renumber_edit` — finds the list containing the cursor line (or
  every list when `whole_file: true`), re-sequences numeric
  markers in-place. Unordered lists are left alone.
- `change_symbol_edit` — rewrites the leading marker on a single
  item, or every item in a list when `whole_list: true`. Uses
  `render_marker` so ordered variants get the right index.
- `change_level_edit` — re-indents the marker line (or every line of
  the item subtree when `whole_subtree: true`) by ±2 spaces per
  level. Dedent clamps at column 0.
- `find_list_at_line` + `find_marker_span` — pure helpers reused by
  the three above.

Editor glue:
- `lua/nuwiki/commands.lua` / `autoload/nuwiki/commands.vim` —
  removed all four "not yet implemented" stubs and wired them to the
  real LSP commands. Lua also exposes `list_change_lvl(direction)`
  for the `:VimwikiListChangeLvl decrease|increase` compat entry.
- Keymaps for `glh` / `gll` / `gLh` / `gLl` (list level single +
  subtree), `glr` / `gLr` (renumber list + whole-file),
  `gl<Space>` / `gL<Space>` (remove done items) now hit the real
  commands instead of printing a "deferred" notification.

Tests: 16 new in `cluster_a_list_rewriters.rs` covering
symbol parsing, marker rendering (alpha + roman + numeric),
removeDone shape, scoped removeDone, no-match cases, renumber
sequencing, whole-file walk, changeSymbol single + whole-list,
changeLevel single + subtree + clamp, and COMMANDS list completeness.
Total 402 Rust tests pass; clippy clean; keymap harness still at
20/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:37:11 +00:00
gffranco cebc806ce3 feat(13.1-C): link helpers — pasteWikilink, pasteUrl, normalize
Closing out §13.1's smallest cluster. Three commands move from
"not yet implemented" stubs to real behaviour:

- `nuwiki.link.pasteWikilink` (server, executeCommand) — derives
  the current page name from the source URI + wiki root, returns a
  `WorkspaceEdit` inserting `[[<page>]]` at the requested cursor
  position.
- `nuwiki.link.pasteUrl` (server) — same lookup, but the inserted
  text is the page's relative HTML output URL
  (`<page>.html`, including subdir segments) so the snippet survives
  when the export root moves.
- `nuwiki.link.normalize` (client) — wraps the word at cursor as
  `[[word]]` without following. Reuses the `wrap_cword_as_wikilink`
  helper that already powers the `<CR>` two-step. Pure-VimL on the
  Vim path; pure-Lua on the Neovim path. No LSP round-trip.

Keymaps:
- `+` (normal + visual) now actually calls `normalize_link` on both
  editor paths instead of stubbing with a "deferred" notification.

Tests:
- 5 new Rust unit tests in `cluster_c_link_helpers.rs` covering
  command-list presence + the page-name derivation for root and
  subdirectory pages + the URL / wikilink shape strings.
- Neovim keymap harness gains a `links.normalize_via_+` case (20
  passing now, up from 19). Vim harness inherits the same
  command-presence check via its existing smoke tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:32:35 +00:00
gffranco 7c9e679169 docs: rewrite README into a proper user-facing intro
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 1m20s
CI / cargo test (push) Successful in 1m22s
CI / editor keymaps (push) Successful in 2m8s
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>
2026-05-12 14:20:18 +00:00
gffranco a229977b77 ci(keymaps): pin Neovim 0.11 from upstream release tarball
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m18s
CI / cargo test (push) Successful in 1m13s
CI / editor keymaps (push) Successful in 1m44s
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>
2026-05-12 13:29:37 +00:00
gffranco a489bea480 test(keymaps): hard timeouts + serialise the CI job behind test
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m18s
CI / editor keymaps (push) Failing after 2m25s
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>
2026-05-12 13:17:10 +00:00
gffranco 5fdd7a842e test(keymaps): Vim path harness + fix 25 broken one-liner functions
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 1m10s
CI / cargo test (push) Successful in 1m20s
CI / editor keymaps (push) Has been cancelled
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>
2026-05-12 12:41:46 +00:00
gffranco 3b6693129c test(keymaps): headless Neovim harness for buffer-local bindings + CI
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 1m20s
CI / cargo test (push) Successful in 1m14s
CI / editor keymaps (push) Has been cancelled
`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>
2026-05-12 12:17:11 +00:00
gffranco 4905858d5e fix(follow-link): two-step <CR> — first wraps, second follows
CI / cargo fmt --check (push) Successful in 18s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m15s
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>
2026-05-12 01:55:20 +00:00
gffranco 347e2f02f7 fix(follow-link): create missing page + wrap word on <CR>
CI / cargo fmt --check (push) Successful in 35s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m16s
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>
2026-05-12 01:44:26 +00:00
gffranco 9d86218b13 fix(keymap): C-Space toggle fires across all terminal byte spellings
CI / cargo fmt --check (push) Successful in 25s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m12s
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>
2026-05-12 01:28:23 +00:00
gffranco 047c9a3cf3 fix(vim): follow-link creates missing pages + safe LSP foldexpr
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 1m7s
CI / cargo test (push) Successful in 1m13s
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>
2026-05-12 01:22:35 +00:00
gffranco c3a8eae9d0 fix(vim): vim-lsp detection + drop hang-prone echo lines
CI / cargo fmt --check (push) Successful in 32s
CI / cargo clippy (push) Successful in 1m22s
CI / cargo test (push) Successful in 1m18s
`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>
2026-05-12 01:15:53 +00:00
gffranco f1bf70d448 fix(vim): gate Neovim TS-style highlight groups + correct bin path lookup
CI / cargo fmt --check (push) Successful in 28s
CI / cargo clippy (push) Successful in 1m20s
CI / cargo test (push) Successful in 1m16s
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>
2026-05-12 01:04:23 +00:00
gffranco d58a34a2d7 feat(vim): close keymap gap with Neovim (47 → 63 maps)
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m14s
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>
2026-05-12 00:50:49 +00:00
gffranco 9e6faa5554 feat(mappings): vimwiki-parity default keymaps for Vim + Neovim
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m22s
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>
2026-05-12 00:46:40 +00:00
gffranco 04df170791 feat(vim): :Vimwiki* / :Nuwiki* commands for plain Vim via vim-lsp
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m27s
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>
2026-05-11 22:23:06 +00:00