Commit Graph

25 Commits

Author SHA1 Message Date
gffranco b1155dee6a Expand README: coc setup, complete Nuwiki-first command/keymap tables
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 22s
CI / cargo test (push) Successful in 47s
CI / editor keymaps (push) Successful in 1m56s
Document third-party LSP client setup (vim-lsp auto-registration and the
coc.nvim coc-settings.json snippet), add a Requirements section and
:NuwikiInstall. Rewrite the command reference as Nuwiki-first tables with
their :Vimwiki* counterparts, list the full command and mapping surface
(including mouse maps), and fix the <Leader>ww description.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:54:12 -03:00
gffranco 8ab6015405 Major clean up and improvements to vimwiki compatibility and documentation.
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m25s
Reviewed-on: #1
2026-05-30 18:35:40 +00:00
gffranco a8b89a5303 fix(vim): correct broken symlink, missing commands, and bad VimwikiGoBackLink
CI / cargo fmt --check (push) Successful in 40s
CI / cargo clippy (push) Successful in 49s
CI / cargo test (push) Successful in 30s
CI / editor keymaps (push) Successful in 1m16s
- README: fix plain-Vim install symlink (ln -s target/… → ln -s ../target/…
  so the relative path resolves correctly from inside bin/)
- ftplugin: add missing VimwikiUISelect command on the Vim path
- ftplugin: add missing -count attribute on NuwikiTabIndex (Vim path)
- ftplugin: fix VimwikiGoBackLink (Neovim path) — `normal!` with no
  argument causes E471; use `execute "normal! \<C-o>"` instead

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:45:39 +00:00
gffranco 7c9e679169 docs: rewrite README into a proper user-facing intro
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 1m20s
CI / cargo test (push) Successful in 1m22s
CI / editor keymaps (push) Successful in 2m8s
The old README was mostly a phase-status table — useful for tracking
progress but unhelpful for a first-time reader landing on the repo.
Restructured around the three things a new user actually needs:

1. **What it is.** One-paragraph pitch + a feature checklist that
   mirrors the SPEC §13.3 status without burying the reader in phase
   numbers. Architecture details + the deferred §13.1 work stay in
   `SPEC.md`, linked from the intro.

2. **How to install.** Plugin-manager snippets for lazy.nvim,
   vim-plug, dein, and a plain-Vim packpath path. Plus a
   "try it without touching your config" pointer to
   `./start-nvim.sh` / `./start-vim.sh` so curious users can kick the
   tyres in 5 seconds.

3. **How to use.** A compact `setup({})` example covering wiki_root,
   multi-wiki, mappings groups, folding strategy, and diagnostic
   severity. Then command and keymap cheatsheets that distinguish
   "vimwiki-compat surface" from "things the LSP server returns",
   plus the migrating-from-vimwiki notes (two behaviour deltas the
   user might trip on).

Kept:
- The repository layout diagram (updated to reflect the renamed
  ftplugin/syntax files and the new `scripts/` test harness).
- The implementation-status tables, now under `Development` so
  contributors find them but new users aren't drowned in them.
- The dual MIT/Apache license footer.

No behaviour changes; documentation-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:20:18 +00:00
gffranco 63e5b6d514 phase 19: editor glue v2
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m18s
Server-side:
- New `folding.rs` module with `folding_ranges(ast, total_lines)`.
  Emits one fold per top-level heading (start → line before the
  next same-or-higher-level heading, or EOF) plus one per top-level
  list block. Nested headings fold inside their parent thanks to the
  level-aware end-line computation; sublists fold implicitly via
  their parent item's source span. `FoldingRangeKind::Region` is set
  on every fold so collapsing UIs render them as section folds.
- `Backend::folding_range` handler wires the module into
  `textDocument/foldingRange`; `ServerCapabilities.folding_range_provider`
  advertises it.
- `folding::line_count` exposed for the handler and tests; treats a
  trailing newline as a virtual empty line (the line LSP positions
  use for EOF).

Editor glue (Neovim primary, Vim minimal):
- `lua/nuwiki/commands.lua` — async `workspace/executeCommand`
  wrappers for every server command shipped through Phase 18. The
  open-* family handles `{ uri }` responses by opening the file
  (with `tab` / `split` variants). `check_links` and `find_orphans`
  hand results to `setqflist` + `:copen` for quickfix-style review.
  §13.1-deferred commands (`list.changeSymbol`, table rewriters,
  `link.pasteWikilink/pasteUrl`, `colorize`) stub out with
  `vim.notify` so users get a clear "not yet implemented" signal
  instead of an LSP "unknown command" error.
- `lua/nuwiki/keymaps.lua` — buffer-local default mappings. Subgroups
  (`list_editing`, `header_nav`, `diary`, `html_export`,
  `text_objects`) flip independently via the new
  `mappings.<group>` config. Heading promote/demote uses `g=`/`g-`
  to avoid clobbering Vim's built-in `=`/`-` operators.
- `lua/nuwiki/textobjects.lua` — `ah`/`ih` (around/inside heading)
  using a buffer scan for the heading-block boundary. The four
  remaining text objects from SPEC §12.10 wait until §13.1 lands
  the table/list rewriters they share infrastructure with.
- `lua/nuwiki/folding.lua` — pure regex `foldexpr` + `foldtext`
  fallback for clients without `foldingRange`. Same heading-block
  model as the server.
- `lua/nuwiki/ftplugin.lua` — single per-buffer attach entry point.
  `folding = 'lsp'` (default) uses Neovim 0.11+'s `vim.lsp.foldexpr`,
  falling back to the regex on older versions; `'expr'` forces the
  fallback; `'off'` skips folding setup.
- `lua/nuwiki/config.lua` — extends defaults with `mappings = {...}`
  (P10 keymap layer) and `folding` (P14 resolved).
- `ftplugin/nuwiki.vim` — declares every `:Vimwiki*` / `:Nuwiki*`
  command from SPEC §12.10 by inlining `lua require(...).fn()`
  bodies (no script-local function indirection so commands stay
  callable after re-source). Plain-Vim users get only the buffer
  options; they're expected to drive the LSP via vim-lsp / coc's
  built-in commands.

Health check (§12.10 additions):
- `:checkhealth nuwiki` now reports the count of `executeCommand`
  entries the server advertises, whether `foldingRange` capability
  was negotiated, and whether the configured HTML output directory
  exists + is writable. Default keymap subgroup status is also
  surfaced.

