Commit Graph

49 Commits

Author SHA1 Message Date
gffranco 724712121d feat(parity): close the entire P3 Mappings section
Both remaining mapping gaps (client-side, both clients):

- Visual <CR>: upstream binds it to NormalizeLinkVisualCR (== visual `+`).
  Added `x <CR>` -> normalize_link(true) (wrap the selection as a wikilink)
  in both clients.

- Insert <S-CR>: upstream's multiline-list-item continuation (VimwikiReturn
  2 2 -> kbd_cr with no new marker). New smart_shift_return (both clients,
  <expr> insert mapping): on a list item returns <CR> + spaces aligning under
  the item text (marker width + 1, +4 for a `[ ] ` checkbox), no marker; off a
  list item, a plain <CR>. Mirrors smart_return's auto-indent handling.

Tests: map.visual_cr_wraps_selection + map.insert_shift_cr_multiline
(test-keymaps.lua, the latter exercising the handler directly since <S-CR>
doesn't round-trip headless feedkeys), and map.visual_cr_wraps_selection +
cr.shift_cr_multiline{,_checkbox} (test-keymaps-vim.vim). Docs (README +
doc/nuwiki.txt) and the gap doc updated. Neovim 305, Vim 299/18/21 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:04:56 +00:00
gffranco a11b742fc1 feat(parity): close the entire P3 Commands section
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 24s
CI / cargo test (push) Successful in 42s
CI / editor keymaps (push) Successful in 1m40s
All six remaining command gaps (client-side; no server change):

- VimwikiRenameFile: bare -> -nargs=? (arg accepted-ignored; rename still
  delegates to the LSP rename prompt). No more E488.

- VimwikiVar / NuwikiVar: get/set the client config (upstream vimwiki#vars#cmd).
  No arg prints config (Vim: g:nuwiki_*; Neovim: resolved setup() opts), one arg
  gets a key, two+ sets it. Global command, both clients.

- VimwikiReturn / NuwikiReturn: command form of the smart <CR> continuation —
  enters insert at EOL and fires the <CR> mapping (return_cmd / vimwiki_return).
  Upstream's mode-flag args accepted but unused.

- VimwikiTableAlignQ vs AlignW: corrected — upstream's gqq/gww are identical on
  a table; the only difference is the non-table fallback (native `normal! gqq`
  vs `gww`). New table_align_or_cmd(cmd) aligns on a table row, else runs
  `normal! <cmd>`. Q/gqq/gq1 -> gqq; W/gww/gw1 -> gww; NuwikiTableAlign -> gqq.

- VimwikiSearch / VWS: scope the lvimgrep to the current wiki's root (resolved
  client-side) over <root>/**/*<ext>, not CWD `**`; empty pattern reuses the
  last search. Both clients.

- VimwikiIndex family: add global Vimwiki/NuwikiIndex + TabIndex entry points
  (plugin/nuwiki.vim + init.lua _setup_global_commands, with -count), so a wiki
  opens from any buffer. Buffer-local copies still shadow inside wiki buffers.

Tests: surface.{Vimwiki,Nuwiki}{Return,Var}, cmd.VimwikiVar_sets_*,
cmd.global_entry_points (both harnesses). Docs (README + doc/nuwiki.txt) and the
gap doc updated. Neovim 299, Vim 291/18/21 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:23:07 +00:00
gffranco 3865b8bf0e fix(parity): fifth-pass re-audit — VimwikiGoto nargs, autowriteall, table_auto_fmt
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 37s
CI / cargo test (push) Successful in 40s
CI / editor keymaps (push) Successful in 1m35s
Closes the actionable fifth-pass findings (gap doc updated):

- VimwikiGoto / NuwikiGoto: -nargs=1 -> -nargs=* in all four defs. Was a
  real bug — `:VimwikiGoto` raised E471, `:VimwikiGoto My Page` raised E488,
  and the handler's empty-arg prompt fallback was unreachable. Both clients
  already prompt on empty, so -nargs=* (keeping -complete=pages) restores
  upstream behavior.

- autowriteall (upstream default ON): while a wiki buffer is current, mirror
  Vim's global 'autowriteall' (auto-save on page switch / :make), restoring
  the prior value on leave. Neovim via ftplugin.lua setup_autowriteall
  (BufEnter/BufLeave) gated by the `autowriteall` setup option; Vim via the
  NuwikiAutoWriteAll augroup gated by g:nuwiki_autowriteall.

- table_auto_fmt (upstream default ON): re-align the table under the cursor
  on InsertLeave. Both clients, gated by `table_auto_fmt` / g:nuwiki_table_auto_fmt,
  guarded on the cursor line containing `|` before the async table_align.

Also logged (no code): VimwikiRenameFile missing -nargs=? (subsumed by the
LSP-rename divergence), table_reduce_last_col, user_htmls, hl_headers/
hl_cb_checked, and a commentstring value-mismatch note. Mappings audit clean.

The Neovim harness disables table_auto_fmt in setup (its async re-align would
perturb the deterministic table-nav keystroke cases); its wiring is verified
on a throwaway scratch buffer instead. Docs (README + doc/nuwiki.txt) updated.
Tests: cmd.VimwikiGoto_accepts_multiword, config.autowriteall_applied,
config.table_auto_fmt_autocmd. Neovim 293, Vim 285/18/21 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:59:33 +00:00
gffranco c161f9d21a feat(parity): P3 quick-wins — ShowVersion, gLH/gLL/gLR, Search/Align nargs
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 26s
CI / cargo test (push) Successful in 36s
CI / editor keymaps (push) Successful in 1m47s
Closes four low-risk P3 parity items (gap doc updated):

- VimwikiShowVersion / NuwikiShowVersion: new global command (both clients)
  echoing g:nuwiki_version (0.1.0) + the host editor, via
  nuwiki#commands#show_version / commands.show_version.

- gLH / gLL / gLR: upstream's case-variant aliases of gLh/gLl/gLr (dedent /
  indent whole item, renumber all lists) added in both clients.

