079e7246ac020b6d675e54aead6e3557fdddc654
68 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
106268d488 |
phase 18: multi-wiki resolver + picker commands
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>
|
||
|
|
d1b807b7d1 |
phase 17: html export commands + extended template vars
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>
|
||
|
|
ad1b55a401 |
backfill: tag commands + file-operations capability
SPEC audit of Phases 12–13 surfaced two gaps. Closing them here so the
v1.1 command surface and capability advertisement match what §12.3 and
§12.4 promise.
Phase 12 — tag commands (§12.3):
- `nuwiki.tags.search` — case-insensitive substring filter over the
workspace tag index. Returns `[{ name, uri, range, scope, page }]`
where `scope` is one of `file` / `heading` / `standalone`.
- `nuwiki.tags.generateLinks` — generates/refreshes a tagged-pages
section. Two modes:
- `{ uri, tag }` → `= Tag: <name> =` + flat list of `[[Page]]`
entries for every page tagged with `<name>`.
- `{ uri }` (no tag) → `= Tags =` umbrella section with `== <tag>
==` subgroups, mirroring `:VimwikiGenerateTagLinks` (no arg).
Idempotent: re-runs replace the existing section by
case-insensitive heading match.
- `nuwiki.tags.rebuild` — synchronous full re-walk of the wiki root.
Drops index entries for pages whose files no longer exist on disk,
re-parses every `.wiki`, and returns `{ pages, files_seen,
stale_removed }`. Useful for cleaning up after out-of-band edits
the LSP didn't see.
Ops layer:
- `tag_hits(index, query) → Vec<TagHit>` — pure filter, sorted by
(name, page, line).
- `tag_pages_snapshot(index) → BTreeMap<tag, [pages]>` — taken under
the read lock so the dispatcher can release the lock before
building the edit.
- `build_tag_links_text` / `tag_links_edit` — text-level rendering
and the WorkspaceEdit producer.
Phase 13 — file-operations capability (§12.4):
- `ServerCapabilities.workspace.file_operations` now advertises
`did_rename` + `did_delete` with filters scoped to `**/*.wiki`
and `**/*.md`. (will_* deferred — those return a WorkspaceEdit
and we'd want to wire them into the rename pipeline before
promising support.)
- `did_rename_files` handler — moves the live DocumentState forward
to the new URI when the file was open, then refreshes the index
(remove old URI, upsert new via read_or_open).
- `did_delete_files` handler — drops both DocumentState and index
entry so backlink lookups don't surface dangling source URIs.
Tests: 15 new in `phase17_backfill.rs` covering tag hit filtering &
ordering, snapshot dedup, single-tag and full-index render shapes,
edit replace-vs-insert flow, JSON shape of `TagHit`, and the COMMANDS
list. Total 324 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e75ad6ca89 |
phase 16: diary path conventions + nav commands
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>
|
||
|
|
ce3ac8c4f8 |
phase 15: link health + TOC/links/orphans
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>
|
||
|
|
f1d046fad1 |
phase 14: list/heading edit commands + CommandOutcome split
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> |
||
|
|
cb4bfae5e7 |
phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
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>
|
||
|
|
8b29d4e9b9 |
phase 12: tags — lexer, AST, index, anchor resolution, ws/symbol
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>
|
||
|
|
ea574f23f8 |
phase 11: plumbing prereqs — Wiki, edits, config, snapshot, diags
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> |
||
|
|
4d461d2425 |
phase 8: navigation — definition, refs, hover, completion, ws/symbol
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> |
||
|
|
6efe154f1a |
phase 7: semantic tokens for syntax highlighting
`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>
|
||
|
|
c44db37bab |
phase 6: LSP foundation — didOpen/didChange, diagnostics, outline
`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> |
||
|
|
f00b0780b8 |
phase 5: renderer trait + HtmlRenderer
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>
|
||
|
|
294963b517 |
phase 4: vimwiki parser + SyntaxPlugin glue
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> |
||
|
|
942dbe2aa8 |
phase 3: vimwiki lexer
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>
|
||
|
|
8f35c0a80c |
phase 2: syntax plugin interface and registry
Add the Lexer/Parser/SyntaxPlugin traits and the SyntaxRegistry per SPEC.md §6.3. - `Lexer<Token>` and `Parser<Token>` are typed building blocks: each syntax owns its own token alphabet (vimwiki today, markdown later). - `TokenStream<T>` is a `Vec<T>` newtype so the eager-vs-lazy choice (§6.6) can flip later without breaking callers. - `SyntaxPlugin` is type-erased: it exposes id / display_name / file_extensions / parse(text) so the registry can hold heterogeneous plugins behind a single trait object. Implementations chain their Lexer + Parser inside parse(). - `SyntaxPlugin: Send + Sync` so the LSP server can stash plugins in an Arc and share them across handler tasks. - `SyntaxRegistry` stores `Arc<dyn SyntaxPlugin>`, supports lookup by id and by file extension, iteration in registration order. Tests cover TokenStream round-trip, full lex→parse chain through a mock plugin, registry lookup hits and misses, and a compile-time assert_send_sync::<SyntaxRegistry>(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dc2a952e67 |
phase 1: core AST, spans, and Visitor
Define every node type in SPEC.md §6.4 — Document, 11 block variants, 15 inline variants (including the 4 link-related ones), plus ListItem / DefinitionItem / TableRow / TableCell, ListSymbol, CheckboxState, Keyword, LinkTarget, LinkKind. Every node carries a `Span` (line + column + byte offset, 0-indexed) per §6.5. Visitor: open-recursion trait with default `visit_*` bodies that delegate to `walk_*` free functions. Override at any granularity; call `walk_*` from your override to keep descending. P4 resolved by defining the trait now so Phase 5 (renderer) and Phase 7 (semantic tokens) can plug in without refactoring traversal. P3 resolved: AST string fields are owned `String`. Cow / Arc<str> can be revisited if the parser grows a zero-copy mode. Tests: visitor smoke test builds a heading + paragraph + list + table by hand and asserts exact visit counts (4 blocks, 12 inlines, 9 text leaves, 1 bold, 1 external link, 1 transclusion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cf336ee839 |
phase 0: scaffold workspace, plugin layout, and CI
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> |