Tests: 8 new in `phase19_folding.rs` covering empty docs, single
heading → EOF, sibling headings each getting their own fold,
nested heading bounded by parent, top-level list folds, single-line
heading non-fold, fold-kind classification, and `line_count`
semantics. Total 377 tests pass; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:57:05 +00:00
gffranco 106268d488 phase 18: multi-wiki resolver + picker commands
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m14s
The Phase 11 plumbing already landed the data structures (`Wiki`
aggregate, per-wiki `WorkspaceIndex`, `wikis = [...]` config shape
with v1.0 desugaring). Phase 18 closes the user-visible gap: cross-
wiki link resolution + the picker command surface.

Cross-wiki resolution:
- `Backend::wikis_snapshot` — clone the Vec under the read lock
  so commands can drop it before awaiting.
- `Backend::wiki_by_index` / `wiki_by_name` — pair with the
  parser's `wiki<N>:` / `wn.<Name>:` interwiki shapes.
- `Backend::resolve_target_uri(target, source_uri)` — single entry
  point for cross-wiki page lookup:
    - `Wiki` / `AnchorOnly` → source wiki's index (Phase 8
      behaviour preserved).
    - `Interwiki` → routes to the wiki referenced by
      `wiki_index` / `wiki_name`, then resolves the page in that
      wiki's `pages_by_name`. Returns `None` when no wiki matches
      or the page isn't indexed.
- `Backend::heading_range_for(target_uri, anchor)` — anchor lookup
  on the target wiki's index. Phase 8's `goto_definition` was
  computing this against the *source* wiki's index, which broke for
  interwiki anchors; both `goto_definition` and `hover` are
  refactored onto the new pair.

Commands:
- `nuwiki.wiki.listAll` — returns `[{ id, name, root, syntax,
  file_extension }]`. Drives the picker UI.
- `nuwiki.wiki.select` — alias of `listAll` per SPEC §12.9
  (selection is client-side; the server just exposes the list).
- `nuwiki.wiki.openIndex` — args `{ wiki? }` accepting an index id
  (number), name (string), or numeric string. Returns
  `{ uri, name, tab: false }` for `<root>/index<file_extension>`.
- `nuwiki.wiki.tabOpenIndex` — same payload but `tab: true` so
  the client knows to open in a new tab/split.
- `nuwiki.wiki.gotoPage` — args `{ page, wiki? }`. Prefers an
  indexed URI when the page is already known; otherwise builds
  `<root>/<page><file_extension>` directly. Drives the
  `:VimwikiGoto {name}` compat command.

Tests: 12 new in `phase18_multi_wiki.rs` covering parser shapes for
`wiki<N>:` / `wn.<Name>:` (and that plain `[[Home]]` stays
`LinkKind::Wiki`), multi-wiki config loading + v1.0 single-root
desugaring, `WikiId` sequential assignment, longest-prefix URI
routing for nested roots, the cross-wiki resolution semantics
(target wiki's index wins when both wikis define the same page
name), missing-wiki fallback, the COMMANDS list completeness, and a
link-kind matrix covering all nine kinds. Total 369 tests pass;
fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:46:33 +00:00
gffranco d1b807b7d1 phase 17: html export commands + extended template vars
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m26s
CI / cargo test (push) Successful in 1m16s
HtmlRenderer (nuwiki-core):
- `with_var(k, v)` / `with_vars(map)` add arbitrary `{{key}}`
  substitution alongside the existing `{{title}}` / `{{content}}`.
  Substitution order: content → title → user vars, so a rendered body
  that happens to contain a literal `{{title}}` isn't double-processed.

WikiConfig (nuwiki-lsp/config):
- New `html: HtmlConfig` field with per-wiki export options matching
  vimwiki's `g:vimwiki_*` keys: `html_path`, `template_path`,
  `template_default`, `template_ext`, `template_date_format`,
  `css_name`, `auto_export`, `html_filename_parameterization`,
  `exclude_files`. Defaults: `<root>/_html` and `<root>/_templates`.
- `RawWiki` accepts all of the above through `initializationOptions`
  (and `workspace/didChangeConfiguration` reload).

New `export` module (pure helpers):
- `output_path_for` — page name → on-disk HTML path. Slugifies only
  the final segment when `html_filename_parameterization = true` so
  the `diary/` subdir survives.
- `template_for` — primary/fallback path lookup tuple.
- `fallback_template` — minimal in-memory page used when neither
  template file exists.
- `format_date` — strftime subset (`%Y`/`%m`/`%d`/`%%`) so we don't
  depend on `chrono`/`time`.
- `build_toc_html` — nested `<ul class="toc">` from the doc's
  headings, with HTML escaping.
- `compute_root_path` — `../` prefix per directory depth for
  stylesheet hrefs in nested pages.
- `is_excluded` — glob matcher supporting `*`, `**`, `?`. Used by
  the `allToHtml*` walker against `exclude_files`.
- `render_page_html` — wires the above into HtmlRenderer with
  `{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}` populated.
- `DEFAULT_CSS` — minimal stylesheet bundled for first-time exports.

New commands:
- `nuwiki.export.currentToHtml` — render the current buffer; honours
  `%nohtml` and `%template`; ensures CSS exists.
- `nuwiki.export.allToHtml` — walk the wiki root, skip pages matched
  by `exclude_files` and `%nohtml`, and only re-export pages whose
  source is newer than the existing HTML (incremental behaviour
  matching `:VimwikiAll2HTML`).
- `nuwiki.export.allToHtmlForce` — same but unconditional re-export.
- `nuwiki.export.browse` — alias of currentToHtml that returns the
  `file://` URL of the rendered page in the response so the client
  can open it.
- `nuwiki.export.rss` — write `<html_path>/rss.xml` listing the
  wiki's diary entries newest-first.
- `did_save` handler — runs the auto-export equivalent when the
  resolved wiki has `auto_export = true`.

LSP capability:
- `text_document_sync` switched from `Kind(FULL)` to `Options(...)`
  so the server can register a `save` notification (otherwise
  clients never push `did_save` events).