- VimwikiSearch / VWS: -nargs=1 -> -nargs=* (Vim + Neovim), so a multi-word
  search no longer raises E488 and an empty invocation reuses the last search
  pattern. (The lvimgrep-vs-vimwiki-engine difference stays open.)

- VimwikiTableAlignQ / AlignW / NuwikiTableAlign: bare -> -nargs=?, so
  `:VimwikiTableAlignQ 2` no longer raises E488 (optional column arg is
  accepted-and-ignored; the gqq-align vs gww-align-without-resize split
  stays open).

Docs (doc/nuwiki.txt + README) updated for the command and the aliases.
Tests: surface.{Vimwiki,Nuwiki}ShowVersion, map[n].gL{H,L,R} (both
harnesses), cmd.search_and_tablealign_nargs (vim). Neovim 291, Vim
282/18/21 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:28:51 +00:00
gffranco 8dc4050b76 fix(commands)+docs: even out :NuwikiUISelect; exhaustive subgroup key lists
CI / cargo fmt --check (push) Failing after 16s
CI / cargo clippy (push) Successful in 31s
CI / cargo test (push) Successful in 37s
CI / editor keymaps (push) Successful in 1m30s
- ftplugin/vimwiki.vim: add buffer-local :NuwikiUISelect to the Vim path
  (it previously existed only on the Neovim path, contradicting the docs'
  "buffer-local on every .wiki buffer" claim — a global fallback masked it).
  Now symmetric with :VimwikiUISelect.

- Make every keymap subgroup description list all of its bindings instead
  of trailing with "…": the README mappings-subgroups table, the README
  config-example comments, the README g:nuwiki_no_<group>_mappings rows,
  the doc/nuwiki.txt group→keys list, and the lua/nuwiki/config.lua comment.
  Also fixes the doc list that had dropped aH/iH from text_objects and adds
  the mouse group. Each notes that the <Leader>w prefix follows map_prefix.

