Commit Graph

70 Commits

Author SHA1 Message Date
gffranco b2f2fc88bd feat(config): P3 config batch — group 1 (clean config + plumbing)
Config plumbing for the whole config batch (HtmlConfig + WikiConfig fields,
RawWiki deserialization, From/defaults) plus these behaviors:

- rss_name / rss_max_items: HtmlConfig fields (wired into write_rss in the RSS
  group).
- toc_link_format: build_toc_text emits [[#anchor]] (no description) for 1,
  [[#anchor|title]] for 0.
- generated_links_caption: build_links_text emits [[page|FirstHeading]] from a
  page->heading map (ops::page_captions) when enabled.
- table_reduce_last_col: column_widths clamps the last column to width 1 when
  set; threaded through table_align_edit/table_move_column_edit + commands.
- color_dic now ships a populated default palette (red/green/blue/...).
- commentstring aligned to upstream's no-space `%%%s`.
- auto_chdir (default off): :lcd into the owning wiki root on buffer enter;
  both clients (ftplugin.lua setup_auto_chdir + Vim NuwikiAutoChdir augroup),
  with config.wiki_root_for / nuwiki#commands#wiki_root_for resolvers.

Also seeded HtmlConfig fields for later groups (valid_html_tags,
list/text_ignore_newline, emoji_enable, user_htmls, color_tag_template) and
WikiConfig (create_link, dir_link, bullet_types, cycle_bullets).

Tests: toc_link_format, generated_links_caption, table_reduce_last_col,
config defaults + JSON round-trip for every new key. Full rust suite +
both keymap harnesses green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:04:36 +00:00
gffranco 9441fe918c fix(parity): close the 2026-06-02 re-audit config/command findings
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 37s
CI / cargo test (push) Successful in 29s
CI / editor keymaps (push) Successful in 1m36s
Implements the remaining fourth-pass findings (gap doc updated):

- list_margin: rework to upstream's buffer-side meaning. Drop the
  nuwiki-only HTML em-margin (renderer field/method + render_page_html
  param removed) and prepend max(0, list_margin) leading spaces to every
  generated bullet in build_toc_text / build_links_text /
  build_tag_links_text / diary::build_index_body; headings stay at col 0.
  From<RawWiki> derives 0 for markdown wikis when unset. Negatives can't
  resolve 'shiftwidth' server-side, so they collapse to zero indent
  (documented divergence).

- diary_months: per-wiki Vec<String> (default 12 English names), threaded
  into the diary-index month labels; missing/empty slots fall back to the
  English name.

- diary_caption_level: widen u8 -> i8 so vimwiki's -1 (min: -1) parses;
  build_index_body clamps < 0 to base tree level 0.

- VimwikiRemoveDone: regain upstream's -range. All four defs are now
  -bang -range, dispatched via remove_done(bang, range, l1, l2) in both
  clients: ! -> whole buffer, explicit range -> new list_remove_done_range
  ({range:[l1-1,l2-1]}), else current list. Server remove_done_edit gained
  an Option<(u32,u32)> range that filters whole-doc victims by start line.

markdown_header_style is deferred: the generators emit vimwiki syntax only
(caption_line never writes markdown headers), so there's no markdown header
to attach the style to. Logged as the "generated-content is vimwiki-only"
intentional divergence pending a later markdown generated-content effort.

Tests: list_margin indent, markdown list_margin-0 default, diary_months
custom + fallback, negative caption_level clamp/parse, ranged remove-done
(server + both keymap harnesses). 553 lsp/core tests pass; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:50:38 +00:00
gffranco 79b502fa5c feat(html): html_header_numbering section numbering for HTML export
CI / cargo fmt --check (push) Successful in 51s
CI / cargo clippy (push) Successful in 27s
CI / cargo test (push) Successful in 49s
CI / editor keymaps (push) Successful in 2m52s
Mirror vimwiki's html_header_numbering / html_header_numbering_sym:
top-level headings at or below the start level get a dotted section
number (1, 1.1, 1.2, ...) with a configurable trailing symbol. Off by
default (level 0). Nested headings in lists/quotes stay unnumbered,
matching upstream's document-level-only scan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:33:01 +00:00
gffranco 2da2168d88 fix(diary): complete the -count wiki selector (repair 874bdd0)
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 38s
CI / cargo test (push) Successful in 40s
CI / editor keymaps (push) Successful in 1m37s
874bdd0 committed the diary -count feature with several Edits that had
silently no-matched, leaving the tree non-compiling and the clients
inconsistent (CI run 236 failed). This completes it:

- commands.rs: add the `wiki` field to OptionalUriArg (the previous edit
  targeted a wrong struct name) and pass None to the six resolve_diary_wiki
  callers that take no selector (date/list/step paths). Server builds clean.
- autoload/nuwiki/commands.vim + lua/nuwiki/commands.lua: actually thread the
  count through s:diary_open / _diary_open and the diary_today/today_tab/
  yesterday/tomorrow/index handlers (these edits had failed before, so the
  ftplugin defs were calling handlers that ignored/rejected the arg).
- Add the test cases the prior commit referenced but never landed:
  cmd.VimwikiMakeDiaryNote_has_count (test-keymaps.lua) +
  cmd.Vimwiki{MakeDiaryNote,DiaryIndex}_accepts_count (test-keymaps-vim.vim).
- Fix the OptUriArg→OptionalUriArg name in the gap-doc note.

Verified with CI flags: workspace test/clippy/fmt clean; lua 284, vim 272/18/21,
all 0 failed; the three new diary cases pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:25:08 +00:00
gffranco 874bdd0c02 feat(diary): -count wiki selector for diary-note + index commands (P3)
CI / cargo fmt --check (push) Successful in 25s
CI / cargo clippy (push) Failing after 34s
CI / cargo test (push) Failing after 33s
CI / editor keymaps (push) Has been skipped
Closes the last command-cluster gap. Upstream's diary-note family
(:Vimwiki{Make,TabMake,MakeYesterday,MakeTomorrow}DiaryNote + VimwikiDiaryIndex,
and the Nuwiki* forms) carry -count=0 where the count selects the wiki number;
nuwiki's were bare. Implemented as a real end-to-end selector, reusing existing
infrastructure:

- Server (commands.rs): OptUriArg gained an optional `wiki` selector;
  resolve_diary_wiki(backend, uri, wiki) now prefers it (via the existing
  resolve_wiki_selector, which already handles a 0-indexed wiki number) over
  the buffer URI. Used by diary_open_relative + diary_open_index.

- Clients: s:diary_open / _diary_open (both) and diary_today/today_tab/
  yesterday/tomorrow/index take a count and send {wiki: count-1} when >0. All
  22 diary-note + DiaryIndex command defs across the 4 contexts are now
  -count=0 passing <count>. diary_next/prev ignore the count.

Tests: cmd.VimwikiMakeDiaryNote_has_count (test-keymaps.lua) +
cmd.Vimwiki{MakeDiaryNote,DiaryIndex}_accepts_count (test-keymaps-vim.vim).
Workspace test/clippy/fmt clean with CI flags; lua 284, vim 272/18/21.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:16:03 +00:00
gffranco 724c12ac16 style: rustfmt the custom_wiki2html test assert (fixes red CI)
CI / cargo fmt --check (push) Successful in 42s
CI / cargo clippy (push) Successful in 38s
CI / cargo test (push) Successful in 36s
CI / editor keymaps (push) Successful in 1m26s
The custom_wiki2html test added in 23aec3e had an over-width `assert!`
that `cargo fmt` wants wrapped; CI's `cargo fmt --all -- --check` job has
been failing on it since. No behavior change — `cargo fmt --all` only.

Verified locally against the exact CI matrix: fmt --check clean, clippy
--workspace --all-targets -D warnings clean, test --workspace --all-targets
all pass, keymap + config harnesses pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:45:35 +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 3fd04b0efa fix(lsp): section-range termination is level-aware (caption audit follow-up)
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 37s
CI / cargo test (push) Successful in 27s
CI / editor keymaps (push) Successful in 1m38s
The post-captions audit found find_section_range terminated a generated
section only at the next *level-1* heading. With a custom non-1
*_header_level (e.g. a level-2 Generated Tags index) a following same-level
section could be absorbed into the replaced span. Terminate at the next
heading of level <= the matched section's level instead, which keeps the
deeper `level+1` sub-groups inside while stopping at a sibling/parent.

Test: find_section_range_stops_at_same_level_sibling. 0 failures, clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:49:00 +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 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 28d5caf581 fix(diary): default diary_caption_level to 0 (upstream parity)
nuwiki defaulted the per-wiki diary_caption_level to 1; upstream vimwiki's
default is 0 (year captions at the top level, months one below, rather than
nested one deeper). Default it to 0 in wiki_defaults(); still per-wiki
overridable. Render tests pass an explicit level so are unaffected; the
default-value assertion in defaults_carry_vimwiki_per_wiki_keys updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:50:59 +00:00
gffranco a53ba167d6 fix(lsp): silence clippy too_many_arguments on render_page_html
CI / cargo fmt --check (push) Successful in 17s
CI / cargo clippy (push) Successful in 33s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m34s
Adding list_margin pushed render_page_html to 8 params, tripping
clippy::too_many_arguments under -D warnings in CI. Every parameter is a
distinct render input and the function has a single production caller, so
scope an #[allow] rather than introduce an options-struct abstraction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 08:13:30 -03:00
gffranco 3a33d53c22 feat(lsp): drive checkbox commands from the wiki listsyms palette
Parse editor and export documents with each wiki's configured listsyms,
and make toggle/cycle/reject plus parent-propagation walk the real
palette glyphs instead of the hardcoded ' .oOX'. Falls back to the
default palette when no wiki matches the URI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:32:15 -03:00
gffranco 85094d6e3b feat(core): make checkbox lexing honor a configurable listsyms palette
Add a ListSyms type (vimwiki's g:vimwiki_listsyms) and thread it through
the lexer so checkbox glyphs are recognized from the configured palette
rather than the hardcoded ' .oOX'. Glyphs map to the existing five-bucket
CheckboxState, so the HTML renderer and AST consumers are unchanged.
VimwikiSyntax::parse_with_listsyms opts into a custom palette; the trait
parse() keeps the default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:32:07 -03:00
gffranco c63ec679ae feat(lsp): wire diary/auto_toc/links_space_char/list_margin; drop client-side keys
Consume several per-wiki config keys that the server deserialized but
ignored, and remove keys whose effect is purely client-side:

- diagnostics: re-publish open-doc diagnostics on
  didChangeConfiguration so a link_severity change takes effect
  immediately; fix the single-key unwrap in apply_change so a minimal
  `{ diagnostic: {...} }` payload isn't mistaken for a namespace wrapper.
- auto_toc: rebuild an existing TOC section on save (new
  ops::toc_rebuild_edit, no-op when the page has no TOC).
- diary index: honour diary_header, diary_sort and diary_caption_level
  when rendering the diary index body.
- links_space_char: apply on rename so spaces in the link target and
  the on-disk path become the configured glyph (default " " = verbatim).
- list_margin: thread the per-wiki value into render_page_html.
- remove nested_syntaxes, maxhi and diary_start_week_day from the server
  config: nested-syntax and heading highlighting are client-side, and
  the weekly diary is ISO-week based so a custom week start has no clean
  server-side meaning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:12:08 -03:00
gffranco 0741c349db feat(core): support configurable list_margin in HTML list rendering
Add `HtmlRenderer::with_list_margin`. When the margin is >= 0 the
outermost list gets an inline `margin-left:<n>em`; nested lists and the
default of -1 stay unstyled (deferring to the stylesheet). Wires the
per-wiki vimwiki `list_margin` key into the export pipeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:11:56 -03:00
gffranco 9b6413c3de feat(lsp): wire diagnostic.link_severity through from client config
The broken-link diagnostic severity was already consulted by
collect_diagnostics, but Config.diagnostic was hardcoded to the default and
never populated from initializationOptions/didChangeConfiguration. Parse a
client-supplied `diagnostic.link_severity` ('off'|'hint'|'warn'|'error',
case-insensitive) into LinkSeverity and apply it in both from_init_params and
apply_change. Partial config updates that omit `diagnostic` preserve the
existing severity rather than resetting it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:53:46 -03:00
gffranco b922f0d612 test(config): verify Vim/Neovim config payload parity
CI / cargo fmt --check (push) Successful in 15s
CI / cargo clippy (push) Successful in 27s
CI / cargo test (push) Successful in 30s
CI / editor keymaps (push) Successful in 1m24s
Expose the init_options payload builders publicly in both clients
(M.init_options/M.server_settings in Lua, nuwiki#lsp#settings() in Vim) so
they can be inspected. Add editor harnesses that dump each client's
server-bound config as deterministic key=value lines and diff against a
shared golden file: nvim == golden and vim == golden together prove the two
clients send identical config. Add a server-side test asserting the Vim flat
shape and the Neovim {nuwiki:{...}} wrapper desugar to the same WikiConfig.
Wire both harnesses into CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 19:24:20 -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 8d8d85daf9 feat(lsp): add list.removeCheckbox and link.catUrl commands
removeCheckbox strips the `[ ]` marker (plus its trailing space) from
the current item or every item in the list. catUrl returns the
`file://` URL of the current page's HTML export, mirroring vimwiki's
:VimwikiCatUrl.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 17:32:41 -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 fa2d1b8b42 feat(keywords): add STOPPED keyword
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 19s
CI / cargo test (push) Successful in 31s
CI / editor keymaps (push) Successful in 1m18s
Adds STOPPED alongside TODO/DONE/STARTED/FIXME/FIXED/XXX across the stack:
lexer, AST Keyword enum, HTML renderer (class="stopped"), LSP keyword_str,
and the Vim syntax red group (nuwikiKeywordAttn). Grouped with the
attention/pending keywords (red) for both export and in-editor highlighting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:05:23 -03:00
gffranco e0f97a5caf fix(export): emit one CSS class per keyword for independent colouring
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 39s
CI / cargo test (push) Successful in 30s
CI / editor keymaps (push) Successful in 1m17s
All keywords previously shared `class="todo"`, so a stylesheet couldn't
distinguish e.g. red TODO from green DONE. Emit a distinct class per keyword
(todo/done/started/fixme/fixed/xxx); TODO keeps the vimwiki `todo` class so
stock vimwiki CSS still styles it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:43:11 -03:00
gffranco e90bbab39e fix(export): render vimwiki-compatible HTML for templates, links, and tasks
Stock vimwiki templates and stylesheets rendered broken pages because the
HTML output diverged from vimwiki conventions on three fronts:

- Templates: the renderer only substituted nuwiki's `{{key}}` placeholders,
  so vimwiki's `%title%`/`%content%`/`%root_path%`/`%date%`/`%wiki_css%`
  passed through literally — dropping the body and breaking asset links.
  Both delimiter styles are now recognised; `%wiki_css%` aliases the css var.
- Links: `[[todo.wiki]]` exported to `todo.wiki.html`. render_page_html now
  takes the wiki file extension and strips it from wiki/interwiki targets
  (via index::strip_wiki_extension) before the `.html` URL is built, matching
  in-editor navigation. file:/local: links keep their literal extension.
- Tasks: checkbox items used bespoke `task-*` classes plus an `<input>`,
  which double-rendered against vimwiki stylesheets. Emit `done0..done4`
  (`[ ] [.] [o] [O] [X]`) and `rejected` (`[-]`) classes with no input — the
  stylesheet draws the box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:19:55 -03:00
gffranco 66964be393 chore(fmt): apply cargo fmt to recent LSP changes
CI / cargo fmt --check (push) Successful in 11s
CI / cargo clippy (push) Successful in 14s
CI / cargo test (push) Successful in 21s
CI / editor keymaps (push) Successful in 1m10s
Pure rustfmt churn — the anchor-match, source-relative resolution,
and config-unwrap commits introduced lines that needed rustfmt
reformatting. CI's `cargo fmt --check` job was blocking on this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:34:31 -03:00
gffranco e5b0042a90 fix(lsp): refresh diagnostics after initial workspace scan completes
`didOpen` for the wiki's index page fires almost immediately after
`initialized`, but the background `index_workspace` task hasn't yet
parsed the other pages. Link-health diagnostics computed at that
point look up every cross-page link target in a near-empty index and
flag them all as missing-page — and the diagnostics are never
recomputed once the scan catches up, so the editor shows stale red
squiggles on every link until the buffer is edited.

After each wiki's initial scan finishes, walk every open document
that lives in that wiki, recompute its diagnostics against the now-
populated index, and re-publish. Snapshots both the URI list and
each doc's `(ast, text, version)` so we don't hold DashMap iterators
across awaits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:47:46 -03:00
gffranco f477f6e6a7 fix(lsp): resolve wikilinks source-relative first, with .. collapsing
`[[llm-wiki-pattern]]` in `tips/index.wiki` should resolve to
`tips/llm-wiki-pattern.wiki` — vimwiki's default is source-relative
for non-absolute targets. The index keyed every page by its
workspace-relative path, so the link looked up `llm-wiki-pattern`
(root-relative) and missed the sibling. Goto-definition then
synthesised a non-existent root URL, and `nuwiki.link` diagnostics
flagged every such link as missing-page.

  - `WorkspaceIndex::resolve_wiki_path` and `page_for_wiki_target`
    centralise the lookup: try `{source_dir}/{path}` first, fall back
    to root-relative. Used by goto-definition, diagnostics, and the
    workspace `checkLinks` walker.
  - `collapse_dots` resolves `.` / `..` segments before lookup so
    `[[../Other]]` from `posts/foo` correctly hits `Other` at the
    wiki root (and is reported broken if no such page exists, rather
    than silently going to nowhere).
  - `canonical_link_name` applies the same rule when keying the
    backlinks index, so backlinks queried by the resolved page name
    pick up references that were written source-relative.
  - `OutgoingLink` carries `is_absolute` now so `classify_outgoing`
    (driven from cached index data, not the live AST) gets the same
    treatment without re-parsing the source.
  - `classify_outgoing`'s File/Local branch now reuses
    `resolve_file_path` instead of duplicating the `//absolute` /
    relative-to-source logic inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:30:32 -03:00
gffranco 859d69e4fe fix(lsp): match wikilink anchors against raw heading text
`[[Page#Heading text]]` parsed the anchor as the literal string after
`#`, but the index stored each heading's anchor as `slugify(title)`.
The two never compared equal, so every TOC-style anchor link
(`[[#1. Topologia atual]]`, etc.) was reported as a broken anchor and
goto-definition couldn't jump to the right heading either.

Slugify the request before comparing — `slugify` is idempotent on
valid-slug input, so the same code path now accepts both
`[[Page#some-heading]]` (slug form) and `[[Page#Some Heading]]`
(raw text). Applied to diagnostics, navigation, and hover preview so
all three agree on what "an anchor matches a heading" means.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:29:30 -03:00
gffranco 2aec51274e fix(lsp): resolve wikilinks that include the .wiki file extension
`[[page.wiki]]` should resolve identically to `[[page]]`. The index
keyed by stem, so links with the explicit extension fell through to
the broken-link diagnostic and to-page navigation failed. Strip the
configured extension before every page lookup (resolve, definition,
backlinks, classify_link, classify_outgoing) and stop appending the
extension in synthesise_page_uri when it's already present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:00:03 -03:00
gffranco 6ae642dbee fix(config): unwrap {nuwiki:{...}} wrapper in apply_change
CI / cargo fmt --check (push) Failing after 22s
CI / cargo clippy (push) Successful in 21s
CI / cargo test (push) Successful in 29s
CI / editor keymaps (push) Successful in 1m19s
workspace/didChangeConfiguration settings from Neovim arrive as
{ "nuwiki": { "wiki_root": ..., "wikis": [...] } } because
server_settings() in lua/nuwiki/lsp.lua wraps the payload in a
"nuwiki" namespace key.  apply_change() was trying to deserialise that
outer object directly as InitOptions, which always succeeded but produced
all-None fields (unknown keys are ignored by serde), so every settings
refresh was silently dropped.

Unwrap single-key objects whose value is also an object before
deserialising — this handles the Neovim wrapper while keeping the flat
VimL shape working unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:24:51 -03:00
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 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 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 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 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 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 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