Tests: 33 new in `phase17_html_export.rs` covering HtmlConfig
defaults + JSON-shape config, the `format_date` subset (including
the `%%` escape and pass-through for unsupported specifiers),
root-path computation, output-path slugification rules, the glob
matcher (single-star, globstar, question-mark, non-match),
template_for primary/fallback selection, render_page_html for the
title/content/date/root_path triad and the override-by-extras flow,
and side-effecting disk paths (write_page creates parents, picks up
on-disk template, RSS is newest-first, ensure_css is idempotent).
Total 357 tests pass; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:37:36 +00:00
gffranco e75ad6ca89 phase 16: diary path conventions + nav commands
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m20s
Hand-rolled date primitive (no chrono/time dep) plus the diary
command surface that closes the `:VimwikiMakeDiaryNote` ergonomics gap.

nuwiki-core/src/date.rs:
- `DiaryDate { year, month, day }` with strict `YYYY-MM-DD` parsing
  (rejects missing zero-padding, alternate separators, whitespace,
  impossible calendar dates including the leap-year exceptions).
- `next_day` / `prev_day` via Howard Hinnant's days-from-civil
  algorithm — proleptic Gregorian, walks across month/year/leap-day
  boundaries.
- `today_utc()` from `SystemTime::now()`. UTC-only by design: a
  local-tz implementation would need a tz database we don't ship.
- `Ord` by epoch days, `Display`/`format` round-trip with `parse`.

WikiConfig (P15 resolution):
- `diary_rel_path: String` (default `"diary"`) — mirrors vimwiki's
  `g:vimwiki_diary_rel_path`.
- `diary_index: String` (default `"diary"`) — the stem inside the
  diary subdir.
- `diary_dir()`, `diary_index_path()`, `diary_path_for(date)` —
  centralise the path math so commands stay declarative.
- `RawWiki` (init-options wire format) accepts both fields, so users
  with `g:vimwiki_diary_rel_path = "journal"` migrate cleanly.

WorkspaceIndex:
- New `diary_rel_path: Option<String>` field set by `Wiki::new`.
- `IndexedPage.diary_date: Option<DiaryDate>` — populated in `upsert`
  when the URI is under `<root>/<diary_rel_path>/` and the stem
  parses. `diary_date_for_uri` is the standalone classifier.
- The classifier *requires* the URI to be inside the diary subdir
  when a root is known; with no root it accepts any file whose stem
  is a date (kept for the ad-hoc test/scratch case).

nuwiki-lsp/src/diary.rs:
- `uri_for_date` / `index_uri` — bridge from `DiaryDate` and config
  to `file://` URIs.
- `list_entries(index) → Vec<DiaryEntry>` — ascending, plus
  `list_entries_filtered(year, month)` for calendar hooks.
- `next_entry` / `prev_entry` — strict less-than/greater-than
  comparison so navigating from a non-diary page (uses today as
  pivot) still moves to the right entry.
- `build_index_body` — newest-first, grouped under `== YYYY ==` /
  `=== Month ===` subheadings, emits `- [[diary/YYYY-MM-DD]]`. Matches
  `:VimwikiDiaryGenerateLinks`.
- `DiaryEntry` JSON shape: `{ date: "YYYY-MM-DD", uri }`. Manual
  serde impl so DiaryDate stays serde-free in nuwiki-core.

Commands (9 new):
- `nuwiki.diary.openToday` / `openYesterday` / `openTomorrow` →
  `{ uri, date }`.
- `nuwiki.diary.openIndex` → `{ uri }`.
- `nuwiki.diary.generateIndex` → WorkspaceEdit. Three paths:
  live document in `backend.documents` → full-doc replace; on-disk
  index → read + full-doc replace; missing → CreateFile + insert.
- `nuwiki.diary.next` / `prev` → pivots on the current page's
  `diary_date`, falls back to today.
- `nuwiki.diary.listEntries` → optional `year` / `month` filter.
- `nuwiki.diary.openForDate` → strict `YYYY-MM-DD` arg.

All commands accept an optional `uri` to scope to a particular wiki
when multi-wiki lands; the dispatcher falls back to `default_wiki()`.

Tests: 35 new in `phase16_diary.rs` (date parser exhaustively,
calendar arithmetic across boundaries + leaps, path conventions,
upsert classification including the "date filename outside diary dir"
no-op case, list/filter/nav, index body grouping, serde shape, command
list completeness). Total 309 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:04:55 +00:00
gffranco ce3ac8c4f8 phase 15: link health + TOC/links/orphans
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m29s
CI / cargo test (push) Successful in 1m40s
New diagnostics module hosts the `nuwiki.link` source: walks every
`WikiLinkNode` and emits a diagnostic per broken target. Severity is
gated by `cfg.link_severity` (off | hint | warn | error). Wired into
`collect_diagnostics` so didOpen/didChange publish link diagnostics
alongside parse errors.

Classification (`BrokenLinkKind`):
- `Wiki` target missing from the workspace index → MissingPage.
- `Wiki`/`AnchorOnly` target with an anchor that matches neither a
  heading nor a `:tag:` on the resolved page → MissingAnchor.
- `file:` / `local:` target whose resolved path isn't on disk →
  MissingFile. Absolute paths are used as-is; relative paths resolve
  against the source URI's parent directory, falling back to the wiki
  root.
- Interwiki/Diary/Raw/external — never diagnosed.

`classify_link` works off a `&WikiLinkNode`; `classify_outgoing` does
the same job from a cached `IndexedPage.outgoing` entry so the
workspace-wide checker doesn't need to re-parse every page.

New commands:
- `nuwiki.toc.generate` — generate `= Contents =` heading + nested
  list of `[[#anchor|Title]]` entries from current headings. Replaces
  any existing h1 "Contents" + its immediate following list
  case-insensitively; inserts at line 0 otherwise. Skips the TOC's
  own heading so re-generation is idempotent.
- `nuwiki.links.generate` — equivalent of `:VimwikiGenerateLinks`.
  Flat alphabetical list under `= Generated Links =`, excludes the
  current page.
- `nuwiki.workspace.checkLinks` — returns `Vec<BrokenLinkEntry>` with
  `{ uri, range, kind, message }` per broken link across the wiki.
  Open documents use live text for range conversion; closed pages
  fall back to the stored span coords. Accepts an optional `uri` to
  pick a specific wiki; defaults to the first registered one.
- `nuwiki.workspace.findOrphans` — returns `Vec<{ uri, name }>` for
  every indexed page with no incoming links. Sorted alphabetically.