helptags validates (no dup tags); all keymap suites pass (266+18+21 Vim,
275 Lua); :NuwikiUISelect confirmed defined on a Vim .wiki buffer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:40:06 +00:00
gffranco 3e82f59f12 docs: fix stale README + help file (audit)
CI / cargo fmt --check (push) Failing after 21s
CI / cargo clippy (push) Successful in 52s
CI / cargo test (push) Successful in 38s
CI / editor keymaps (push) Successful in 1m27s
A three-agent audit cross-checked README.md and doc/nuwiki.txt against the
actual config, commands, and mappings in code. Fixes:

Mappings:
- doc/nuwiki.txt wrongly described <Leader>ww as "open today's diary" — it
  opens the wiki index (diary-today is <Leader>w<Leader>w). Corrected, and
  the missing <Leader>wt (wiki index in a tab) row added.
- README feature bullet referenced <Leader>ww for the diary; fixed to
  <Leader>w<Leader>w.
- Both keymap listings now note <Leader>w is the default map_prefix.

Config globals:
- README globals table was missing real, code-read globals: g:nuwiki_syntax,
  g:nuwiki_diary_rel_path, g:nuwiki_wikis, g:nuwiki_map_prefix,
  g:nuwiki_binary_path, g:nuwiki_build_from_source. Added.
- doc/nuwiki.txt globals section gained g:nuwiki_diary_rel_path and
  g:nuwiki_link_severity (both read in autoload but undocumented).
- doc/nuwiki.txt per-wiki list gained template_date_format,
  html_filename_parameterization, color_dic, list_margin (in README only).

Commands:
- doc/nuwiki.txt was missing a whole list/task/table command group plus
  :NuwikiFindOrphans, :NuwikiNormalizeLink, :NuwikiColorize,
  :NuwikiPasteLink/PasteUrl/CatUrl. Added, matching README's descriptions.

No default-value mismatches and no phantom (documented-but-nonexistent)
entries were found. helptags validates with no duplicate tags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:33:20 +00:00
gffranco feedd30c4d feat(keymaps): configurable map_prefix for the wiki command family (P2)
CI / cargo fmt --check (push) Failing after 33s
CI / cargo clippy (push) Successful in 31s
CI / cargo test (push) Successful in 41s
CI / editor keymaps (push) Successful in 1m49s
Mirror vimwiki's g:vimwiki_map_prefix. The whole <Leader>w* family was
hardcoded across the map layer; it is now built from a configurable prefix:

- Neovim: `map_prefix` setup() option (default '<Leader>w'), read by
  lua/nuwiki/keymaps.lua (buffer-local) and lua/nuwiki/init.lua (global
  entry points).
- Vim: g:nuwiki_map_prefix, applied via :exe in plugin/nuwiki.vim (global)
  and ftplugin/vimwiki.vim (buffer-local).

Setting a custom prefix relocates <prefix>{w,t,s,i,n,d,r,c,h,hh,ha} and
<prefix><Leader>{w,y,t,m,i}, leaving nothing under the old <Leader>w*.
Non-prefix maps (<CR>, gl*, headers, …) are untouched.

Tests: new test-keymaps-vim-prefix.vim harness (21 cases, wired into
test-keymaps-vim.sh) + map_prefix.relocates_wiki_family in test-keymaps.lua.
Docs: README, doc/nuwiki.txt, lua config comment, vimwiki-gap.md ticked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:18:06 +00:00
gffranco 23aec3e6c7 feat(config): custom_wiki2html external converter + base_url for RSS (P2)
CI / cargo fmt --check (push) Failing after 16s
CI / cargo clippy (push) Successful in 32s
CI / cargo test (push) Successful in 48s
CI / editor keymaps (push) Successful in 1m39s
Add three per-wiki HtmlConfig keys mirroring vimwiki globals:

- custom_wiki2html / custom_wiki2html_args: when set, export shells out
  to the external converter via `sh -c` with vimwiki's exact arg order
  (<cmd> <force> <syntax> <ext> <out_dir> <in_file> <css> <tpl_path>
  <tpl_default> <tpl_ext> <root_path> <args>, `-` for empty optionals)
  instead of the built-in renderer.
- base_url: when set, diary RSS item + channel links become
  <base_url><diary_rel_path>/<date>.html instead of file:// URIs.

