- Add class='toc' to TOC heading in HTML export by detecting the
configured toc_header value in render_heading()
- Add toc_header field to HtmlRenderer and wire it through
render_page_html()
- Add toc_header, toc_header_level, html_header_numbering, and
html_header_numbering_sym to Lua config defaults so users can
discover and set them
Exported HTML anchor links (TOC entries, [[Page#Heading]], [[#Heading]])
went nowhere because heading elements carried no `id`. Worse, the two TOC
builders slugified anchors (`#my-heading`) while the link resolver emits
the raw heading text (`#My Heading`, matching upstream vimwiki), so even
once ids existed the two halves wouldn't have agreed.
Fix — adopt vimwiki's scheme (raw heading text as the anchor) everywhere:
- render_heading emits `<hN id="<plain heading text>">`.
- build_toc_html (HTML export TOC) uses the raw, HTML-escaped title for
both the href and the link text (was slugify).
- collect_toc_items (:VimwikiTOC buffer output) uses the raw title for the
generated `[[#anchor]]` (was slugify). In-editor navigation slugifies
both sides, so it still resolves.
Consistency: add canonical `nuwiki_core::ast::inline_text()` and route the
heading id, the HTML-TOC title, the buffer-TOC title, and
`diagnostics::heading_text` through it — three duplicated extractors
collapsed into one, so the anchor and its validation can't drift.
Regression test: heading_id_matches_anchor_link_href (core) asserts the
heading id and a `#anchor` link's href are identical. Updated the heading
assertions in the renderer/export tests for the new `id=` attribute.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the 18 findings from the parallel codebase review, tracked in
development/vimwiki-gap.md.
Correctness / perf:
- Wiki.config -> Arc<WikiConfig> so cloning a Wiki is a refcount bump (R5)
- WorkspaceIndex::remove is no longer O(n^2): a per-source contributions
map limits the scan to buckets the source actually wrote into (R6)
- render_color now expands color_tag_template (__STYLE__/__CONTENT__),
consuming the previously-dead field; ColorNode documented as an
extension point; 3 renderer tests added (R3/R4)
- wiki_root_for returns empty/nil on no-match instead of falling back to
the first wiki (R2); auto_header honours links_space_char (R7)
Cleanup / dedup:
- Remove dead #[allow(dead_code)] stubs + uncalled pub helpers, narrow
imports (R10/R11)
- Dedup span_of_inline x3 -> InlineNode::span() (R12)
- diary_step single read lock; page_captions single pass (R13)
- Lua auto_header loop -> wiki_list(); detect_current_symbol cleanup
(R14/R18)
Client / docs:
- :VimwikiNormalizeLink Vim cmds -> <q-args> (R17); ftplugin header fix
(R16); vars.vim multi-wiki limitation documented (R15)
- Document 19 config options in README.md + doc/nuwiki.txt; fix
list_margin/shiftwidth doc and stale comments (R1/R9)
- R8 investigated, confirmed not a real bug (documented)
Verified: Neovim harness 307, Vim harness 301/18/21, Rust suite 568, all
0 failed; clippy clean; fresh parallel-agent audit found no regressions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the deferred half of these options: a soft line break inside a
paragraph / list item can render as <br /> instead of a space.
- Parser: soft breaks now emit a dedicated InlineNode::SoftBreak node
(parse_inline_seq K::Newline + the list-continuation synthetic join) instead
of collapsing to Text(" "). Added SoftBreakNode + the variant.
- All inline consumers treat SoftBreak as whitespace: AST visitor (no-op),
text extraction (index.rs + lib.rs → space), nav/semantic_tokens span_of_inline,
semantic emit (skip like Text), parser span_of_inline.
- Renderer: render_inlines_break renders top-level SoftBreaks as the
context-specific string; render_paragraph uses text_ignore_newline,
render_list_item uses list_ignore_newline; default render_inline → space.
with_newline_handling builder; export.rs wires both from HtmlConfig.
Default (true/true = upstream default) behaviour is unchanged (space). Updated
3 core tests + 1 parser test that asserted the old Text(" ") shape to treat
SoftBreak as whitespace. New test render_page_html_ignore_newline_controls_soft_breaks.
Gap doc: P3 Config item closed — only the deferred markdown cluster remains.
Full rust suite + clippy clean; Neovim 307, Vim 301/18/21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parallel-agent audit of the config batch found:
- cycle_bullets used find_marker_span's third element as an indent column, but
it's the ABSOLUTE byte offset after the marker — so depth (and the rotated
glyph) was wrong for any item not near offset 0 (tests passed by coincidence).
Fixed to use the marker span's column (the indentation). Regression test now
places the list deep in the document.
- :VimwikiColorize interpolated the colour into the template via an unescaped
replacement: Lua gsub treats `%` specially, Vim substitute() treats `&`/`\`.
Lua now uses a function replacement (verbatim); Vim escapes `\&~`.
- render_swapped_table applied table_reduce_last_col before the column swap, so
a swap involving the last column moved the narrow slot — now clamped after.
- Fixed a stale toc_link_format doc comment (format 1 = [[#anchor]], not a
full parent path).
Verified-correct by the audit (no change needed): RSS rfc822/cdata/fidelity,
prune_orphan_html reverse-mapping + guards, write_escaped_allowing/match_allowed_tag
(multibyte-safe), substitute_emoji (byte-boundary-safe), resolve_target_uri
create_link/dir_link ordering, all config defaults + From/constructors.
Full rust suite + clippy clean; Neovim 307, Vim 301/18/21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
allToHtml now prunes orphan HTML files (no corresponding wiki source) under
html_path — export_ops::prune_orphan_html walks the output tree, maps each
<rel>.html back to <root>/<rel>.<ext>, and deletes those with no source,
skipping the CSS file and any basename in user_htmls. Gated on
!html_filename_parameterization (slugified names can't be reverse-mapped),
mirroring upstream; the pruned paths are reported in the command result.
hl_headers / hl_cb_checked are NOT implemented as toggles: nuwiki already
highlights headers per-level (@vimwikiHeading.level1..6) and checkboxes
always-on via LSP semantic tokens, which supersedes the upstream opt-in
toggles (default off) — same class as the maxhi divergence. Documented in the
gap doc rather than degrading the existing highlighting.
Test: prune_orphan_html_deletes_sourceless_keeps_protected. Full rust suite +
clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the RSS-fidelity change: the `<rss version="2.0">` substring now
carries the xmlns:atom attribute, so match the open-tag prefix instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
write_rss now emits upstream's full RSS 2.0 structure:
- xmlns:atom on <rss>; channel <link> points at the diary index page
(base_url + diary_rel + diary_index + .html), not bare base_url.
- <atom:link rel="self"> to base_url + rss_name.
- channel + per-item <pubDate> (RFC-822, derived from the diary date;
1970-01-01=Thu weekday math via to_days_epoch).
- per-item <guid isPermaLink="false"> holding the bare date.
- per-item <![CDATA[ rendered page body ]]> (render_entry_body reads+parses
the entry and renders body-only with the wiki's colour/valid-tag/emoji opts;
cdata_safe splits any literal ]]>).
- rss_name drives the output filename; rss_max_items caps the feed (0 =
unlimited).
Tests: updated write_rss_uses_base_url_for_public_links for the new channel
link/guid/atom/pubDate; new write_rss_honours_rss_name_and_max_items. Full
rust suite + clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- valid_html_tags: HTML export now passes the allowlisted inline tags through
verbatim (b/i/sub/sup/kbd/div/center/strong/em/...) and escapes the rest,
via a render-time tag-aware escaper (write_escaped_allowing/match_allowed_tag)
— mirroring upstream's s:safe_html_line regex rather than a parser change.
`span` is always allowed so colour spans render.
- emoji_enable: render-time `:alias:` -> glyph substitution (curated common
set), default on; with_emoji builder + substitute_emoji/emoji_for.
- color_tag_template: :VimwikiColorize now wraps via a configurable template
(g:nuwiki_color_tag_template / setup color_tag_template), splitting on
__CONTENT__ with __COLORFG__ -> colour; both clients. Colour spans render in
export via the valid_html_tags span passthrough. Default template reproduces
the prior `<span style="color:NAME">` output (harness colorize tests green).
Renderer gets with_valid_html_tags + with_emoji; export.rs wires both from
HtmlConfig. Tests: valid_html_tags passthrough + escape, emoji on/off
(html_export.rs). Full rust suite + both harnesses green (Neovim 307,
Vim 301/18/21); clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cycle_bullets (default off): when an unordered list item changes indent level
(gll/glh), rotate its glyph through the configured bullet_types as a depth-keyed
ring buffer — change_level_edit gains cycle_bullets + bullet_types params,
locates the marker via find_marker_span, and rewrites the glyph to
bullet_types[new_depth % len] for single-char unordered markers. Ordered
markers are untouched. list_change_level reads both from the wiki config.
Tests: cycle_bullets_rotates_glyph_on_indent + cycle_bullets_off_leaves_glyph.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- create_link (default true): server resolve_target_uri now only synthesises a
missing-page URI when create_link is set; when off, following a link to a
non-existent page returns no location (no-op).
- dir_link: a [[dir/]] link now resolves to <dir>/<dir_link><ext> when dir_link
is set, else opens the directory itself. Uses the already-parsed
LinkTarget.is_directory.
- auto_header (default off): new wiki pages get a `= Stem =` header derived from
the filename, skipping the wiki index + diary index. Client-side BufNewFile
autocmd in both clients (init.lua + plugin/nuwiki.vim), with auto_header
handlers in both command layers (trailing-slash-robust root matching).
Tests: cmd.auto_header_inserts + cmd.auto_header_skips_diary_index (nvim) /
cmd.auto_header_skips_index (vim). Full rust suite + both harnesses green
(Neovim 307, Vim 301/18/21).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`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>
`[[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>
`[[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>
`[[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>
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>
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>
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>
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>
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>