From cb244a8080917105a62f6a64bf98dd6c117e94a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Mon, 11 May 2026 14:16:00 +0000 Subject: [PATCH] spec: add Phase 11 plumbing prereqs; resolve P14; renumber 12+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture-review follow-through. The v1.1 plan as originally written started directly at Tags, which would have meant retrofitting five cross-cutting pieces into Phases 12–19 ad hoc. This commit lifts that infrastructure into its own numbered phase up front so the rest of v1.1 stays mechanical. Phase table (§10): inserts new "Phase 11 — Plumbing prerequisites"; Tags becomes 12, Workspace edits 13, …, Editor glue v2 19. v1.1 now has 9 phases instead of 8. §12.2 new "Plumbing prerequisites" subsection covers: - Server config receipt (initializationOptions + didChangeConfiguration) - WorkspaceEditBuilder + span helpers (text_edit_*, op_*) - Closed-doc loader (read_or_open, returns DocSnapshot) - Diagnostic source chain (collect_diagnostics with composable sources) - Wiki aggregate (lands single-entry, multi-entry config in Phase 18) Plus explicit "what does *not* change" and a tests-list. §11 decisions: - P14 (folding) resolved: LSP textDocument/foldingRange (primary) + foldexpr fallback in ftplugin. Server already has the AST + heading boundaries; duplicating that in VimL would be lossy. - P10/P11/P12/P13/P15/P16/P17 phase references bumped by one. - P18 added: server-config transport (initializationOptions + didChangeConfiguration, both). Targeted notes in the renumbered subsections: - §12.4 (Workspace edits): closed-doc rewrites use the Phase 11 loader and re-parse on demand — no trust in stale index spans. - §12.7 (Diary): roll-our-own YYYY-MM-DD parser; no chrono dep. - §12.9 (Multi-wiki): data structures already land in Phase 11, so Phase 18 is a config-shape change, not a refactor. Subsections 12.3 → 12.13 renumbered; phase labels in headers updated to match the new §10 table. Co-Authored-By: Claude Opus 4.7 (1M context) --- SPEC.md | 242 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 215 insertions(+), 27 deletions(-) diff --git a/SPEC.md b/SPEC.md index 753bc4f..c123137 100644 --- a/SPEC.md +++ b/SPEC.md @@ -592,14 +592,15 @@ A semver-breaking change (`v0.x` → `v0.x+1` or `v1.x` → `v2.0`) is defined a | 9 | Editor Glue | Installable plugin, health check, binary download | | 10 | CI/CD | Automated lint/test/release pipeline | | **— v1.1: full vimwiki replacement (see §12) —** | | | -| 11 | Tags | `:tag:` lex + parse, `TagNode`, index, tag-as-anchor resolution | -| 12 | Workspace edits + executeCommand | `workspace/rename` with cross-doc link rewrite, `DeleteFile` ops, `executeCommand` infrastructure | -| 13 | List & table edit commands | Server commands returning `WorkspaceEdit` for checkbox toggle, list-symbol/level change, renumber, table align, column move, table insert | -| 14 | Link health + link/TOC generation | Broken-link diagnostics, `nuwiki.toc.generate`, `nuwiki.links.generate`, `nuwiki.workspace.checkLinks` | -| 15 | Diary | Diary subpath, today/yesterday/tomorrow open commands, diary index generation, calendar hook API | -| 16 | HTML export commands | `nuwiki.export.*` commands, `%template` resolution, CSS file management, auto-export option | -| 17 | Multi-wiki | `wikis = [...]` config, per-wiki `WorkspaceIndex`, URI → wiki resolution, wiki-picker commands | -| 18 | Editor glue v2 | Full `:Vimwiki*` command compat layer, default keymaps, text objects (`ah`/`aH`/`a\\`/`ac`/`al`), `foldexpr` folding | +| 11 | Plumbing prerequisites | Server config receipt, `WorkspaceEditBuilder` + span helpers, closed-doc loader, diagnostic source chain, `Wiki` aggregate (single-entry to start) | +| 12 | Tags | `:tag:` lex + parse, `TagNode`, index, tag-as-anchor resolution | +| 13 | Workspace edits + executeCommand | `workspace/rename` with cross-doc link rewrite, `DeleteFile` ops, `executeCommand` infrastructure | +| 14 | List & table edit commands | Server commands returning `WorkspaceEdit` for checkbox toggle, list-symbol/level change, renumber, table align, column move, table insert | +| 15 | Link health + link/TOC generation | Broken-link diagnostics, `nuwiki.toc.generate`, `nuwiki.links.generate`, `nuwiki.workspace.checkLinks` | +| 16 | Diary | Diary subpath, today/yesterday/tomorrow open commands, diary index generation, calendar hook API | +| 17 | HTML export commands | `nuwiki.export.*` commands, `%template` resolution, CSS file management, auto-export option | +| 18 | Multi-wiki | `wikis = [...]` config shape, multi-entry `Wiki` aggregate (data structures already landed in Phase 11), URI → wiki resolution, wiki-picker commands | +| 19 | Editor glue v2 | Full `:Vimwiki*` command compat layer, default keymaps, text objects (`ah`/`aH`/`a\\`/`ac`/`al`), folding via LSP `foldingRange` + `foldexpr` fallback | --- @@ -619,14 +620,15 @@ These decisions are required before or during the phase indicated. | ~~P8~~ | ~~**Workspace indexing strategy**~~ | ~~Phase 8~~ | ✅ **Lazy + background with progress** | Initial scan runs as a background tokio task; nav features answer with partial data until complete; progress reported via `window/workDoneProgress`. | | ~~P9~~ | ~~**macOS runner**~~ | ~~Phase 10~~ | ✅ **Stay manual** | Release workflow cross-compiles the 4 Linux targets via `cross`; macOS + Windows binaries are produced ad hoc by maintainers and uploaded to the same Gitea release. Revisit when a Mac runner is available. | | **v1.1 decisions** | | | | | -| P10 | **Command namespace** | Phase 12 | `:Vimwiki*` only · `:Nuwiki*` only · both | Both → easy migration *and* discoverable native commands; one canonical name + alias. | -| P11 | **Tag syntax** | Phase 11 | Strict vimwiki `:tag:` only · Add markdown `#tag` as a second flavour | Strict keeps parity; adding `#tag` collides with hash list markers and ordered-list `#`. | -| P12 | **List-edit transport** | Phase 13 | Server returns `WorkspaceEdit` from `executeCommand` · Editor-side text manipulation in VimL/Lua | `WorkspaceEdit` keeps logic in Rust + testable, but adds latency per keystroke for ``. | -| P13 | **Multi-wiki config shape** | Phase 17 | Lua list (Neovim-native) · JSON config file (cross-editor) | Lua is ergonomic for Neovim; JSON works for Vim + arbitrary clients. | -| P14 | **Folding mechanism** | Phase 18 | LSP `textDocument/foldingRange` · Vim-side `foldexpr` in ftplugin | LSP is correct (server knows AST); foldexpr is what vimwiki users expect and works offline. | -| P15 | **Diary path scheme** | Phase 15 | Fixed `/diary/` · Per-wiki configurable `diary_rel_path` | Vimwiki has `diary_rel_path`; matching keeps migrations clean. | -| P16 | **Backwards-compat for v1.0 config** | Phase 17 | Preserve old single-`wiki_root` shape · Force migration to `wikis = [...]` | Single-wiki should keep working without re-config; `wiki_root` becomes sugar for `wikis = [{ root = wiki_root }]`. | -| P17 | **Markdown syntax in v1.1** | Phase 11+ | Include · Defer to v1.2 | Architecture already supports it (`SyntaxPlugin`); cost is parser work + per-syntax differences in commands. | +| P10 | **Command namespace** | Phase 13 | `:Vimwiki*` only · `:Nuwiki*` only · both | Both → easy migration *and* discoverable native commands; one canonical name + alias. | +| P11 | **Tag syntax** | Phase 12 | Strict vimwiki `:tag:` only · Add markdown `#tag` as a second flavour | Strict keeps parity; adding `#tag` collides with hash list markers and ordered-list `#`. | +| P12 | **List-edit transport** | Phase 14 | Server returns `WorkspaceEdit` from `executeCommand` · Editor-side text manipulation in VimL/Lua | `WorkspaceEdit` keeps logic in Rust + testable, but adds latency per keystroke for ``. | +| P13 | **Multi-wiki config shape** | Phase 18 | Lua list (Neovim-native) · JSON config file (cross-editor) | Lua is ergonomic for Neovim; JSON works for Vim + arbitrary clients. | +| ~~P14~~ | ~~**Folding mechanism**~~ | ~~Phase 19~~ | ✅ **LSP `textDocument/foldingRange` (primary) + `foldexpr` fallback** | Server already has the AST + heading boundaries — duplicating that knowledge as regex in VimL would be lossy. `foldexpr` ships in `ftplugin` as a fallback for when the server hasn't attached. | +| P15 | **Diary path scheme** | Phase 16 | Fixed `/diary/` · Per-wiki configurable `diary_rel_path` | Vimwiki has `diary_rel_path`; matching keeps migrations clean. | +| P16 | **Backwards-compat for v1.0 config** | Phase 18 | Preserve old single-`wiki_root` shape · Force migration to `wikis = [...]` | Single-wiki should keep working without re-config; `wiki_root` becomes sugar for `wikis = [{ root = wiki_root }]`. | +| P17 | **Markdown syntax in v1.1** | Phase 12+ | Include · Defer to v1.2 | Architecture already supports it (`SyntaxPlugin`); cost is parser work + per-syntax differences in commands. | +| P18 | **Server-config transport** | Phase 11 | `initializationOptions` only · `workspace/didChangeConfiguration` only · both | Both → simple boot path *and* live reload. Initialise at startup, accept change notifications afterwards. | --- @@ -657,7 +659,171 @@ New AST nodes (e.g. `TagNode`) and new LSP capabilities are opt-in by inspection. The Visitor trait gains default-bodied `visit_*` methods for new node kinds so existing Visitor implementations don't break. -### 12.2 Tags (Phase 11) +### 12.2 Plumbing prerequisites (Phase 11) + +Cross-cutting infrastructure that every later v1.1 phase depends on. +Landing it as Phase 11 — before any user-visible feature — keeps Phases +12–19 mechanical instead of full of "oh we also need to refactor X". + +#### 12.2.1 Server config receipt (P18) + +Per P18: both `initializationOptions` and `workspace/didChangeConfiguration` +land in Phase 11. + +- `initialize.initializationOptions` carries the full v1.1 config (§12.11 + schema) verbatim. Parsed into a server-side `Config` struct. +- `workspace/didChangeConfiguration` re-applies the config for live reload. +- Every handler reads from a single `Arc>` field on + `Backend`. + +```rust +pub struct Config { + pub log_level: LogLevel, + pub diagnostic: DiagnosticConfig, + pub wikis: Vec, // always ≥ 1; v1.0-shape desugars + // … +} + +impl Config { + pub fn from_init_options(value: serde_json::Value) -> Self; + pub fn from_v1_0_compat(legacy: LegacyConfig) -> Self; // P16 path +} +``` + +#### 12.2.2 `WorkspaceEditBuilder` + span helpers + +New module `crates/nuwiki-lsp/src/edits.rs`: + +```rust +pub fn text_edit_replace(span: Span, replacement: String, text: &str, utf8: bool) -> TextEdit; +pub fn text_edit_insert(pos: Position, content: String) -> TextEdit; +pub fn text_edit_delete(span: Span, text: &str, utf8: bool) -> TextEdit; +pub fn op_rename(old: Url, new: Url) -> DocumentChangeOperation; +pub fn op_delete(uri: Url) -> DocumentChangeOperation; + +pub struct WorkspaceEditBuilder { + changes: HashMap>, + file_ops: Vec, +} +impl WorkspaceEditBuilder { + pub fn edit(&mut self, uri: Url, edit: TextEdit) -> &mut Self; + pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self; + pub fn build(self) -> WorkspaceEdit; +} +``` + +All v1.1 commands that produce `WorkspaceEdit` (Phases 13–17) go through +the builder. Standardises UTF-8/UTF-16 conversion and document-change +ordering. + +#### 12.2.3 Closed-doc loader + +```rust +pub struct DocSnapshot { + pub text: String, + pub ast: DocumentNode, + pub in_memory: bool, // held in `documents` DashMap +} + +impl Backend { + pub async fn read_or_open(&self, uri: &Url) -> io::Result; +} +``` + +If the URI is in `documents`, return live state. Otherwise read the file +from disk and re-parse so cross-document operations (Phase 13 +`workspace/rename`) can compute accurate spans + UTF-16 conversions +instead of trusting stale `WorkspaceIndex` data. + +#### 12.2.4 Diagnostic source chain + +Today's `ast_diagnostics(&ast, &text, utf8)` is replaced by a composable +collector: + +```rust +pub fn collect_diagnostics( + ast: &DocumentNode, + text: &str, + index: Option<&WorkspaceIndex>, + page_name: Option<&str>, + cfg: &DiagnosticConfig, + utf8: bool, +) -> Vec; +``` + +Composes: + +1. Parse errors (`ErrorNode` walk; already shipped) +2. Broken-link warnings (Phase 15) +3. (future) other sources + +Each gated by `cfg`. Per-source severity is config-driven. + +#### 12.2.5 `Wiki` aggregate + +Single-wiki state becomes a `Wiki` aggregate, even though Phase 11 only +ever builds one entry. Phase 18 then changes config shape, not data +structures. + +```rust +pub struct WikiId(u32); +pub struct WikiConfig { + pub name: String, + pub root: PathBuf, + pub file_extension: String, + pub syntax: String, + pub diary_rel_path: String, + pub html_path: PathBuf, + pub template_path: PathBuf, + // … all per-wiki options +} +pub struct Wiki { + pub id: WikiId, + pub config: WikiConfig, + pub index: Arc>, +} + +struct Backend { + // … + wikis: Arc>>, +} + +impl Backend { + fn resolve_uri_to_wiki(&self, uri: &Url) -> Option; // longest-prefix root match + fn wiki(&self, id: WikiId) -> Option>; + fn default_wiki(&self) -> Option>; +} +``` + +`WorkspaceIndex` itself doesn't gain any new responsibilities in this +phase — it just lives inside a `Wiki`. All v1.0 handlers that today read +the single `index` field get refactored to call `default_wiki()` (or +`resolve_uri_to_wiki()` where the URI is known). + +#### 12.2.6 What does *not* change + +Explicitly out of scope for Phase 11: + +- AST shape (no new node kinds — Tags wait for Phase 12) +- LSP capability set (no new providers — capabilities expand from Phase 13) +- HtmlRenderer surface (Phase 17 extends the template substitution set) +- Anything user-visible (this phase is invisible to clients) + +#### 12.2.7 Tests + +Phase 11 is fully covered by unit tests on the new helpers: + +- `WorkspaceEditBuilder`: multi-uri accumulation, file-op ordering, + UTF-16 conversion for non-ASCII spans. +- `read_or_open`: live doc returns `in_memory: true`; missing path + returns `Err`; on-disk path returns `in_memory: false` with parsed AST. +- `collect_diagnostics`: parse errors emitted; link-health stub returns + empty until Phase 15 wires it. +- `Config::from_init_options`: legacy single-wiki shape desugars to + one-entry `wikis = [...]`. +- `Backend::resolve_uri_to_wiki`: longest-prefix match wins. + +### 12.3 Tags (Phase 12) #### Syntax @@ -714,7 +880,15 @@ existing backlink machinery in §6 stays as-is. - New `executeCommand` operations: `nuwiki.tags.search`, `nuwiki.tags.generateLinks(tag, ...)`, `nuwiki.tags.rebuild` (force re-index). -### 12.3 Workspace edits + `executeCommand` (Phase 12) +### 12.4 Workspace edits + `executeCommand` (Phase 13) + +> **Note on closed documents.** When `workspace/rename` rewrites incoming +> links across pages that are *not* open in the editor, the server uses the +> Phase 11 closed-doc loader (`read_or_open`) to re-parse them on demand. +> Stored `WorkspaceIndex` spans are byte-accurate but may have drifted if +> the on-disk file changed outside the editor; the re-parse guarantees we +> emit `TextEdit`s against the current source text and convert to LSP +> positions correctly (esp. for non-ASCII content under UTF-16 encoding). #### Capability @@ -757,7 +931,7 @@ takes either: and returns a `WorkspaceEdit` (which the client applies via `workspace/applyEdit`). -### 12.4 List & table editing commands (Phase 13) +### 12.5 List & table editing commands (Phase 14) | Command | Operates on | Result | |---|---|---| @@ -782,7 +956,7 @@ and returns a `WorkspaceEdit` (which the client applies via All operations resolve to a textual diff over the existing source so the AST stays the source of truth. None of them rely on editor-side state. -### 12.5 Link health + TOC/index generation (Phase 14) +### 12.6 Link health + TOC/index generation (Phase 15) #### Broken-link diagnostics @@ -804,7 +978,13 @@ A new diagnostic source `nuwiki.link` runs after each parse: | `nuwiki.workspace.checkLinks` | returns a `Vec` for the client to render in a quickfix-style view; also emits diagnostics | | `nuwiki.workspace.findOrphans` | returns pages with no incoming links | -### 12.6 Diary (Phase 15) +### 12.7 Diary (Phase 16) + +> **Date parsing.** Diary entries use `YYYY-MM-DD` (and `YYYY-Www`, +> `YYYY-MM`, `YYYY` for non-daily frequencies). v1.1 ships a small +> hand-rolled parser/formatter in `nuwiki-core` for these formats — +> no `chrono` / `time` dependency. The parsable surface is small enough +> that the dependency cost outweighs the convenience. #### Path conventions @@ -838,7 +1018,7 @@ External plugins (Calendar.vim, neorg-style calendars) can query: These are `executeCommand` operations, not LSP standard methods. -### 12.7 HTML export commands (Phase 16) +### 12.8 HTML export commands (Phase 17) The `HtmlRenderer` from Phase 5 stays as the engine. v1.1 wraps it in user-facing commands and the per-wiki HTML lifecycle. @@ -878,7 +1058,15 @@ The `HtmlRenderer` template substitution gains: hrefs in nested pages) - `{{toc}}` (rendered TOC for current page) -### 12.8 Multi-wiki (Phase 17) +### 12.9 Multi-wiki (Phase 18) + +> **Data-structure pre-work.** The `Wiki` aggregate, `WorkspaceIndex` +> per-wiki ownership, and `Backend::resolve_uri_to_wiki` already landed +> in Phase 11 with a single-entry `wikis` list. Phase 18 is therefore +> a *config-shape* change (accept `wikis = [...]` from +> `initializationOptions`) and *resolution-policy* change (interwiki +> links resolve through the matching wiki's index) — not a refactor of +> the data flow. #### Config shape @@ -928,7 +1116,7 @@ the matching wiki's index. | `nuwiki.wiki.tabOpenIndex` | open in new tab (client-side) | | `nuwiki.wiki.listAll` | returns the registered wikis for editor UIs | -### 12.9 Editor glue v2 (Phase 18) +### 12.10 Editor glue v2 (Phase 19) The thickest user-facing layer. Per SPEC §6.3 it still contains no logic — every command body is a wrapper that issues `workspace/executeCommand`. @@ -1060,7 +1248,7 @@ Per P14 resolution; the spec'd default (subject to that decision): - HTML output path writable - Default keymaps installed (or disabled by config) -### 12.10 Updated user-config schema +### 12.11 Updated user-config schema ```lua require('nuwiki').setup({ @@ -1114,7 +1302,7 @@ require('nuwiki').setup({ }) ``` -### 12.11 Updated §9 syntax checklist (additions) +### 12.12 Updated §9 syntax checklist (additions) The following items extend the v1.0 checklist: @@ -1132,7 +1320,7 @@ The following items extend the v1.0 checklist: - [ ] Multi-line list item (indented continuation lines) - [ ] Mixed-symbol lists at the same level -### 12.12 Updated CI/CD +### 12.13 Updated CI/CD No new workflow files. The existing `ci.yaml` continues to gate `fmt`/`clippy`/`test`. New phases land behind feature additions in the