write_page() gains a `force` flag threaded through export_current (false),
export_all (its own flag) and the did_save auto-export path (false).

Tests: write_page_invokes_custom_wiki2html_converter,
write_rss_uses_base_url_for_public_links (html_export.rs), config
round-trip + defaults (index_and_config.rs). Docs: README, doc/nuwiki.txt,
lua config comment, vimwiki-gap.md item ticked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:06:42 +00:00
gffranco 09b5a2bd46 feat(config): on-save autoregen family — links/tags/diary-index (P2)
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 38s
CI / cargo test (push) Successful in 41s
CI / editor keymaps (push) Successful in 1m25s
Added per-wiki auto_generate_links, auto_generate_tags, auto_diary_index
(default false, like upstream), wired into the did_save hook mirroring
auto_toc:

- auto_generate_links / auto_generate_tags rebuild the Generated Links /
  Generated Tags section ONLY when it already exists (new
  links_rebuild_edit / tag_links_rebuild_edit, like toc_rebuild_edit) — never
  inserts one into a page that lacks it.
- auto_diary_index regenerates the diary index page when a dated diary entry
  (not the index itself) is saved, via diary_generate_index_edit (handles the
  index file open / on-disk / absent).

auto_tags is recorded as an intentional divergence: nuwiki re-indexes tags on
every change, so the tag metadata is always fresh (no on-disk file to update).

Tests: links_rebuild_edit_only_acts_when_section_present,
tag_links_rebuild_edit_only_acts_when_index_present, config round-trip +
defaults. README/doc/lua comment updated. fmt/clippy clean; 0 Rust failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:55:51 +00:00
gffranco 545367c3f6 feat(config): configurable generated-section captions (P2)
CI / cargo fmt --check (push) Successful in 32s
CI / cargo clippy (push) Successful in 24s
CI / cargo test (push) Successful in 31s
CI / editor keymaps (push) Successful in 1m40s
TOC / links / tags section headings (and their levels) were hardcoded.
Added per-wiki toc_header/toc_header_level, links_header/links_header_level,
tags_header/tags_header_level — defaults Contents / Generated Links /
Generated Tags at level 1, matching upstream (also fixes the tags index
heading: Tags -> Generated Tags).

A caption_line(name, level) helper emits the `=`-markers; the configured
text + level thread through toc_edit / links_edit / tag_links_edit and the
auto_toc save hook. find_section_range matches the configured text, so
regeneration stays idempotent at any level.

Tests: captions_honour_custom_header_and_level (link_health) + config
round-trip + default assertions (index_and_config); existing tag/toc/links
tests updated for the new arity and the Generated Tags default. fmt/clippy
clean (8-arg ops get allow(too_many_arguments), matching the codebase
precedent); 0 Rust test failures; config-parity green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:44:35 +00:00
gffranco 2d3db458fb feat(config): make listsym_rejected configurable (P2)
The rejected/cancelled checkbox glyph was a hardcoded `const REJECTED = '-'`.
ListSyms now carries a `rejected` field (new_with_rejected); a per-wiki
`listsym_rejected` key (default `-`) is added to WikiConfig, and
WikiConfig::list_syms() builds the palette with it. Routed the three
config-driven ListSyms construction sites (parse path in lib.rs + the
toggle/cycle commands) through list_syms(), so a custom rejected glyph both
lexes and round-trips.

Tests: listsyms::custom_rejected_glyph_is_honoured + config round-trip.
README/doc/lua config comment updated. fmt clean; 0 Rust test failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:33:25 +00:00
gffranco db9c1c5c11 feat: real range/bang/visual handling for the deferred command gaps
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 22s
CI / cargo test (push) Successful in 53s
CI / editor keymaps (push) Successful in 1m29s
Implements the four items previously left as "needs handler work" — all
fully wired, not cosmetic.

- ToggleListItem / IncrementListItem / DecrementListItem are now -range
  (both clients). The client loops the per-line op over [<line1>,<line2>]
  (toggle_list_item_range / list_cycle_symbol_range). The server's edits are
  version-less single-line WorkspaceEdits, so each line applies independently
  — no server protocol change. `:'<,'>VimwikiToggleListItem` toggles the whole
  selection.