Pure ops (`commands::ops`):
- `build_toc_text(items, heading_name)` — formats the TOC body with
  2-space indent per level, anchors via `index::slugify`.
- `build_links_text(pages, heading_name, exclude)` — formats the flat
  list.
- `find_section_range(ast, heading_name)` — locates an existing h1
  section (heading + immediate following list) by case-insensitive
  title match.
- `toc_edit` / `links_edit` — full edit producers.
- `collect_workspace_broken_links` / `find_orphans`.

Plumbing:
- `collect_diagnostics` gains a `uri: Option<&Url>` parameter so
  link-health can resolve `file:` / `local:` relatives. `ast_diagnostics`
  back-compat wrapper preserved.
- `Backend::default_wiki` is no longer `#[allow(dead_code)]`.

Tests: 31 new in `phase15_link_health.rs` covering severity mapping,
classification on every kind/anchor case, TOC nesting, links exclusion,
section-range case-insensitivity, idempotent re-generation, orphan
detection, and the COMMANDS list. The Phase 11 stub test was rewritten
into a real "no index = no diagnostics" assertion. All 274 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:49:32 +00:00
gffranco f1d046fad1 phase 14: list/heading edit commands + CommandOutcome split
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m11s
CI / cargo test (push) Successful in 1m11s
Six new executeCommand entries — the cheap surgical edits that account
for the bulk of vimwiki users' daily keymap traffic. Heavier commands
(table.align/moveColumn/insert, list.changeSymbol/changeLevel/renumber/
removeDone, link.normalize, colorize) are deferred for a follow-up
since each needs its own structural rewriter.

CommandOutcome split:
- `Edit(WorkspaceEdit)` — server applies via apply_edit.
- `Value(Value)` — returned verbatim to the client (used by
  read-only queries like nextTask).
- execute_command handler in lib.rs dispatches both cases.

New commands:
- nuwiki.list.toggleCheckbox — [ ] ↔ [X]; mid-states snap to [X];
  rejected reverts to [ ].
- nuwiki.list.cycleCheckbox — [ ] → [.] → [o] → [O] → [X] → [ ];
  rejected resets to [ ].
- nuwiki.list.rejectCheckbox — toggle the [-] rejected marker.
- nuwiki.list.nextTask — returns Location of the first unfinished
  task after the cursor; wraps to the start of the doc when none
  found below. Only items with checkboxes count.
- nuwiki.heading.addLevel — = → ==; clamps at 6.
- nuwiki.heading.removeLevel — == → =; clamps at 1 (no-op on h1).

Pure ops:
- ops::toggle_state / cycle_state / reject_state — checkbox state
  transitions.
- ops::checkbox_edit / next_task / heading_change_level —
  end-to-end edit producers. Tests drive these directly without
  spinning up a Backend.
- ops::rewrite_heading_with_level — text-level rewrite preserving
  the leading whitespace that marks a centered heading (lives
  outside the heading span, so the edit doesn't touch it).
- ops::find_list_item_at / find_checkbox_span / item_in_list —
  AST + source-text helpers, pub for downstream tooling.

Tests (21 new):
- State transitions: round-trip, progression, dash toggle.
- checkbox_edit: empty → done, partial → cycle, reject path,
  no-op on plain item, no-op off list, nested-sublist resolution.
- heading_change_level: h2→h3, level-6 cap, h3→h2, h1 no-op,
  off-heading no-op, span boundary semantics.
- next_task: first-unfinished, wrap-around, all-done returns
  None, plain items ignored.
- COMMANDS list ↔ handler match.

All 211 prior tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:41:11 +00:00
gffranco cb4bfae5e7 phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m15s
CI / cargo test (push) Successful in 1m15s
Server-side rename with cross-document link rewriting (P11's
read_or_open delivers on its promise) and the first executeCommand
entry. Plus the routing infrastructure that Phases 14-17 will hang
their own commands off of.

commands.rs:
- commands::execute(backend, name, args) dispatches by command name
  and returns the WorkspaceEdit to apply.
- COMMANDS const slice is the source of truth for what's advertised
  in ExecuteCommandOptions.commands.
- First entry: nuwiki.file.delete with args { uri: <Url> } returning
  a WorkspaceEdit with a DeleteFile op.

rename.rs:
- compute_rename(backend, target_uri, new_name, utf8):
  1. Resolve the wiki via wiki_for_uri.
  2. Build new URI from wiki.root + new_name + extension.
  3. Snapshot the index's backlinks for the old page name before
     releasing the read lock so we don't hold it across awaits.
  4. For each backlink, read_or_open the source (live state or
     disk + reparse) and validate the cached span against current
     text.
  5. Rewrite the [[...]] segment with rewrite_wikilink_target,
     preserving anchor and description.
  6. Add the RenameFile op so the builder emits it before the text
     edits in documentChanges ordering.
- build_new_uri handles subdirectory paths and avoids double-
  appending the file extension.
- rewrite_wikilink_target is pub so other tooling can call it.

lib.rs:
- Capabilities: rename_provider + execute_command_provider listing
  commands::COMMANDS.
- New rename handler: cursor-on-wikilink renames the linked page,
  else the buffer's file. Delegates to rename::compute_rename.
- New execute_command handler dispatches via commands::execute, then
  pushes the resulting WorkspaceEdit through client.apply_edit. On
  unknown command or bad args, logs and returns null.
- read_or_open loses its allow(dead_code) attribute now that Phase
  13 calls it.
- rename_target_uri helper shares index-resolution with goto_def.

Tests (11 new in phase13_rename_commands.rs):
- rewrite_wikilink_target: plain, with description, with anchor,
  with both, subdirectory path, non-wikilink-text rejection.
- build_new_uri: under root, with subdirectory, extension dedup,
  empty path segments skipped.
- nuwiki.file.delete builder smoke test (DocumentChanges + DeleteFile).

All 211 v1.0+Phase 11+12 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:27:39 +00:00
gffranco 8b29d4e9b9 phase 12: tags — lexer, AST, index, anchor resolution, ws/symbol
CI / cargo fmt --check (push) Successful in 18s
CI / cargo clippy (push) Successful in 1m3s
CI / cargo test (push) Successful in 1m11s
Vimwiki `:tag1:tag2:` syntax across the full stack. P11 resolved as
strict-vimwiki — no markdown `#tag` alias.

AST (nuwiki-core):
- `TagScope { File, Heading(usize), Standalone }` mirrors vimwiki's
  three placement-determined meanings: lines 0-1 → File; one or two
  lines below the most recent heading → Heading(idx); otherwise
  Standalone.
- `TagNode { span, tags: Vec<String>, scope }` + `BlockNode::Tag`.
- `PageMetadata.tags: Vec<String>` accumulates the file-scope tag
  names — convenience projection of the Tag blocks whose scope is
  File. New field is additive (Default derive) so existing consumers
  using `..Default::default()` keep working.
- `Visitor::visit_tag` lands with a default body and `walk_block`
  dispatches the new arm.

Lexer (`syntax/vimwiki/lexer.rs`):
- `VimwikiTokenKind::Tag(Vec<String>)` block-level token.
- `try_lex_tag` runs in the normal-line dispatcher right after heading
  recognition; demands the *whole trimmed line* match `:name:name…:`,
  with non-empty whitespace-free names between adjacent colons. Empty
  segments (`::tag::`), whitespace inside a name, or single-colon
  lines are all rejected. Doesn't fight `Term::` (definition-list
  marker) because that pattern requires text before the `::`.
