spec: add Phase 11 plumbing prereqs; resolve P14; renumber 12+
CI / cargo fmt --check (push) Successful in 38s
CI / cargo clippy (push) Successful in 1m11s
CI / cargo test (push) Successful in 1m21s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 14:16:00 +00:00
parent f435679814
commit cb244a8080
+215 -27
View File
@@ -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 `<C-Space>`. |
| 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 `<wiki_root>/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 `<C-Space>`. |
| 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 `<wiki_root>/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
1219 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<RwLock<Config>>` field on
`Backend`.
```rust
pub struct Config {
pub log_level: LogLevel,
pub diagnostic: DiagnosticConfig,
pub wikis: Vec<WikiConfig>, // 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<Url, Vec<TextEdit>>,
file_ops: Vec<DocumentChangeOperation>,
}
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 1317) 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<DocSnapshot>;
}
```
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<Diagnostic>;
```
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<RwLock<WorkspaceIndex>>,
}
struct Backend {
// …
wikis: Arc<RwLock<Vec<Wiki>>>,
}
impl Backend {
fn resolve_uri_to_wiki(&self, uri: &Url) -> Option<WikiId>; // longest-prefix root match
fn wiki(&self, id: WikiId) -> Option<Arc<Wiki>>;
fn default_wiki(&self) -> Option<Arc<Wiki>>;
}
```
`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<BrokenLink>` 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