- VimwikiRebuildTags gains -bang: `:…RebuildTags!` passes { all: true } and
  the server (tags_rebuild) re-indexes every configured wiki via
  wikis_snapshot(), not just the current one.
- VimwikiNormalizeLink is -nargs=?: with `1` (upstream's visual flag; the
  x-mode `+` mapping now passes it) it wraps the '< / '> selection as
  [[selection]] (new wrap_visual_as_wikilink in both clients).
- VimwikiCheckLinks is -range: a ranged invocation filters the broken-link
  report to the current buffer's selected lines (client-side filter in
  results_to_qf / check_links).

Tests: cmd.toggle_list_item_range + cmd.normalize_link_visual_wraps_selection
(test-keymaps.lua), normalize.visual_wraps_selection (test-keymaps-vim.vim).
README + doc/nuwiki.txt + gap doc updated. fmt/clippy clean; nvim 274,
vim 266+18, all Rust tests + config-parity green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:57:03 +00:00
gffranco 212b300fb4 fix(client): small mapping/command-attribute parity gaps
CI / cargo fmt --check (push) Successful in 37s
CI / cargo clippy (push) Successful in 24s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m44s
Clean, fully-wired parity fixes (skipping the accept-but-ignore ones):

- <D-CR> (macOS Cmd+Return) → follow_link_drop, alongside <C-S-CR>, in both
  clients. Upstream binds both to VimwikiTabDropLink.
- plugin/nuwiki.vim global map block: fix the same diary collision the
  buffer-local maps had — <Leader>w<Leader>t now opens today in a new tab and
  <Leader>w<Leader>m (tomorrow) is added.
- Neovim TabIndex: add -count to NuwikiTabIndex and switch both
  Vimwiki/NuwikiTabIndex from vim.v.count (always 0 in command context) to
  <count>, so a count is actually honored.
- Converge VimwikiTableMoveColumn{Left,Right}: the Vim-branch commands now call
  the table_move_column_left/right aliases, matching the Neovim branch.
- VimwikiColorize/NuwikiColorize: -nargs=1 → -nargs=* (all four defs) for
  upstream parity; a bare :VimwikiColorize now prompts for the colour.

Docs (README + doc/nuwiki.txt) updated; mapping surfaces gain <D-CR> and
<Leader>w<Leader>m in both harnesses. vim 265+18, nvim 272, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:28:06 +00:00
gffranco b8586537f8 feat(diary): restore diary_start_week_day via configurable weekly naming
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 34s
CI / cargo test (push) Successful in 31s
CI / editor keymaps (push) Successful in 1m34s
c63ec67 dropped diary_start_week_day, hardwiring the weekly diary to ISO
(Monday) weeks. Upstream vimwiki instead names a weekly note by the
week-start day's date (YYYY-MM-DD) and honours diary_start_week_day. Rather
than force one scheme, make it a per-wiki choice so migrators keep upstream
behaviour while existing nuwiki weekly files keep working:

- New per-wiki key `diary_weekly_style`: `iso` (default — `YYYY-Www`,
  Monday-based, nuwiki's original) or `date`/`vimwiki` (`YYYY-MM-DD` of the
  week-start day, upstream parity).
- Restored per-wiki key `diary_start_week_day` (`monday`..`sunday`, default
  monday); applies only in `date` mode.

Implementation:
- nuwiki-core::date gains WeeklyStyle, WeekStart, and DiaryCalendar (owns
  today/next/prev — date-mode snaps to the week-start and steps ±7 days;
  iso/daily/monthly/yearly defer to the existing DiaryPeriod logic).
- WikiConfig gains the two fields (+ defaults, RawWiki, From) and a
  diary_calendar() builder; commands.rs diary_open_relative and the
  next/prev pivot use it.