- Indentation is tolerated; the span covers the trimmed run.

Parser (`syntax/vimwiki/parser.rs`):
- `ParseState` gains `headings_seen` + `last_heading_line`.
- `parse_heading` bumps both on every closed heading so the tag
  parser can detect the heading window.
- `parse_tag` reads the Tag token, computes the scope via
  `scope_for_tag(tag_line)`, eats the trailing newline, and emits the
  TagNode.
- `parse_document` post-processes each block: when a Tag with
  `scope == File` is emitted, its names are pushed (de-duped) into
  `metadata.tags`.

Renderer (`render/html.rs`):
- `render_tag` emits `<div class="tags"><span class="tag"
  id="tag-name">name</span>...</div>`. The `id` is what makes
  `[[Page#tag-name]]` jump to the rendered tag.

Semantic tokens (`semantic_tokens.rs`):
- New `TOKEN_TAG = 19` / `vimwikiTag` so editors highlight tag lines
  distinctly. `emit_block` adds the Tag arm.

Workspace index (`index.rs`):
- `IndexedPage.tags: Vec<TagInfo>` records each tag occurrence on a
  page with its span + scope.
- `WorkspaceIndex.tags_by_name: HashMap<String, Vec<TagOccurrence>>`
  is the reverse map for cross-page anchor resolution + the Phase 13
  `nuwiki.tags.search` command.
- `upsert` populates both; `remove` scrubs the reverse map and drops
  empty entries; `tags_for(name)` is the public lookup.

LSP handlers (`lib.rs`):
- `heading_range_in` falls back to tag matching when the anchor
  isn't a heading slug — so `[[Page#some-tag]]` lands on the matching
  TagNode. Heading anchors win over tag anchors when both match (the
  vimwiki precedence).
- `workspace/symbol` includes tags alongside headings, named `:tag:`
  and kinded `PROPERTY` so editor UIs can distinguish them.

Tests (20 new):
- Syntax (15 in `vimwiki_tags.rs`):
  lexer recognition + rejection of `::tag::`, whitespace-in-name,
  Term::-conflict; parser scope paths (file, heading-1-below,
  heading-2-below, standalone, no-heading, line-1 priority);
  metadata dedup; renderer HTML + escaping.
- LSP index (5 in `tags_index_and_lsp.rs`):
  per-page tag population, cross-page `tags_by_name` aggregation,
  re-upsert replaces stale tags, `remove` cleans the reverse map,
  heading-scope tag indexed but not in file metadata.

All 191 v1.0 + Phase 11 tests still pass — backward compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:54:55 +00:00
gffranco ea574f23f8 phase 11: plumbing prereqs — Wiki, edits, config, snapshot, diags
CI / cargo fmt --check (push) Successful in 25s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m15s
Cross-cutting infrastructure that every v1.1 phase depends on. Lands as
a numbered phase before any user-visible feature so Phases 12–19 stay
mechanical.

5 pieces:

- `config.rs` — `Config` + `WikiConfig` + `DiagnosticConfig` + `LinkSeverity`.
  Serde-deserialised from `initialization_options`. v1.0 single-wiki
  shape (`wiki_root` + `file_extension` + `syntax`) desugars to one-entry
  `wikis = [...]` per P16. `~/...` expansion without pulling in `dirs`.
  Fallbacks: workspace_folders[0] → deprecated root_uri → empty list.

- `wiki.rs` — `Wiki` aggregate (id + WikiConfig + Arc<RwLock<Index>>).
  `build_wikis(&[WikiConfig])` materialises the list; `resolve_uri_to_wiki`
  picks the longest-prefix root match so nested wikis route correctly.
  Single-entry today, multi-entry once Phase 18 lands.

- `edits.rs` — `WorkspaceEditBuilder` + `text_edit_replace/insert/delete`
  + `op_rename/delete/create`. Handles the UTF-8/UTF-16 conversion at the
  span boundary so each Phase 13+ command stays a 3-liner. Builder picks
  `documentChanges` when file ops are present (LSP requires it), `changes`
  map otherwise (works for LSP 3.15- clients). File ops precede content
  edits in the output so created/renamed files exist when edits apply.
  `DeleteFile` is built without `annotation_id` — lsp-types 0.94.x ships
  it that way; later versions added the field.

- `Backend` refactor — `index: Arc<RwLock<WorkspaceIndex>>` replaced by
  `wikis: Arc<RwLock<Vec<Wiki>>>` + `config: Arc<RwLock<Config>>`. New
  `wiki(id)` / `default_wiki()` / `wiki_for_uri(uri)` helpers. Every v1.0
  handler (definition, references, hover, completion, workspace/symbol,
  update_document) goes through `wiki_for_uri`. `initialize` builds wikis
  from `Config::from_init_params`; `initialized` spawns one indexer task
  per wiki with the existing `window/workDoneProgress` wrapper. New
  `did_change_configuration` handler re-applies settings (rebuild
  semantics revisited in Phase 18). The old `workspace_root_from_params`
  helper is gone.

- `read_or_open` + `DocSnapshot` (scaffolded, `#[allow(dead_code)]` until
  Phase 13's `workspace/rename` consumes it). Returns the live document
  when held in the documents map, otherwise async-reads from disk and
  re-parses so cross-document operations get accurate text and spans
  instead of trusting stale `WorkspaceIndex` data.

- `collect_diagnostics` composable collector. Same `ErrorNode` walk
  today; takes `Option<&WorkspaceIndex>` + `page_name` + `&DiagnosticConfig`
  so Phase 15 can drop a link-health source in without further refactor.
  `ast_diagnostics` kept as a thin wrapper for the v1.0 test surface.

New deps: `serde` + `serde_json` (already transitive through tower-lsp;
elevated to direct deps for clarity and stability). `tokio` gains the
`fs` feature for `read_or_open`'s async disk read.

Tests (19 new in `phase11_plumbing.rs`):
- Edits: UTF-8 passthrough + UTF-16 conversion (`héllo`), insert/delete
  shape, builder mode selection, file-op accumulation, `op_create` round-trip
- Config: defaults, v1.0 desugar, explicit list overrides legacy, empty
  JSON, invalid JSON preserves prior state
- Wiki: sequential id assignment, longest-prefix resolution, no-match,
  `Wiki::contains`
- Diagnostics: error-node composition, link-health stub returns empty in
  Phase 11

All 172 v1.0 tests still pass — backward compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:32:22 +00:00
gffranco 02291c2744 phase 10: release pipeline (Gitea release on v* tag)
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m13s
P9 resolved (SPEC §11): stay manual. CI builds the four Linux targets
via cross; macOS + Windows binaries are produced ad hoc by maintainers
and uploaded to the same Gitea release.

§8.2/§8.4 updated: crates.io publish is deferred for now (workflow
ships the Gitea release only; can be re-enabled with a one-line
`cargo publish -p nuwiki-core` job once we're ready to publish). The
CARGO_REGISTRY_TOKEN row in §8.4 is struck through accordingly.

release.yaml:

- Trigger: `push` on tags matching `v*`.
- build matrix (4 jobs): x86_64/aarch64 × gnu/musl. Each pins
  Rust 1.83, caches `~/.cargo/{bin,registry,git}` + `target` keyed
  on target + Cargo.lock hash, installs `cross 0.2.5` if not in
  cache, runs `cross build --release -p nuwiki-ls`, packages as
  `nuwiki-ls-{version}-{target}.tar.gz`, uploads as an artifact.
- release job: `needs: build`. Downloads all build artifacts via
  download-artifact@v4 with `merge-multiple: true`; installs jq +
  curl if missing; POSTs a release-create payload to
  `/api/v1/repos/$REPO/releases` using `RELEASE_TOKEN`, then streams
  each `.tar.gz` to `/releases/{id}/assets?name=…`. Up-front check
  fails fast if the secret is unset.

README + SPEC top-line status: "All phases (0–10) complete." 172
tests still green locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:47:40 +00:00
gffranco ee61d40872 phase 9: editor glue — plugin, LSP wiring, install, health, docs
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m23s
CI / cargo test (push) Successful in 1m29s
P5 + P6 resolved (SPEC §11): Neovim 0.11+ and Vim 9.1+.

Universal Vim entry:
- plugin/nuwiki.vim dispatches Vim vs Neovim. Neovim path waits for
  the user's `require('nuwiki').setup()` call; Vim path starts the
  LSP client on `FileType vimwiki` and exposes `:NuwikiInstall`.
- ftdetect/nuwiki.vim associates `*.wiki` with the `vimwiki` filetype.
- ftplugin/nuwiki.vim sets commentstring + comments + formatoptions +
  suffixesadd + iskeyword, with a clean `b:undo_ftplugin`.
- syntax/nuwiki.vim ships default `@vimwiki*` highlight links for the
  semantic-token legend (with `level1..6` + `centered` modifiers),
  plus a minimal regex fallback so static highlighting works before
  the LSP attaches.

Lua glue (Neovim 0.11+):
- init.lua exposes setup() / install() / health() — wiring only per
  SPEC §6.2.
- config.lua holds the §7.5 schema (wiki_root / file_extension /
  syntax / log_level) and a tbl_deep_extend apply().
- lsp.lua registers via `vim.lsp.config{} + vim.lsp.enable` on 0.11+;
  falls back to a FileType autocmd calling `vim.lsp.start` on older
  builds. Root dir walks up for `.git` / `.nuwiki`, then uses
  wiki_root.
- install.lua does target-triple detection (Linux gnu/musl x x86_64/
  aarch64, macOS arm/x86, Windows), curl+tar download from the
  release URL, cp/chmod into bin/, with a cargo fallback. Honours
  `g:nuwiki_build_from_source`.
- health.lua implements `:checkhealth nuwiki` per §7.6.

VimL glue (Vim 9.1+):
- autoload/nuwiki/lsp.vim follows §7.4 preference order:
  vim-lsp (calls `lsp#register_server`) → coc.nvim (prints the
  coc-settings.json snippet) → error.
- scripts/download_bin.vim mirrors the Lua installer for Dein /
  vim-plug build hooks — same target triples, same download →
  fallback → cargo build chain.

Help: doc/nuwiki.txt with tags for install (lazy.nvim / vim-plug /
Dein), config options, health, LSP feature list, `:NuwikiInstall`.

Rust workspace unchanged this phase; 172 tests still green locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:38:09 +00:00
gffranco 4d461d2425 phase 8: navigation — definition, refs, hover, completion, ws/symbol
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m6s
CI / cargo test (push) Successful in 1m17s
P8 resolved (SPEC §11): lazy + background indexing with progress.
Initial scan runs in a tokio task spawned from `initialized`; the
server stays responsive immediately and nav features answer with
partial data until the index is complete.

WorkspaceIndex (`index.rs`):
- Per-URI `IndexedPage` keeps name, title, headings, outgoing links.
- Reverse maps: `pages_by_name`, `backlinks: HashMap<page, …>`.
- `upsert` scrubs stale entries first so re-parses don't leak.
- `resolve` handles Wiki + AnchorOnly link kinds against the index;
  other kinds (Interwiki/Diary/File/Local/Raw) fall through.
- `slugify` for anchors (lowercases, collapses runs, drops trailing
  dashes, preserves Unicode), `page_name_from_uri` for workspace-
  rooted names, `walk_wiki_files` for the background scanner.

Backend wiring (`lib.rs`):
- `initialize` captures workspace root from `workspaceFolders` or the
  deprecated `root_uri`; advertises definition/references/hover/
  completion (trigger `[`)/workspace_symbol providers.
- `initialized` spawns the indexer, wrapped in
  `window/workDoneProgress` begin/end notifications.
- `did_open`/`did_change` synchronously update the index alongside
  publishing diagnostics; `did_close` keeps the indexed projection
  (file may still exist on disk and other pages link to it) and
  only clears diagnostics + drops the live document state.

Position lookup (`nav.rs`):
- `lsp_to_byte_pos` translates between LSP encoding (UTF-8 or
  UTF-16) and our byte columns.
- `find_inline_at` descends blocks → inlines → inline containers,
  returning the most specific inline whose span covers the cursor.

Handlers:
- definition: wikilinks resolve to a target URI; anchored links
  return the range at the matching heading.
- references: backlinks of the cursor's link target, or of the
  current page when the cursor isn't on a link. Ranges fall back
  to byte-column LSP positions when the source doc isn't open.
- hover: markdown preview with page title + outline (first 8
  headings), or a "not in index" fallback.
- completion: fires only inside an unterminated `[[ … ` on the
  current line; suggests every indexed page name.
- workspace/symbol: case-insensitive substring over every indexed
  heading.

Tests (20 new): slugify (basic / punctuation / Unicode / trailing
dash), page-name derivation under various roots, upsert / replace /
remove, resolve for Wiki + AnchorOnly, sorted `page_names`, UTF-8
passthrough + UTF-16 conversion, position lookup hits and misses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:27:06 +00:00
gffranco 6efe154f1a phase 7: semantic tokens for syntax highlighting
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m20s
`textDocument/semanticTokens/full` + `/range` per SPEC §6.9. Token
type scheme settled in P7: custom `vimwiki*` types
(`vimwikiHeading`, `vimwikiBold`, `vimwikiCodeBlock`, `vimwikiKeyword`,
`vimwikiWikilink`, `vimwikiTransclusion`, …) plus `level1`..`level6`
and `centered` modifiers. Default highlight groups for these arrive
in Phase 9.

Pipeline (`semantic_tokens.rs`):

- `legend()` builds the `SemanticTokensLegend` advertised in
  `initialize` capabilities.
- `LineIndex` precomputes byte offsets per line so multi-line span
  splitting and UTF-16 conversion stay linear.
- `collect_tokens` walks the AST. Blocks that are visually distinct
  (heading, preformatted, math-block, comment) emit one token over
  their span; multi-line ones get split per line because the LSP wire
  format can't represent a token that crosses a newline. Container
  blocks (paragraph, list item, table cell, blockquote) recurse into
  inlines instead of wrapping them, so we end up with a non-overlapping
  tile of inline tokens. Headings shadow nested inlines (one heading
  token covers the line, no Bold-inside-Heading).
- `encode` produces the LSP delta-quintuple stream
  `(deltaLine, deltaStart, length, type, mods)`. UTF-16 mode converts
  byte offsets/lengths to code-unit counts on the fly (BMP + surrogate
  pairs).
- `build_data` is the entry point used by both handlers; the `range`
  variant filters tokens by overlap in the same encoding the client
  requested.

Capability declared as `SemanticTokensOptions { full = true,
range = true, legend = … }`. Both handlers reuse the document store.

P7 resolved in SPEC §11 — custom vimwiki types with rationale and a
note that Phase 9 will ship default highlight-group definitions in
`syntax/nuwiki.vim` and the Lua glue.

Tests (21 new): legend integrity, per-construct emission for every
inline + block kind, heading shadowing of inner inlines, multi-line
code/math block splitting, sort order, delta encoding (same line and
crossing lines), UTF-16 length conversion for é, range filter,
empty-doc edge case, end-to-end `build_data` parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:14:01 +00:00
gffranco c44db37bab phase 6: LSP foundation — didOpen/didChange, diagnostics, outline
CI / cargo fmt --check (push) Successful in 48s
CI / cargo clippy (push) Successful in 1m19s
CI / cargo test (push) Successful in 1m19s
`tower-lsp` server (`nuwiki-lsp::run` + `run_stdio`) wired up by the
`nuwiki-ls` binary. Backend holds an `Arc<DashMap<Url, DocumentState>>`
per SPEC §6.10 plus an `Arc<SyntaxRegistry>` pre-loaded with
`VimwikiSyntax`. Re-parse is full per change; incremental parsing
deferred post-v1 per spec.

Capabilities declared:
- `text_document_sync = FULL`
- `document_symbol_provider = true`
- `position_encoding = UTF-8` when the client opts into LSP 3.17+
  encodings (SPEC §6.11). Otherwise we keep the legacy UTF-16 default
  and translate spans through a per-line UTF-16 code-unit walk that
  handles BMP and surrogate pairs alike.

Handlers: `initialize` (negotiate encoding), `initialized` (log it),
`shutdown`, `did_open` / `did_change` (full re-parse +
`publishDiagnostics`), `did_close` (drop state, clear diagnostics),
`document_symbol` (nested outline).

Diagnostics: every `BlockNode::Error` (from §6.7's resilient parser)
emits one `DiagnosticSeverity::ERROR` with `source = "nuwiki"`. The
walker descends into Blockquotes and ListItems so errors nested inside
those don't slip through.

Document outline: nested `DocumentSymbol` tree built recursively from
heading levels (`= H1 =` → root, `== H2 ==` → child of preceding H1,
etc.). Titles flatten inline markers via `inline_to_text` so
`*world*` shows up as `world` in the outline.

Pure helpers (`to_lsp_position`, `to_lsp_range`, `ast_diagnostics`,
`headings_to_symbols`) are `pub` so the integration tests can verify
conversion correctness without spinning up the async server. End-to-
end LSP message exercises will land in Phase 8 alongside navigation.

Tests (11): UTF-8 passthrough, UTF-16 conversion for é + 🦀, single
and multi-error diagnostic emission, blockquote descent, flat outline,
nested outline by levels, inline-marker stripping in titles, empty-
heading fallback, registry-dispatch parity.

Dep pin: `idna_adapter` held at 1.2.0 — 1.2.2 needs Cargo's
edition2024 feature (Rust 1.85), our MSRV is 1.83.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:44:44 +00:00
gffranco f00b0780b8 phase 5: renderer trait + HtmlRenderer
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 50s
CI / cargo test (push) Successful in 57s
Writer-based `Renderer` trait per SPEC §6.8: object-safe
`fn render(&self, doc, w: &mut dyn Write) -> io::Result<()>` plus
a `render_to_string` convenience. Object safety preserves the option
of `Box<dyn Renderer>` in the LSP layer.

`HtmlRenderer` builder accepts a link-resolution callback (via
`with_link_resolver`) and an optional enclosing template (via
`with_template`). The default resolver mirrors §9: Wiki paths become
`path.html`, interwiki links land in sibling directories
(`../wiki<N>/` / `../wn-<Name>/`), diary entries under `diary/`,
file/local schemes use literal paths, anchor-only collapses to `#`.

Renderer covers every AST node:

- block: heading (centered class), paragraph, hr, blockquote,
  preformatted (`language-<lang>` class), math block (with
  `\begin{env}` when an environment is set), lists (ordered/unordered
  + every checkbox state, plus recursive sublists), definition list
  (dt/dd), table (thead/tbody driven by `is_header`, col/row span as
  CSS class hooks), comment (as HTML comment with `--` neutralised),
  error node (as `.parse-error` span)
- inline: strong/em + bold-italic, `<del>`, code (escaped),
  `<sup>`/`<sub>`, math-inline wrapped in `\(...\)`, keyword spans
  with per-keyword class, color spans, wikilink/external/raw URL
  anchors, transclusion as `<img>` with sorted attrs

HTML escaping is byte-level (`<>&"'`). Template substitution replaces
`{{content}}` first so a body containing the literal `{{title}}`
isn't double-substituted.

Tests (31 new) cover every block + inline construct, every LinkKind
through the default resolver, HTML escaping, custom resolver
override, template with/without title metadata, and an end-to-end
smoke test on a multi-construct document.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:12:01 +00:00
gffranco 294963b517 phase 4: vimwiki parser + SyntaxPlugin glue
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 1m2s
CI / cargo test (push) Successful in 1m5s
Hand-rolled recursive-descent parser over the token stream. Resilient
per SPEC §6.7: never aborts the document — anything the dispatcher
can't claim becomes BlockNode::Error and the cursor advances, and
unterminated wikilinks / transclusions / delimiters fall back to
literal text.

Block coverage: every variant from §6.4 — Heading (with inline
content between markers), Paragraph (with soft-break across single
newlines), HorizontalRule, Blockquote (both `>` and 4-space styles),
Preformatted, MathBlock, List (with checkbox + indent-driven
sublists), DefinitionList, Table (header separator promotes the
preceding row, plus colspan / rowspan cells), Comment (single +
multiline), and page-level placeholders threaded into PageMetadata.

Inline pairing: left-to-right scan with "find next matching delim",
recursing on the slice between. `_*x*_` becomes Italic(Bold(...)),
mirroring SPEC §6.7 precedence. URLs inside `[[ ]]` route to
ExternalLinkNode; everything else to WikiLinkNode with full
LinkTarget classification (Wiki / Interwiki numbered + named /
Diary / File / Local / AnchorOnly / directory / root-relative /
filesystem-absolute).

VimwikiSyntax registers as id="vimwiki", extensions=[".wiki"] and
chains lexer + parser inside SyntaxPlugin::parse.

SPEC §4 updated to record the hand-rolled choice with rationale —
span-bearing tokens fit awkwardly into winnow/nom combinator style;
resilience and recovery are explicit in code instead.

Tests (41 new parser cases) cover every AST construct, inline
precedence including nested bold/italic, every LinkKind, transclusion
alt + attrs, resilience on unterminated links, and end-to-end
SyntaxRegistry dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:58:41 +00:00
gffranco 942dbe2aa8 phase 3: vimwiki lexer
CI / cargo fmt --check (push) Successful in 22s
CI / cargo clippy (push) Successful in 53s
CI / cargo test (push) Successful in 1m2s
Two-pass, hand-rolled lexer per SPEC.md §6.6. Block pass walks the
source line by line; recognised constructs emit structural tokens, and
text-bearing line content is fed to the inline pass. Multi-line fences
(`{{{ }}}`, `{{$ }}$`, `%%+ +%%`) flip a `BlockMode` so subsequent
lines are captured raw until the matching closer.

VimwikiToken covers the full §9 feature checklist:

- headings (1–6, centered), horizontal rule, blockquotes (`>` and
  4-space), all list-marker variants, checkboxes, definition `::`,
  tables (separators, header row, col/row span)
- preformatted fences (with language + key=val attrs), math blocks
  (with `%env%`), single- and multi-line comments, all four
  page placeholders (%title / %nohtml / %template / %date)
- inline: bold/italic/strike/super/sub delimiters, inline code, inline
  math, all six keywords (word-bounded), wikilinks (with description
  separator), transclusions (with attr separator), raw URLs across
  http(s)/ftp/mailto/file schemes — trailing sentence punctuation
  stripped from URL spans

Spans are byte-accurate per SPEC §6.5 (0-indexed line + byte column +
absolute byte offset). The lexer is permissive: every delimiter is
emitted, the parser will pair them and fall back to literal text on
mismatches.

Tests (41 cases) cover one example per construct in §9, plus
word-boundary keyword detection, list-vs-bold disambiguation,
indent-vs-list disambiguation, and a span-correctness check.

README now carries a per-phase status table; SPEC top-line status
advanced to Phase 3 complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:20:46 +00:00
gffranco cf336ee839 phase 0: scaffold workspace, plugin layout, and CI
CI / cargo fmt --check (push) Successful in 46s
CI / cargo clippy (push) Successful in 1m33s
CI / cargo test (push) Successful in 53s
Lay down the empty repo skeleton defined in SPEC.md §5 so subsequent
phases have somewhere to land:

- Cargo workspace (resolver v2, edition 2021, MSRV 1.83) with
  nuwiki-core, nuwiki-lsp, nuwiki-ls — dep direction matches §6.2.
- Vim/Neovim plugin directory layout (plugin, lua, autoload, ftdetect,
  ftplugin, syntax, doc, scripts) with header-only stubs.
- .gitea/workflows/ci.yaml running fmt, clippy -D warnings, and tests
  pinned to Rust 1.83.
- README, dual MIT/Apache-2.0 license texts, .gitignore.

Verified: cargo check / fmt --check / clippy / test all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:01:58 +00:00
gffranco cf751383e1 initial setup 2026-05-10 14:53:58 +00:00