Defaults preserve current behaviour (iso/monday), so no breaking change.
Tests: DiaryCalendar cases in nuwiki-core/tests/diary.rs (snap, ±7, sunday
start, iso delegation, daily) + config round-trip in
nuwiki-lsp/tests/index_and_config.rs. README + doc/nuwiki.txt + lua config
comment updated. fmt + clippy clean; all crate tests + config-parity green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:06:09 +00:00
gffranco 086455e19b fix(client): P2 diary tab collision + <M-CR> badd-link parity
CI / cargo fmt --check (push) Successful in 36s
CI / cargo clippy (push) Successful in 40s
CI / cargo test (push) Successful in 32s
CI / editor keymaps (push) Successful in 1m35s
Two upstream-parity keymap fixes (confirmed against upstream
ftplugin/vimwiki.vim):

1. <Leader>w<Leader>t collision — nuwiki bound both <Leader>w<Leader>t and
   <Leader>w<Leader>m to tomorrow's diary. Upstream: <Leader>w<Leader>t opens
   today's diary in a NEW TAB, <Leader>w<Leader>m opens tomorrow. <Leader>w
   <Leader>t now calls diary_today_tab; <Leader>w<Leader>m stays tomorrow.

2. <M-CR> badd-link — adding a link target to the buffer list was mouse-only
   (<MiddleMouse>) / command-only (:VimwikiBaddLink). Upstream binds
   <M-CR> -> VimwikiBaddLink. Added normal-mode <M-CR> -> badd_link to the
   links group of both clients.

Both clients (lua/nuwiki/keymaps.lua, ftplugin/vimwiki.vim); README + doc/
nuwiki.txt updated. Tests: map[n].<M-CR> in both harnesses;
diary.leader_t_opens_today_in_tab / diary.leader_m_opens_tomorrow (vim).
nvim 270, vim 263+18, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:21:49 +00:00
gffranco 6983daa637 fix(follow-link): resolve+open definitions ourselves on the Vim clients
CI / cargo fmt --check (push) Successful in 16s
CI / cargo clippy (push) Successful in 21s
CI / cargo test (push) Successful in 32s
CI / editor keymaps (push) Successful in 1m39s
The Vim follow-link path delegated to the LSP client's jump UI
(:LspDefinition / coc jumpDefinition), which broke two ways for wiki
link-following:

1. **No create-on-follow (vim-lsp + coc).** vim-lsp's location handler
   readfile()s the target to build a quickfix entry; for a not-yet-created
   page that throws E484 and aborts the jump, so <CR> on [[NewPage]] did
   nothing. The server was correct throughout — textDocument/definition
   returns a *synthesised* location for missing pages (verified over
   JSON-RPC).
2. **Wrong window placement (coc).** Without an explicit open command coc
   fell back to coc.preferences.jumpCommand (e.g. a tab command).

Stop delegating: nuwiki#commands#follow_link_or_create() /
follow_link_drop() now resolve textDocument/definition directly and open
the URI themselves via a shared s:open_definition(open_cmd) helper — :edit
for <CR>, `tab drop` for <C-S-CR>. :edit opens a buffer for a missing path
(save creates it, matching vimwiki) and gives exact current-window
placement regardless of the user's coc jumpCommand. Removes
s:jump_definition / s:drop_from_response.

Also fix the coc action name: CocAction('getDefinition') does not exist
(E605 "Action getDefinition does not exist") — the registered action is
'definitions'. Fixed in s:open_definition and badd_link.

coc config delivery: the setup guidance shipped without
initializationOptions, so coc users never sent wiki_root and the server
resolved links against its default root (existing pages flagged broken,
follow-to-create landed in the cwd). The printed snippet
(autoload/nuwiki/lsp.vim) now emits initializationOptions populated with
the resolved settings, and the README coc snippet documents it as required.

Add development/start-vim-coc.sh — a coc.nvim dev launcher (sibling of
start-vim.sh) that wires coc's prebuilt release branch + a generated
coc-settings.json (with initializationOptions), deliberately omitting
vim-lsp so the coc path is exercised; doubles as a reproducer.

Tests: new cr.follow_opens_missing_page_in_current_window case in
test-keymaps-vim.vim (stubs CocAction to a missing page, asserts
current-window :edit). Keymap suite 254+18 green; config-parity (Vim +
Neovim) green. Neovim path unchanged and re-confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 15:01:14 +00:00
gffranco 1521d6c096 docs(client): document bare gl/gL remove-checkbox + tick parity gap P1 #3
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 29s
CI / cargo test (push) Successful in 26s
CI / editor keymaps (push) Successful in 1m22s
Update the lists keymap tables and group hints (gl/gL = remove checkbox;
remove-done is command-only via :NuwikiRemoveDone[!]), note the 'timeoutlen'
prefix behavior, and tick P1 #3 in the vimwiki gap tracker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:41:33 -03:00
gffranco 164b326216 docs(client): document gl/gL list change-symbol mappings
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Failing after 23s
CI / cargo test (push) Successful in 37s
CI / editor keymaps (push) Successful in 1m27s
Add gl<sym>/gL<sym> to the lists keymap tables in README and help, noting
that 1) is command-only. Tick parity gap P1 #2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:10:15 -03:00
gffranco 214707e327 docs(client): document link-follow commands on both editors + tab-drop
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Failing after 18s
CI / cargo test (push) Successful in 40s
CI / editor keymaps (push) Successful in 1m26s
The split/tab link-follow commands are no longer Neovim-only; document
:Nuwiki/Vimwiki{Split,VSplit,Tabnew,TabDrop,GoBack}Link and note that
<C-S-CR> follows a link in a tab reusing an existing one. Tick parity
gap P1 #1 in the tracking doc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 07:54:32 -03:00
gffranco e9243f743e feat(client): add per-subgroup Vim mapping opt-out globals
CI / cargo fmt --check (push) Successful in 40s
CI / cargo clippy (push) Failing after 38s
CI / cargo test (push) Successful in 38s
CI / editor keymaps (push) Successful in 1m30s
The plain-Vim ftplugin only exposed a whole-layer
g:nuwiki_no_default_mappings gate, so Vim users could not drop a single
keymap group the way Neovim users can via mappings.<group> = false. Add
g:nuwiki_no_{wiki_prefix,links,lists,headers,table_editing,diary,
html_export,text_objects}_mappings, each wrapping its group block, to
reach parity with lua/nuwiki/keymaps.lua. Cover them with a dedicated
opt-out harness and document them in the README and help file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:40:29 -03:00
gffranco d22c81ea4f docs(config): drop client-side keys and refresh per-wiki reference
Remove nested_syntaxes, maxhi and diary_start_week_day from the config
reference (block, tables and key lists) since the server no longer
consumes them, and note that language-tagged code fences are highlighted
automatically with no nested_syntaxes key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:12:14 -03:00
gffranco e3f28d7dfc test(config): cover link_severity in parity harnesses and docs
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 31s
CI / cargo test (push) Successful in 31s
CI / editor keymaps (push) Successful in 1m28s
Both editor harnesses now emit diagnostic.link_severity and exercise a
non-default override ('error') in the sample scenario, diffed against the
shared golden. Drop the README "not wired" caveat now that the value flows
through, and document the g:nuwiki_link_severity global.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:53:56 -03:00
gffranco 47af0d6c1e docs(config): add configuration reference tables and fix links_space_char default
Document every config key (top-level, mappings, per-wiki, HTML export, and
g:nuwiki_* globals) with types, defaults, and accepted values in the README
Configuration section. Correct the links_space_char doc comment: the default
is a literal space (spaces kept verbatim), not "_". Note that
diagnostic.link_severity is accepted but not yet wired through to the server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 19:24:08 -03:00
gffranco 63ef1b59a2 docs(parity): document new commands and extend keymap harness coverage
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 44s
CI / cargo test (push) Successful in 47s
CI / editor keymaps (push) Successful in 1m30s
Update the README command tables for the full :Nuwiki*/:Vimwiki* surface
and add command-registration assertions (removeCheckbox, catUrl, and the
other parity entry points) to both the Vim and Neovim keymap harnesses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 17:36:05 -03:00
gffranco b1155dee6a Expand README: coc setup, complete Nuwiki-first command/keymap tables
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 22s
CI / cargo test (push) Successful in 47s
CI / editor keymaps (push) Successful in 1m56s
Document third-party LSP client setup (vim-lsp auto-registration and the
coc.nvim coc-settings.json snippet), add a Requirements section and
:NuwikiInstall. Rewrite the command reference as Nuwiki-first tables with
their :Vimwiki* counterparts, list the full command and mapping surface
(including mouse maps), and fix the <Leader>ww description.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:54:12 -03:00
gffranco 8ab6015405 Major clean up and improvements to vimwiki compatibility and documentation.
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m25s
Reviewed-on: #1
2026-05-30 18:35:40 +00: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 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 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 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 63e5b6d514 phase 19: editor glue v2
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m18s
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>
2026-05-11 21:57:05 +00:00
gffranco 106268d488 phase 18: multi-wiki resolver + picker commands
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m14s
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>
2026-05-11 21:46:33 +00:00
gffranco d1b807b7d1 phase 17: html export commands + extended template vars
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m26s
CI / cargo test (push) Successful in 1m16s
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>
2026-05-11 21:37:36 +00:00
gffranco e75ad6ca89 phase 16: diary path conventions + nav commands
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m20s
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>
2026-05-11 21:04:55 +00:00
gffranco ce3ac8c4f8 phase 15: link health + TOC/links/orphans
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m29s
CI / cargo test (push) Successful in 1m40s
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>
2026-05-11 20:49:32 +00:00
gffranco f1d046fad1 phase 14: list/heading edit commands + CommandOutcome split
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m11s
CI / cargo test (push) Successful in 1m11s
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>
2026-05-11 17:41:11 +00:00
gffranco cb4bfae5e7 phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m15s
CI / cargo test (push) Successful in 1m15s
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>
2026-05-11 16:27:39 +00:00
gffranco 8b29d4e9b9 phase 12: tags — lexer, AST, index, anchor resolution, ws/symbol
CI / cargo fmt --check (push) Successful in 18s
CI / cargo clippy (push) Successful in 1m3s
CI / cargo test (push) Successful in 1m11s
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>
2026-05-11 14:54:55 +00:00
gffranco ea574f23f8 phase 11: plumbing prereqs — Wiki, edits, config, snapshot, diags
CI / cargo fmt --check (push) Successful in 25s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m15s
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>
2026-05-11 14:32:22 +00:00
gffranco 02291c2744 phase 10: release pipeline (Gitea release on v* tag)
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m13s
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>
2026-05-11 00:47:40 +00:00
gffranco ee61d40872 phase 9: editor glue — plugin, LSP wiring, install, health, docs
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m23s
CI / cargo test (push) Successful in 1m29s
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>
2026-05-11 00:38:09 +00:00
gffranco 4d461d2425 phase 8: navigation — definition, refs, hover, completion, ws/symbol
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m6s
CI / cargo test (push) Successful in 1m17s
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>
2026-05-11 00:27:06 +00:00
gffranco 6efe154f1a phase 7: semantic tokens for syntax highlighting
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m20s
`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>
2026-05-10 21:14:01 +00:00
gffranco c44db37bab phase 6: LSP foundation — didOpen/didChange, diagnostics, outline
CI / cargo fmt --check (push) Successful in 48s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m19s
`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>
2026-05-10 19:44:44 +00:00
gffranco f00b0780b8 phase 5: renderer trait + HtmlRenderer
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 50s
CI / cargo test (push) Successful in 57s
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>
2026-05-10 19:12:01 +00:00
gffranco 294963b517 phase 4: vimwiki parser + SyntaxPlugin glue
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 1m2s
CI / cargo test (push) Successful in 1m5s
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>
2026-05-10 17:58:41 +00:00
gffranco 942dbe2aa8 phase 3: vimwiki lexer
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 53s
CI / cargo test (push) Successful in 1m2s
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>
2026-05-10 17:20:46 +00:00
gffranco cf336ee839 phase 0: scaffold workspace, plugin layout, and CI
CI / cargo fmt --check (push) Successful in 46s
CI / cargo clippy (push) Successful in 1m33s
CI / cargo test (push) Successful in 53s
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>
2026-05-10 16:01:58 +00:00
gffranco cf751383e1 initial setup 2026-05-10 14:53:58 +00:00