From 5135840f053255110afbdeea5dc32c0c77050bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Sat, 30 May 2026 15:07:59 -0300 Subject: [PATCH] Move SPEC.md to development/ and rewrite as a technical reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old spec was a 1426-line forward-looking planning doc (phases, pending decisions, design sketches for work now shipped). Replace it with a concise 339-line reference describing the implemented system — architecture, AST, LSP capabilities + executeCommand surface, editor integration, config schema, keymaps, syntax support, and CI — verified against the current code. Lives under development/ with the other developer-only docs. Co-Authored-By: Claude Opus 4.7 --- SPEC.md | 1425 ------------------------------------- development/ONBOARDING.md | 1 + development/SPEC.md | 339 +++++++++ 3 files changed, 340 insertions(+), 1425 deletions(-) delete mode 100644 SPEC.md create mode 100644 development/SPEC.md diff --git a/SPEC.md b/SPEC.md deleted file mode 100644 index 3da8bfe..0000000 --- a/SPEC.md +++ /dev/null @@ -1,1425 +0,0 @@ -# nuwiki — Project Specification - -> Last updated: 2026-05-11 -> Status: v1.0 (Phases 0–10) shipped; v1.1 (Phases 11–18) in design - -> ### Versioning convention -> -> - **v1.0** — the parsing + highlighting + navigation foundation. LSP server -> speaks the spec's §6.9 method set; editor glue provides install + setup -> + health check. -> - **v1.1** — full vimwiki replacement. Adds tags, diary, the -> text-manipulation command surface (toggle checkbox, change list symbol, -> format table, rename page, …), multi-wiki, HTML export commands, -> link-health diagnostics, and the keymap/text-object layer that matches -> vimwiki's daily-authoring ergonomics. - ---- - -## 1. Overview - -nuwiki is a Vim/Neovim plugin that provides full vimwiki syntax support, implemented as a Rust-based Language Server (LSP). It is a spiritual successor to vimwiki, architecturally distinct from it, designed for correctness, performance, and extensibility. - -**Goals:** -- Full vimwiki syntax support (highlighting, navigation, link following, diagnostics) -- First-class support for both Vim 9+ and Neovim 0.5+ -- Installable via standard plugin managers (lazy.nvim, vim-plug, Dein) -- Extensible to Markdown syntax without changes to core architecture -- Independently testable core library with no editor dependencies - -**Non-goals (v1.0):** -- MediaWiki syntax support -- Multi-wiki configurations *(addressed in v1.1, see §12)* -- Incremental / tree-sitter parsing -- Browser-based or WASM build - -**v1.1 scope** *(see §12 for the full plan)*: -- Vimwiki-parity command surface (`:Vimwiki*` compat aliases over LSP - `executeCommand`) -- Tags, diary, link health, HTML export commands, multi-wiki, folding - ---- - -## 2. Repository - -| Property | Value | -|---|---| -| License | Dual MIT/Apache-2.0 | -| MSRV | Rust 1.83 (stable-2) | -|---|---| -| URL | `https://code.gfran.co/gffranco/nuwiki` | -| Version control | Gitea | -| CI/CD | Gitea Actions | - ---- - -## 3. Naming Conventions - -| Context | Name | -|---|---| -| GitHub/Gitea repo | `nuwiki` | -| Cargo workspace | `nuwiki` | -| Core library crate | `nuwiki-core` | -| LSP bridge crate | `nuwiki-lsp` | -| Binary crate | `nuwiki-ls` | -| Vim plugin entry | `plugin/nuwiki.vim` | -| Lua module | `require('nuwiki')` | -| VimL autoload | `nuwiki#lsp#start()` | -| Vim help file | `doc/nuwiki.txt` → `:h nuwiki` | -| Release binary | `nuwiki-ls-{version}-{target}.tar.gz` | - ---- - -## 4. Technology Stack - -| Concern | Choice | Rationale | -|---|---|---| -| Implementation language | Rust | Performance, type safety, single binary distribution, ideal for AST modelling | -| Editor integration | LSP over stdio | Works in both Vim and Neovim without a shared scripting language | -| LSP server library | `tower-lsp` | Async, tokio-based, higher-level than `lsp-server`; easier to start with | -| Parser library | None — hand-rolled recursive descent | Tokens are span-bearing structs that fit awkwardly into `winnow`/`nom` combinator style; resilience and error recovery are explicit in the parser code. Originally specified as `winnow`; revisited in Phase 4. | -| Cargo edition | 2021 | Current standard; required for resolver v2 | -| Cargo resolver | v2 | Required for edition 2021 workspaces | -| Vim glue layer | VimL (`plugin/nuwiki.vim`, `autoload/`) | Universal entry point for all plugin managers | -| Neovim glue layer | Lua (`lua/nuwiki/`) | First-class Neovim API access via `vim.lsp.start()` | - ---- - -## 5. Repository Layout - -``` -nuwiki/ -│ -├── Cargo.toml # Rust workspace root -├── Cargo.lock -│ -├── crates/ -│ ├── nuwiki-ls/ # Binary crate — thin main.rs, starts stdio LSP server -│ │ ├── Cargo.toml -│ │ └── src/main.rs -│ │ -│ ├── nuwiki-core/ # Library crate — parser, AST, renderer (no editor deps) -│ │ ├── Cargo.toml -│ │ └── src/ -│ │ ├── lib.rs -│ │ ├── syntax/ -│ │ │ ├── mod.rs -│ │ │ ├── registry.rs -│ │ │ └── vimwiki/ -│ │ │ ├── mod.rs -│ │ │ ├── lexer.rs -│ │ │ └── parser.rs -│ │ ├── ast/ -│ │ │ ├── mod.rs -│ │ │ ├── block.rs -│ │ │ ├── inline.rs -│ │ │ └── link.rs -│ │ └── render/ -│ │ ├── mod.rs -│ │ └── html.rs -│ │ -│ └── nuwiki-lsp/ # Library crate — LSP protocol bridge -│ ├── Cargo.toml -│ └── src/lib.rs -│ -├── plugin/ # Universal Vim/Neovim entry point -│ └── nuwiki.vim -│ -├── lua/ # Neovim-specific Lua layer -│ └── nuwiki/ -│ ├── init.lua -│ ├── config.lua -│ ├── lsp.lua -│ └── install.lua # Binary download / build-from-source logic -│ -├── autoload/ # Lazy-loaded VimL (Vim compat layer) -│ └── nuwiki/ -│ └── lsp.vim -│ -├── ftdetect/ # Filetype detection for .wiki files -│ └── nuwiki.vim -│ -├── ftplugin/ # Per-filetype buffer settings -│ └── nuwiki.vim -│ -├── syntax/ # Static fallback syntax highlighting (no LSP required) -│ └── nuwiki.vim -│ -├── doc/ # Vim help documentation -│ └── nuwiki.txt -│ -├── scripts/ -│ └── download_bin.vim # VimL binary download (used by Dein/vim-plug build hooks) -│ -└── .gitea/ - └── workflows/ - ├── ci.yaml # Lint, test, fmt check on every push/PR - └── release.yaml # Cross-compile + release on v* tag -``` - ---- - -## 6. Architecture - -### 6.1 Layer Model - -``` -┌─────────────────────────────────────────────────────┐ -│ Consumer Layer │ -│ (editor highlight, HTML export, TOC, …) │ -└────────────────────────┬────────────────────────────┘ - │ operates on - ▼ -┌─────────────────────────────────────────────────────┐ -│ AST (nuwiki-core, shared) │ -│ Document > Block nodes > Inline nodes │ -└────────────────────────┬────────────────────────────┘ - │ produced by - ▼ -┌─────────────────────────────────────────────────────┐ -│ Parser — syntax-specific (nuwiki-core) │ -│ Registered per syntax; receives TokenStream │ -└────────────────────────┬────────────────────────────┘ - │ consumes - ▼ -┌─────────────────────────────────────────────────────┐ -│ Lexer — syntax-specific (nuwiki-core) │ -│ Registered per syntax; receives raw text │ -└────────────────────────┬────────────────────────────┘ - │ reads - ▼ - Raw text input -``` - -### 6.2 Crate Dependency Rules - -``` -nuwiki-ls → nuwiki-lsp → nuwiki-core -``` - -- `nuwiki-core` must never depend on `nuwiki-lsp` or `nuwiki-ls` -- `nuwiki-lsp` must never depend on `nuwiki-ls` -- The VimL and Lua editor layers must never contain logic — pure wiring only - -### 6.3 Syntax Plugin Interface - -Every syntax (vimwiki, future markdown) implements: - -``` -SyntaxPlugin - id: string -- "vimwiki" | "markdown" - display_name: string - file_extensions: string[] -- [".wiki"] | [".md"] - lexer: fn(text) → TokenStream - parser: fn(TokenStream) → DocumentNode -``` - -Registry: -``` -SyntaxRegistry - register(plugin: SyntaxPlugin) - get(id: &str) → Option<&SyntaxPlugin> - detect_from_extension(ext: &str) → Option<&SyntaxPlugin> -``` - -All plugins must be `Send + Sync` (held in the LSP server registry). - -### 6.4 AST Node Types - -#### Document -``` -DocumentNode - children: Vec - metadata: PageMetadata -- title, nohtml, template, date -``` - -#### Block Nodes -``` -HeadingNode level: 1–6 | children: Vec | centered: bool -ParagraphNode children: Vec -HorizontalRuleNode -BlockquoteNode children: Vec -PreformattedNode content: String | language: Option -MathBlockNode content: String | environment: Option -ListNode ordered: bool | symbol: ListSymbol | items: Vec -DefinitionListNode items: Vec -TableNode rows: Vec | has_header: bool -CommentNode content: String -ErrorNode raw: String | message: String -- resilient parse failure -``` - -#### Inline Nodes -``` -TextNode content: String -BoldNode children: Vec -ItalicNode children: Vec -BoldItalicNode children: Vec -StrikethroughNode children: Vec -CodeNode content: String -SuperscriptNode children: Vec -SubscriptNode children: Vec -MathInlineNode content: String -KeywordNode keyword: Keyword -- TODO|DONE|STARTED|FIXME|FIXED|XXX -ColorNode color: String | children: Vec -WikiLinkNode target: LinkTarget | description: Option> -ExternalLinkNode url: String | description: Option> -TransclusionNode url: String | alt: Option | attrs: HashMap -RawUrlNode url: String -``` - -#### Supporting Types -``` -ListItemNode - symbol: ListSymbol - level: usize - checkbox: Option - children: Vec - sublist: Option - -DefinitionItemNode - term: Option> - definitions: Vec> - -TableRowNode cells: Vec | is_header: bool -TableCellNode children: Vec | col_span: bool | row_span: bool - -LinkTarget - kind: LinkKind -- Wiki|Interwiki|Diary|File|Local|Raw|AnchorOnly - path: Option - wiki_index: Option - wiki_name: Option - anchor: Option - is_absolute: bool - is_directory: bool - -ListSymbol = Dash | Star | Hash | Numeric | NumericParen - | AlphaParen | AlphaUpperParen | RomanParen | RomanUpperParen -CheckboxState = Empty | Quarter | Half | ThreeQuarters | Done | Rejected -Keyword = Todo | Done | Started | Fixme | Fixed | Xxx -``` - -### 6.5 Span / Source Location - -Every AST node carries a `Span`: - -```rust -struct Span { - start: Position, - end: Position, -} - -struct Position { - line: u32, -- 0-indexed - column: u32, -- 0-indexed, byte offset within line - offset: usize, -- absolute byte offset from document start -} -``` - -Spans are required for LSP diagnostics, semantic tokens, and go-to-definition. - -### 6.6 Lexer Strategy - -- **Two-pass:** block-level pass first, then inline pass within each block -- **Token types:** syntax-specific (`VimwikiToken`) — not shared across syntaxes -- **TokenStream:** eager `Vec` wrapped in a `TokenStream` newtype (changeable to lazy later) -- Operates on Rust `char`s for correctness; spans stored as byte offsets - -### 6.7 Parser Strategy - -- Uses `winnow` parser combinator library -- Resilient: on malformed input, emits `ErrorNode` and continues — never fails the whole document -- Inline marker precedence rules documented explicitly in code and tests - -### 6.8 Renderer - -- `Renderer` trait: writer-based (`fn render(&self, doc: &DocumentNode, w: &mut dyn Write)`) -- `HtmlRenderer` is the first implementation -- Link resolution injected as a callback at construction time -- Template support: minimal `{{content}}` / `{{title}}` token substitution -- CSS: linked external stylesheet (matches vimwiki convention) - -### 6.9 LSP Features - -| Feature | LSP Method | -|---|---| -| Syntax highlighting | `textDocument/semanticTokens/full` + `/range` | -| Diagnostics (broken links, parse errors) | `textDocument/publishDiagnostics` | -| Follow link / go to definition | `textDocument/definition` | -| Backlinks | `textDocument/references` | -| TOC / outline | `textDocument/documentSymbol` | -| Link preview | `textDocument/hover` | -| Link autocomplete | `textDocument/completion` (trigger: `[[`) | -| Page rename | `workspace/rename` | -| Workspace search | `workspace/symbol` | - -### 6.10 Document Store - -``` -DocumentStore: Arc> - -DocumentState - text: String - ast: DocumentNode - version: i32 -``` - -Full re-parse on every `didOpen` / `didChange`. Incremental parsing deferred post-v1. - -### 6.11 UTF-16 / Position Encoding - -LSP positions negotiated as UTF-8 (`positionEncoding = "utf-8"`) during `initialize` where the client supports LSP 3.17+. UTF-16 conversion fallback implemented in the LSP layer for older clients. `nuwiki-core` is always UTF-8 / byte-offset internally. - ---- - -## 7. Editor Integration - -### 7.1 Plugin Manager Installation - -**lazy.nvim:** -```lua -{ - "gffranco/nuwiki", - build = "lua require('nuwiki').install()", - ft = { "vimwiki" }, - opts = {}, -} -``` - -**vim-plug:** -```vim -Plug 'gffranco/nuwiki', { 'do': 'vim -e -s -c "source scripts/download_bin.vim" -c "q"' } -``` - -**Dein:** -```vim -call dein#add('gffranco/nuwiki', { - \ 'build': 'vim -e -s -c "source scripts/download_bin.vim" -c "q"' - \ }) -``` - -### 7.2 Binary Distribution - -- Pre-built binaries published as Gitea release assets -- Named: `nuwiki-ls-{version}-{target}.tar.gz` -- Downloaded at install time by `lua/nuwiki/install.lua` or `scripts/download_bin.vim` -- Fallback: `cargo build --release` if download fails or `g:nuwiki_build_from_source = 1` -- Binary installed to `{plugin_dir}/bin/nuwiki-ls[.exe]` - -### 7.3 Editor Detection - -```vim -" plugin/nuwiki.vim -if has('nvim') - lua require('nuwiki').setup() -else - call nuwiki#lsp#start() -endif -``` - -### 7.4 Vim LSP Client Preference Order (Vim only) - -1. `vim-lsp` (matoto/vim-lsp) -2. `coc.nvim` -3. Error message if neither found - -### 7.5 User Config Schema - -```lua -require('nuwiki').setup({ - wiki_root = "~/vimwiki", -- root directory of the wiki - file_extension = ".wiki", -- file extension to associate - syntax = "vimwiki", -- "vimwiki" | "markdown" (future) - log_level = "warn", -- "error"|"warn"|"info"|"debug" -}) -``` - -### 7.6 Health Check - -`:checkhealth nuwiki` verifies: -- Binary exists at `{plugin_dir}/bin/nuwiki-ls` -- Binary is executable and responds to `--version` -- LSP client is running -- Filetype detection works for `.wiki` files - ---- - -## 8. CI/CD - -### 8.1 Runner Setup - -| Property | Value | -|---|---| -| System | Gitea Actions (`act_runner`) | -| Mode | Docker (jobs run in containers) | -| Docker access | Host socket mounted (`/var/run/docker.sock`) | -| Internet access | Yes | - -### 8.2 Workflow Files - -**`.gitea/workflows/ci.yaml`** — triggers on every push and PR: -- `cargo fmt --check` -- `cargo clippy -- -D warnings` -- `cargo test --workspace` - -**`.gitea/workflows/release.yaml`** — triggers on `v*` tag push: -- Cross-compile for all 4 Linux targets using `cross` -- Package binaries as `.tar.gz` -- Create Gitea release and upload assets via REST API using `RELEASE_TOKEN` secret -- crates.io publish is deferred — workflow ships the Gitea release only. Re-enable by adding a `cargo publish -p nuwiki-core` job once the crate is ready for publication. - -### 8.3 Build Targets - -| Target | Built by | -|---|---| -| `x86_64-unknown-linux-gnu` | CI (`cross`) | -| `aarch64-unknown-linux-gnu` | CI (`cross`) | -| `x86_64-unknown-linux-musl` | CI (`cross`) | -| `aarch64-unknown-linux-musl` | CI (`cross`) | -| `x86_64-apple-darwin` | Manual | -| `aarch64-apple-darwin` | Manual | -| `x86_64-pc-windows-msvc` | Manual | - -### 8.4 Release Secrets - -| Secret | Purpose | -|---|---| -| `RELEASE_TOKEN` | Gitea personal access token for creating releases and uploading assets | -| ~~`CARGO_REGISTRY_TOKEN`~~ | ~~crates.io API token for publishing `nuwiki-core`~~ — deferred (see §8.2) | - -### 8.5 `actions/cache` Configuration - -Requires `act_runner`'s `config.yaml` to have the cache server host set to the runner host's LAN IP so job containers can reach it. - -### 8.6 Breaking Change Policy - -A semver-breaking change (`v0.x` → `v0.x+1` or `v1.x` → `v2.0`) is defined as any of: -- AST node type additions, removals, or field changes in `nuwiki-core` -- `SyntaxPlugin` or `Renderer` trait signature changes -- User config schema key removals or type changes -- Removed LSP capabilities - ---- - -## 9. Vimwiki Syntax Feature Checklist - -### Typefaces -- [ ] Bold: `*text*` -- [ ] Italic: `_text_` -- [ ] Bold italic: `_*text*_` / `*_text_*` -- [ ] Strikethrough: `~~text~~` -- [ ] Inline code: `` `text` `` -- [ ] Superscript: `super^script^` -- [ ] Subscript: `sub,,script,,` -- [ ] Keywords: `TODO` `DONE` `STARTED` `FIXME` `FIXED` `XXX` - -### Links -- [ ] Plain wikilink: `[[Target]]` -- [ ] Described wikilink: `[[Target|Description]]` -- [ ] Subdirectory wikilink: `[[dir/Page]]` -- [ ] Root-relative wikilink: `[[/Page]]` -- [ ] Filesystem-absolute wikilink: `[[//path]]` -- [ ] Subdirectory link: `[[dir/]]` -- [ ] Interwiki numbered: `[[wiki1:Page]]` -- [ ] Interwiki named: `[[wn.Name:Page]]` -- [ ] Diary link: `[[diary:YYYY-MM-DD]]` -- [ ] Anchor-only link: `[[#Anchor]]` -- [ ] Wikilink with anchor: `[[Page#Anchor]]` -- [ ] Raw URLs: `https://…` `mailto:…` `ftp://…` -- [ ] External file link: `[[file:path]]` / `[[local:path]]` -- [ ] Transclusion: `{{URL}}` with optional alt and attrs -- [ ] Thumbnail link: `[[imgURL|{{thumbURL}}]]` - -### Headers -- [ ] Levels 1–6: `= H1 =` … `====== H6 ======` -- [ ] Centered header (leading whitespace before `=`) - -### Lists -- [ ] Unordered: `-` and `*` -- [ ] Ordered: `1.` `1)` `a)` `A)` `i)` `I)` `#` -- [ ] Nested and mixed types -- [ ] Multi-line items (indentation continuation) -- [ ] Definition lists: `Term:: Definition` -- [ ] Checkboxes: `[ ]` `[.]` `[o]` `[O]` `[X]` `[-]` - -### Tables -- [ ] Basic `|`-delimited table -- [ ] Header row separator: `|---|` -- [ ] Column span: `>` -- [ ] Row span: `\/` -- [ ] Inline formatting inside cells - -### Preformatted Text -- [ ] Fenced block: `{{{ … }}}` -- [ ] Optional language/class on opening fence - -### Mathematical Formulae -- [ ] Inline math: `$ … $` -- [ ] Block display math: `{{$ … }}$` -- [ ] Block environment math: `{{$%env% … }}$` - -### Blockquotes -- [ ] 4-space indent blockquote -- [ ] `>` prefix blockquote - -### Comments -- [ ] Single-line: `%% …` -- [ ] Multi-line: `%%+ … +%%` - -### Horizontal Rule -- [ ] Four or more dashes: `----` - -### Placeholders -- [ ] `%title ` -- [ ] `%nohtml` -- [ ] `%template ` -- [ ] `%date ` - ---- - -## 10. Implementation Phases - -| Phase | Name | Key Output | -|---|---|---| -| 0 | Scaffolding | Compiling empty workspace + CI skeleton | -| 1 | Core AST | All node types, spans, Visitor trait | -| 2 | Syntax Plugin Interface | `Lexer`/`Parser`/`SyntaxPlugin` traits, `SyntaxRegistry` | -| 3 | Vimwiki Lexer | `VimwikiToken`, two-pass lexer with spans | -| 4 | Vimwiki Parser | Full AST from token stream, error recovery | -| 5 | Renderer | `Renderer` trait + `HtmlRenderer` | -| 6 | LSP Foundation | `didOpen`/`didChange`, diagnostics, document symbols | -| 7 | Semantic Tokens | AST → LSP semantic token stream | -| 8 | Navigation | Definition, references, hover, completion, workspace index | -| 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 | 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 | - ---- - -## 11. Pending Decisions - -These decisions are required before or during the phase indicated. - -| # | Decision | Needed by | Options | Notes | -|---|---|---|---|---| -| ~~P1~~ | ~~**License**~~ | ~~Phase 0~~ | ✅ **Dual MIT/Apache-2.0** | | -| ~~P2~~ | ~~**MSRV**~~ | ~~Phase 0~~ | ✅ **stable-2 (Rust 1.83)** | | -| ~~P3~~ | ~~**String representation in AST**~~ | ~~Phase 1~~ | ✅ **`String` (owned)** | | -| ~~P4~~ | ~~**Visitor pattern**~~ | ~~Phase 1~~ | ✅ **Defined in Phase 1** | Open-recursion `Visitor` trait + `walk_*` helpers | -| ~~P5~~ | ~~**Minimum Neovim version**~~ | ~~Phase 9~~ | ✅ **Neovim 0.11+** | Server registered via `vim.lsp.config{}` + `vim.lsp.enable`; a `vim.lsp.start` autocmd fallback is wired up but only fires when the declarative API is unavailable. | -| ~~P6~~ | ~~**Minimum Vim version**~~ | ~~Phase 9~~ | ✅ **Vim 9.1+** | autoload glue assumes 9.1 idioms; uses `vim-lsp` first, then falls back to `coc.nvim`. | -| ~~P7~~ | ~~**Semantic token type mapping**~~ | ~~Phase 7~~ | ✅ **Custom vimwiki-specific token types** | ~20 types (`vimwikiHeading`, `vimwikiBold`, …) + `level1`..`level6` + `centered` modifiers. Phase 9 ships default highlight groups in `syntax/nuwiki.vim` and the Lua glue. | -| ~~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 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. | - ---- - -## 12. v1.1 — Full vimwiki Replacement - -v1.0 shipped the parsing + highlighting + navigation foundation. A vimwiki -user transitioning to nuwiki today gets accurate syntax recognition and -modern LSP nav, but loses the dense interactive layer that makes vimwiki -ergonomic for daily authoring: ≈40 `:Vimwiki*` commands, ≈60 keymaps, the -diary, the tag system, link health, HTML export commands, and multi-wiki -configuration. - -v1.1 closes that gap. The architecture is unchanged — every new editing -operation lands as an LSP `executeCommand` returning a `WorkspaceEdit`, so -the `nuwiki-core` AST stays the single source of truth and the editor glue -stays pure wiring. - -### 12.1 Stability commitment - -v1.1 is additive. The following stay backward-compatible: - -- `nuwiki-core` public AST + Visitor (additions only; no field renames) -- LSP capabilities advertised in v1.0 -- `Renderer` trait + `HtmlRenderer` default output shape -- v1.0 user-config keys (`wiki_root`, `file_extension`, `syntax`, `log_level`) - -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 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 - -Vimwiki tags are colon-delimited sequences of non-space characters: - -``` -:tag-one:tag-two:other-tag: -``` - -Placement rules (matching vimwiki): - -- On line 1 or 2 of a file → file-level tag -- Within 2 lines after a `= Heading =` → header-level tag -- Otherwise → standalone anchor at the source line - -#### AST additions - -``` -TagNode (block) - span: Span - tags: Vec - scope: TagScope -- File | Heading(idx) | Standalone - -PageMetadata (extend) - tags: Vec -- file-level tags, accumulated -``` - -`TagNode` slots into `BlockNode` as a new variant. The Visitor gains -`visit_tag` (default-bodied). - -#### Indexing - -`WorkspaceIndex` extends `IndexedPage` with: - -``` -IndexedPage (extend) - tags: Vec - -TagInfo - name: String - scope: TagScope - span: Span -``` - -Plus a `tags_by_name: HashMap>` reverse map. The -existing backlink machinery in §6 stays as-is. - -#### LSP behaviour - -- `tags-as-anchors` extends `textDocument/definition`: `[[Page#some-tag]]` - resolves to the tag's location in the target page (in addition to headings). -- `workspace/symbol` results include tags (with `SymbolKind::PROPERTY` or a - custom kind). -- New `executeCommand` operations: `nuwiki.tags.search`, - `nuwiki.tags.generateLinks(tag, ...)`, `nuwiki.tags.rebuild` (force re-index). - -### 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 - -``` -ServerCapabilities { - execute_command_provider: Some(ExecuteCommandOptions { - commands: vec!["nuwiki.*", …], - }), - rename_provider: Some(true), - workspace: Some(WorkspaceServerCapabilities { - file_operations: Some(FileOperationOptions { - will_rename: Some(...), - did_rename: Some(...), - will_delete: Some(...), - did_delete: Some(...), - }), - }), -} -``` - -#### `workspace/rename` semantics - -When a user renames `Page A` → `Page B`: - -1. Server emits a `WorkspaceEdit` containing a `RenameFile` op (`A.wiki` → - `B.wiki`). -2. For every page in the index that links to `A`, the edit also rewrites - `[[A]]` → `[[B]]` (preserving description, anchor, kind). -3. Anchors don't trigger renames — `[[A#anchor]]` follows. - -#### Custom commands router - -A single `executeCommand` handler dispatches by command name. Every command -takes either: - -``` -{ uri: Url, position?: Position, range?: Range, ...args } -``` - -and returns a `WorkspaceEdit` (which the client applies via -`workspace/applyEdit`). - -### 12.5 List & table editing commands (Phase 14) - -| Command | Operates on | Result | -|---|---|---| -| `nuwiki.list.toggleCheckbox` | list item under cursor | `[ ]` ↔ `[X]`, propagates parent state | -| `nuwiki.list.cycleCheckbox` | list item | `[ ]` → `[.]` → `[o]` → `[O]` → `[X]` cycle | -| `nuwiki.list.rejectCheckbox` | list item | toggle `[-]` rejected state | -| `nuwiki.list.changeSymbol` | list item, args: `symbol: ListSymbol`, `whole_list: bool` | rewrite marker, renumber if numeric | -| `nuwiki.list.changeLevel` | list item, args: `delta: i32`, `whole_subtree: bool` | re-indent + adjust child levels | -| `nuwiki.list.renumber` | list, args: `whole_file: bool` | re-sequence numeric markers | -| `nuwiki.list.removeDone` | range or current list | remove every `[X]` / `[-]` item and its checked children | -| `nuwiki.list.nextTask` | document | navigate to next unfinished task (returns `Location`, not edit) | -| `nuwiki.table.align` | table under cursor | reformat columns to max width | -| `nuwiki.table.moveColumn` | cell, args: `dir: "left"\|"right"` | swap column with neighbour | -| `nuwiki.table.insert` | cursor, args: `cols, rows` | insert blank table at cursor | -| `nuwiki.heading.addLevel` | heading | promote (`==` → `===`) | -| `nuwiki.heading.removeLevel` | heading | demote | -| `nuwiki.link.normalize` | word/selection | convert to `[[wikilink]]`, add description if missing | -| `nuwiki.link.pasteWikilink` | cursor | paste current page name as an absolute wikilink | -| `nuwiki.link.pasteUrl` | cursor | paste the corresponding HTML output URL | -| `nuwiki.colorize` | range, args: `color: String` | wrap in colour tag per `color_tag_template` | - -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.6 Link health + TOC/index generation (Phase 15) - -#### Broken-link diagnostics - -A new diagnostic source `nuwiki.link` runs after each parse: - -- Wiki target → not in `WorkspaceIndex` → severity from config (default - `Warning`) -- Anchor target → page exists but anchor doesn't → `Warning` -- `file:` / `local:` → file exists on disk → `Warning` if missing -- Raw URL / external → never diagnosed (we don't fetch) -- Configurable filtering via `diagnostic.link.severity` (`off|hint|warn|error`) - -#### Commands - -| Command | Behaviour | -|---|---| -| `nuwiki.toc.generate` | inserts a nested list of headings in the current page; replaces existing `%toc` placeholder if present | -| `nuwiki.links.generate` | inserts a flat list of all wiki pages (vimwiki's `:VimwikiGenerateLinks`) | -| `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.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 - -``` -// # default: "diary" - diary.wiki # configurable: diary_index - 2026-05-11.wiki # daily: YYYY-MM-DD - 2026-W19.wiki # weekly: YYYY-Www (configurable) - 2026-05.wiki # monthly - 2026.wiki # yearly -``` - -#### Custom commands - -| Command | Behaviour | -|---|---| -| `nuwiki.diary.openToday` | open / create today's diary page | -| `nuwiki.diary.openYesterday` | analogous | -| `nuwiki.diary.openTomorrow` | analogous | -| `nuwiki.diary.openIndex` | open the diary index page | -| `nuwiki.diary.generateIndex` | rebuild the diary index page with current entries grouped by year/month | -| `nuwiki.diary.next` | navigate to the chronologically next diary entry from the current one | -| `nuwiki.diary.prev` | analogous | - -#### Calendar hook - -External plugins (Calendar.vim, neorg-style calendars) can query: - -- `nuwiki.diary.listEntries(year, month)` → list of dates with entries -- `nuwiki.diary.openForDate(date)` → opens that date's entry - -These are `executeCommand` operations, not LSP standard methods. - -### 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. - -#### Per-wiki HTML options - -| Key | Default | Description | -|---|---|---| -| `html_path` | `/_html` | output directory | -| `template_path` | `/_templates` | template lookup root | -| `template_default` | `default` | base template name | -| `template_ext` | `.tpl` | template extension | -| `template_date_format` | `%Y-%m-%d` | format passed to `{{date}}` | -| `css_name` | `style.css` | CSS file copied/created at export | -| `auto_export` | `false` | export on save | -| `html_filename_parameterization` | `false` | URL-safe slugs in output filenames | -| `exclude_files` | `[]` | globs to skip | - -#### Commands - -| Command | Behaviour | -|---|---| -| `nuwiki.export.currentToHtml` | render current page; honours `%template`, copies CSS if missing | -| `nuwiki.export.allToHtml` | export every page in the wiki | -| `nuwiki.export.allToHtmlForce` | `:VimwikiAll2HTML!` equivalent | -| `nuwiki.export.browse` | export current + open default browser to result | -| `nuwiki.export.rss` | emit `rss.xml` of recent diary entries (when diary enabled) | - -#### Template variables - -The `HtmlRenderer` template substitution gains: - -- `{{title}}` (already shipped) -- `{{content}}` (already shipped) -- `{{date}}` (formatted via `template_date_format`) -- `{{root_path}}` (relative path from output to `html_path` — for stylesheet - hrefs in nested pages) -- `{{toc}}` (rendered TOC for current page) - -### 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 - -```lua -require('nuwiki').setup({ - wikis = { - { - name = 'personal', - root = '~/vimwiki', - file_extension = '.wiki', - syntax = 'vimwiki', - diary_rel_path = 'diary', - html_path = '~/vimwiki/_html', - -- … any per-wiki option - }, - { - name = 'work', - root = '~/work-notes', - syntax = 'vimwiki', - }, - }, - log_level = 'warn', -}) -``` - -The v1.0 single-wiki shape continues to work; internally it desugars to -`wikis = [{ root = wiki_root, file_extension = file_extension, syntax = syntax }]`. - -#### Per-wiki context - -`WorkspaceIndex` becomes one per wiki. Backend holds: - -``` -indexes: Arc>>> -``` - -URI → wiki resolution walks the URI's filesystem path upward, matching -against each registered root. Cross-wiki links (interwiki) resolve through -the matching wiki's index. - -#### Custom commands - -| Command | Behaviour | -|---|---| -| `nuwiki.wiki.select` | client-side picker over `wikis`; resolves to a wiki id | -| `nuwiki.wiki.openIndex` | open the selected wiki's `index.wiki` | -| `nuwiki.wiki.tabOpenIndex` | open in new tab (client-side) | -| `nuwiki.wiki.listAll` | returns the registered wikis for editor UIs | - -### 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`. - -#### `:Vimwiki*` command compatibility - -`ftplugin/nuwiki.vim` defines every command listed in §4 of vimwiki's help, -mapped to the corresponding `executeCommand`: - -| Vimwiki command | nuwiki implementation | -|---|---| -| `:VimwikiIndex` `[count]` | `nuwiki.wiki.openIndex(count)` | -| `:VimwikiTabIndex` | tab-open variant | -| `:VimwikiUISelect` | `nuwiki.wiki.select` | -| `:VimwikiDiaryIndex` | `nuwiki.diary.openIndex` | -| `:VimwikiMakeDiaryNote` | `nuwiki.diary.openToday` | -| `:VimwikiMakeYesterdayDiaryNote` | `nuwiki.diary.openYesterday` | -| `:VimwikiMakeTomorrowDiaryNote` | `nuwiki.diary.openTomorrow` | -| `:VimwikiFollowLink` | client `vim.lsp.buf.definition()` | -| `:VimwikiGoBackLink` | client `` (jumplist) | -| `:VimwikiSplitLink` / `:VimwikiVSplitLink` | client-side `:split`/`:vsplit` + `definition` | -| `:VimwikiTabnewLink` / `:VimwikiTabDropLink` | client-side `:tabnew` + `definition` | -| `:VimwikiNextLink` / `:VimwikiPrevLink` | `nuwiki.cursor.nextLink` / `prevLink` | -| `:VimwikiGoto {name}` | `nuwiki.wiki.gotoPage(name)` | -| `:VimwikiDeleteFile` | server-side delete via `executeCommand` | -| `:VimwikiRenameFile` | client prompt → `workspace/rename` | -| `:VimwikiRemoveDone` | `nuwiki.list.removeDone` | -| `:VimwikiNextTask` | `nuwiki.list.nextTask` | -| `:Vimwiki2HTML` | `nuwiki.export.currentToHtml` | -| `:Vimwiki2HTMLBrowse` | `nuwiki.export.browse` | -| `:VimwikiAll2HTML[!]` | `nuwiki.export.allToHtml[Force]` | -| `:VimwikiRss` | `nuwiki.export.rss` | -| `:VimwikiToggleListItem` | `nuwiki.list.toggleCheckbox` | -| `:VimwikiToggleRejectedListItem` | `nuwiki.list.rejectCheckbox` | -| `:VimwikiListChangeLvl CMD` | `nuwiki.list.changeLevel` / `changeSymbol` | -| `:VimwikiSearch` / `:VWS` | client `:lvimgrep` wrapper over wiki root | -| `:VimwikiBacklinks` / `:VWB` | client `vim.lsp.buf.references()` | -| `:VimwikiTable [cols] [rows]` | `nuwiki.table.insert` | -| `:VimwikiTableMoveColumnLeft/Right` | `nuwiki.table.moveColumn` | -| `:VimwikiGenerateLinks` | `nuwiki.links.generate` | -| `:VimwikiDiaryGenerateLinks` | `nuwiki.diary.generateIndex` | -| `:VimwikiDiaryNextDay` / `:VimwikiDiaryPrevDay` | `nuwiki.diary.next/prev` | -| `:VimwikiTOC` | `nuwiki.toc.generate` | -| `:VimwikiCheckLinks` | `nuwiki.workspace.checkLinks` | -| `:VimwikiRebuildTags` | `nuwiki.tags.rebuild` | -| `:VimwikiSearchTags {tag}` | `nuwiki.tags.search` | -| `:VimwikiGenerateTagLinks` | `nuwiki.tags.generateLinks` | -| `:VimwikiColorize {color}` | `nuwiki.colorize` | -| `:VimwikiPasteLink` | `nuwiki.link.pasteWikilink` | -| `:VimwikiPasteUrl` | `nuwiki.link.pasteUrl` | - -Each `:Vimwiki*` form ships with a matching `:Nuwiki*` alias (P10 -resolution). - -#### Default keymaps - -`g:nuwiki_key_mappings` (Vim) / `mappings = { … }` (Neovim) controls -opt-in keymap registration. Default: enabled; group flags let users disable -the table/list/heading/diary subsets independently. - -Buffer-local maps mirror vimwiki: - -- `ww` / `wt` / `ws` / `wi` / - `ww` etc. — wiki navigation -- `` / `` / `` / `` / `` — link follow variants -- `` / `` / `` — link history + per-page link navigation -- `=` / `-` / `[[` / `]]` / `[=` / `]=` / `]u` / `[u` — header manipulation + - navigation (pure VimL/Lua, regex over current buffer — no LSP round-trip) -- `+` — `nuwiki.link.normalize` -- `` — `nuwiki.list.toggleCheckbox` -- `gnt` — `nuwiki.list.nextTask` -- `gl` / `gL` — checkbox remove (single / list) -- `gln` / `glp` — checkbox cycle up/down -- `gll` / `gLl` / `glh` / `gLh` — list level change (single / subtree) -- `glr` / `gLr` — list renumber -- `gl*` / `gl#` / `gl-` / `gl+` / `gl1` / `gla` / `glA` / `gli` / `glI` — - list symbol change (single / subtree variants `gL`*) -- `glx` — `:VimwikiToggleRejectedListItem` -- `gqq` / `gww` / `gq1` / `gw1` — table align -- `` / `` — table move column -- `` / `` — diary prev/next day -- `wh` / `whh` — HTML export current / browse -- `wc` — colorize (asks for colour) -- `wn` — goto/create new page -- `wd` / `wr` — delete / rename current page -- Mouse: `<2-LeftMouse>`, ``, ``, - `` (opt-in via `g:nuwiki_mouse_mappings`) - -#### Insert-mode behaviour - -- Table cells: ``/``/`` navigate cells, create rows - on overflow; implemented in Lua/VimL via a small autocmd -- Lists: `` inserts the next bullet/number; `` continues without - a new bullet; ``/`` indent/dedent; ``/`` - cycle the list symbol; `` toggles a list item on/off -- All gated by `b:did_ftplugin` and remappable - -#### Text objects - -`omap`/`xmap` defining: - -| Object | Selects | -|---|---| -| `ah` / `ih` | a header (with/without trailing blank lines + header line itself) | -| `aH` / `iH` | a header + all sub-headers ([count] climbs parent levels) | -| `a\` / `i\` | a table cell | -| `ac` / `ic` | a table column | -| `al` / `il` | a list item (with/without children) | - -Pure VimL/Lua — no LSP round-trip. Reuses our AST-shaped regexes. - -#### Folding - -Per P14 resolution; the spec'd default (subject to that decision): - -- `setlocal foldmethod=expr` + `foldexpr=NuwikiFold(v:lnum)` (or LSP - `foldingRange` driving it on Neovim 0.11+) -- Folds at heading boundaries; nested headings nest folds -- List-item folds when configured via `g:nuwiki_folding = 'list'` -- Custom variant via `g:nuwiki_folding = 'custom'` - -#### Health check additions - -`:checkhealth nuwiki` v1.1 reports: - -- LSP commands registered (per command name) -- Index loaded for each registered wiki -- Tags table built -- HTML output path writable -- Default keymaps installed (or disabled by config) - -### 12.11 Updated user-config schema - -```lua -require('nuwiki').setup({ - -- single-wiki shorthand (v1.0 compat) - wiki_root = '~/vimwiki', - file_extension = '.wiki', - syntax = 'vimwiki', - log_level = 'warn', - - -- or the v1.1 multi-wiki form - wikis = { - { - name = 'personal', - root = '~/vimwiki', - file_extension = '.wiki', - syntax = 'vimwiki', - diary_rel_path = 'diary', - diary_frequency = 'daily', -- daily | weekly | monthly | yearly - diary_start_week_day = 'monday', - template_path = '~/vimwiki/_templates', - template_default = 'default', - template_ext = '.tpl', - css_name = 'style.css', - html_path = '~/vimwiki/_html', - auto_export = false, - auto_toc = false, - auto_tags = false, - exclude_files = {}, - bullet_types = { '-', '*', '#' }, - listsyms = ' .oOX', - listsym_rejected = '-', - listsyms_propagate = true, - color_dic = { red = 'red', green = 'green' }, - }, - }, - - -- workspace-wide behaviour - diagnostic = { - link_severity = 'warn', -- off | hint | warn | error - }, - mappings = { - enabled = true, - table_editing = true, - list_editing = true, - header_nav = true, - diary = true, - html_export = true, - mouse = false, - }, - folding = 'syntax', -- off | syntax | list | custom -}) -``` - -### 12.12 Updated §9 syntax checklist (additions) - -The following items extend the v1.0 checklist: - -#### Tags -- [ ] Inline tag sequence: `:tag-one:tag-two:` -- [ ] File-level tag (line 1–2 of a wiki page) -- [ ] Header-level tag (within 2 lines after a heading) -- [ ] Tag-as-anchor target (`[[Page#some-tag]]`) -- [ ] Hex-colour code rendering in code blocks (`` `#ffe119` ``) - -#### Tables (additions) -- [ ] Column-alignment markers in header separator: `|:--|` / `|:--:|` / `|--:|` - -#### Lists (parser extensions) -- [ ] Multi-line list item (indented continuation lines) -- [ ] Mixed-symbol lists at the same level - -### 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 -existing crates; the test suite grows accordingly. - ---- - -## 13. Pending Implementation - -Items specified in earlier phases but not yet implemented. Tracked here -so a future audit doesn't have to re-derive the scope from the per-phase -prose. Each entry includes the originating SPEC section, why it was -deferred, and a sketch of what the implementation needs. - -### 13.1 Phase 14 — list/table/link/colorize commands ✅ - -All eleven commands deferred from Phase 14 are now shipped: - -| Command | Status | Notes | -|---|---|---| -| `nuwiki.list.changeSymbol` | ✅ done | `ops::change_symbol_edit` + `parse_symbol` + `render_marker` (incl. alpha + roman variants). | -| `nuwiki.list.changeLevel` | ✅ done | ±2-space indent per delta; `whole_subtree` walks descendants; dedent clamps at column 0. | -| `nuwiki.list.renumber` | ✅ done | Re-sequences numeric markers in the current list, or every list when `whole_file = true`. | -| `nuwiki.list.removeDone` | ✅ done | Strips `[X]` / `[-]` items + their sublists; optional `position` scopes to one item. | -| `nuwiki.table.align` | ✅ done | Width-per-column pass + row re-emit. Re-inserts the dropped `|---|---|` separator after the header. | -| `nuwiki.table.moveColumn` | ✅ done | Swap column with neighbour (`dir: "left" \| "right"`); widths swap alongside. | -| `nuwiki.table.insert` | ✅ done | Pure templating — `cols × rows` blank cells + header separator. | -| `nuwiki.link.normalize` | ✅ done | Client-side word-wrap (Lua/VimL). Mirrors the `` two-step wrap step. | -| `nuwiki.link.pasteWikilink` | ✅ done | Server-side; inserts `[[]]` at cursor. | -| `nuwiki.link.pasteUrl` | ✅ done | Server-side; inserts `.html` (subdir-aware). | -| `nuwiki.colorize` | ✅ done | Client-side wrap in `` (matches vimwiki default template). | - -`color_dic` integration with the `ColorNode` HTML renderer also -landed alongside `colorize` — the dict maps vimwiki colour-tag names -to CSS values; missing names fall back to `class="color-"`. - -### 13.2 Phase 13 — `will_rename` / `will_delete` capability - -The Phase 17 backfill (commit `ad1b55a`) advertised `did_rename` and -`did_delete` but intentionally left `will_rename` / `will_delete` off. -Those variants return a `WorkspaceEdit` synchronously from the server — -useful for, e.g., rewriting all incoming `[[Page]]` links *before* a -client-initiated file rename commits to disk. Implementation would -share most of the cross-doc rewrite logic from -`rename::compute_rename` (Phase 13) but at a different LSP entry point. -Deferred because the value proposition over the existing -`workspace/rename` flow is marginal and the editor-side support is -inconsistent across clients. - -### 13.3 v1.1 status - -All numbered phases through 19 are shipped. The remaining work is -the §13.1 deferred Phase 14 command surface (table/list rewriters, -link helpers, colorize) and the small follow-ups noted under §13.2 / -§13.4. - -Earlier v1.1 phases now shipped: - -- Phase 17 (HTML export commands) — `currentToHtml`, - `allToHtml[Force]`, `browse`, `rss`, template-file + fallback - resolution, `{{date}}` / `{{root_path}}` / `{{toc}}` / `{{css}}` - template variables, and `did_save` auto-export when - `auto_export = true`. `color_dic` integration with the `ColorNode` - renderer is still open (only useful once §13.1 `colorize` lands), - as is the §13.1 `link.pasteUrl` follow-up that depends on the same - export config. -- Phase 18 (multi-wiki) — `wikis = [...]` config shape was already - accepted by Phase 11 plumbing; this phase added the cross-wiki - interwiki resolver (`Backend::resolve_target_uri` / - `heading_range_for` route `[[wiki:Page]]` and - `[[wn.:Page]]` to the destination wiki's index) and four - picker commands: `nuwiki.wiki.listAll`, `nuwiki.wiki.select`, - `nuwiki.wiki.openIndex`, `nuwiki.wiki.tabOpenIndex`, plus - `nuwiki.wiki.gotoPage` for the `:VimwikiGoto` compat surface. -- Phase 19 (editor glue v2) — LSP `textDocument/foldingRange` - provider (`folding::folding_ranges`) with heading-block + top-level - list semantics; advertised via `folding_range_provider`. A pure - Lua `foldexpr` fallback ships in `lua/nuwiki/folding.lua` for - clients without `foldingRange` support. `ftplugin/nuwiki.vim` - defines the full `:Vimwiki*` compat surface plus `:Nuwiki*` - aliases — every command that maps to a server-side `executeCommand` - is wired; §13.1-deferred commands stub out with a "not yet - implemented" notification so users get a clear signal. Default - buffer-local keymaps cover the working subset (checkbox toggle, - next task, heading promote/demote via `g=`/`g-`, diary navigation - via ``/``, HTML export under `wh*`). A - heading text object (`ah`/`ih`) ships now; `aH`/`iH`/`a\`/`i\`/ - `ac`/`ic`/`al`/`il` land once the §13.1 table/list rewriters - arrive. - -### 13.4 §9 syntax checklist status - -The §9 checklist tracks every vimwiki-syntax surface. Items currently -marked `[ ]` there are parser-level coverage assertions, not missing -commands — they're driven by the existing test suite, not by Phase -14+ command work. Don't confuse "syntax checklist unchecked" with -"feature missing" — most of those rows are exercised by the parser -tests under `crates/nuwiki-core/tests/` and just haven't had their -checkboxes ticked. - ---- - diff --git a/development/ONBOARDING.md b/development/ONBOARDING.md index fb9b3d2..c9370cb 100644 --- a/development/ONBOARDING.md +++ b/development/ONBOARDING.md @@ -70,6 +70,7 @@ nuwiki/ │ ├── development/ # Developer-only tooling and docs (not shipped) │ ├── ONBOARDING.md # This file +│ ├── SPEC.md # Technical reference (architecture, LSP, commands) │ ├── nuwiki-architecture.html │ ├── start-nvim.sh # Launch Neovim against a generated sample wiki │ ├── start-vim.sh # Launch Vim against a generated sample wiki diff --git a/development/SPEC.md b/development/SPEC.md new file mode 100644 index 0000000..15f52cc --- /dev/null +++ b/development/SPEC.md @@ -0,0 +1,339 @@ +# nuwiki — Technical Reference + +A developer-facing description of how nuwiki is built and what it implements. +For day-one setup see [ONBOARDING.md](ONBOARDING.md); for user-facing help see +`doc/nuwiki.txt` (`:h nuwiki`). + +nuwiki is a vimwiki-compatible Vim/Neovim plugin backed by a Rust language +server. The editor layers are thin clients; all parsing, navigation, and +editing logic lives in the Rust crates and is reached over LSP. + +| Property | Value | +|---|---| +| License | Dual MIT / Apache-2.0 | +| MSRV | Rust 1.83 | +| Edition / resolver | 2021 / v2 | +| Repo | `https://code.gfran.co/gffranco/nuwiki` | +| VCS / CI | Gitea + Gitea Actions | +| LSP library | `tower-lsp` (tokio, stdio transport) | +| Min editors | Neovim 0.11+, Vim 9.1+ | + +--- + +## 1. Architecture + +### Layer model + +``` +Editor client (VimL / Lua) — pure wiring, no logic + │ LSP over stdio + ▼ +nuwiki-lsp — protocol bridge, executeCommand, document store, config + │ + ▼ +nuwiki-core — lexer → parser → AST → renderer (no editor deps) +``` + +### Crate dependency rules + +``` +nuwiki-ls → nuwiki-lsp → nuwiki-core +``` + +- `nuwiki-core` never depends on `nuwiki-lsp` / `nuwiki-ls`. +- `nuwiki-lsp` never depends on `nuwiki-ls`. +- The VimL and Lua layers contain no logic — every command body issues a + `workspace/executeCommand` or a built-in LSP request. + +### Repository layout + +``` +crates/ + nuwiki-core/ # parser, AST, renderer — editor-independent + src/ + ast/ # block.rs inline.rs link.rs span.rs visit.rs + syntax/ # registry.rs + vimwiki/{lexer,parser}.rs + render/ # html.rs + date.rs # diary period math (no chrono dependency) + nuwiki-lsp/ # LSP backend + src/ + lib.rs # Backend, capabilities, textDocument handlers + commands.rs # executeCommand dispatcher + pure edit ops + config.rs # Config / WikiConfig / HtmlConfig + diagnostics.rs index.rs nav.rs rename.rs semantic_tokens.rs + diary.rs export.rs edits.rs folding.rs wiki.rs + nuwiki-ls/ # thin binary: starts the stdio server + +plugin/nuwiki.vim # universal entry point (detects Vim vs Neovim) +lua/nuwiki/ # Neovim layer: init, config, lsp, keymaps, commands, + # folding, install +autoload/nuwiki/ # Vim layer: lsp.vim, commands.vim +ftdetect/ ftplugin/ syntax/ # filetype detection, buffer setup, fallback HL +doc/nuwiki.txt # :help +scripts/download_bin.vim # plugin-runtime binary download (build hooks) +development/ # dev-only tooling + this document (not shipped) +.gitea/workflows/ # ci.yaml, release.yaml +``` + +--- + +## 2. nuwiki-core + +### AST + +Every node carries a `Span { start, end }` of `Position { line, column, offset }` +(0-indexed; column and offset are byte-based). Spans drive diagnostics, +semantic tokens, and go-to-definition. + +**Block nodes:** `Heading` (level 1–6, `centered`), `Paragraph`, +`HorizontalRule`, `Blockquote` (recursive), `Preformatted` (optional +language), `MathBlock` (optional environment), `List`, `DefinitionList`, +`Table`, `Comment`, `Tag`, `Error` (resilient parse failure). + +**Inline nodes:** `Text`, `Bold`, `Italic`, `BoldItalic`, `Strikethrough`, +`Code`, `Superscript`, `Subscript`, `MathInline`, `Keyword`, `Color`, +`WikiLink`, `ExternalLink`, `Transclusion`, `RawUrl`. + +**Enums:** +- `ListSymbol` = `Dash | Star | Hash | Numeric | NumericParen | AlphaParen | + AlphaUpperParen | RomanParen | RomanUpperParen` +- `CheckboxState` = `Empty | Quarter | Half | ThreeQuarters | Done | Rejected` +- `Keyword` = `Todo | Done | Started | Fixme | Fixed | Xxx | Stopped` +- `LinkKind` = `Wiki | Interwiki | Diary | File | Local | Raw | AnchorOnly` +- `TagScope` = `File | Heading(idx) | Standalone` + +`PageMetadata` carries `title`, `nohtml`, `template`, `date`, and aggregated +file-level `tags`. + +### Lexer / parser + +- Two-pass lexer (block pass, then inline pass per block); syntax-specific + `VimwikiToken`s with byte-offset spans, collected eagerly into a + `TokenStream`. +- Hand-rolled recursive-descent parser (not a combinator library). Resilient: + malformed input yields an `ErrorNode` and parsing continues — a document + never fails to parse. +- Full re-parse on every `didOpen` / `didChange` (no incremental parsing). + +### Syntax plugin interface + +Syntaxes register against a `SyntaxRegistry` keyed by id and file extension: + +``` +SyntaxPlugin { id, display_name, file_extensions, lexer, parser } +``` + +vimwiki (`.wiki`) is the only registered syntax; the registry exists so a +markdown plugin can be added without touching the LSP or editor layers. + +### Renderer + +`Renderer` trait writes to a `dyn Write`; `HtmlRenderer` is the implementation. +Link resolution is injected as a callback. Template substitution supports +`{{title}}`, `{{content}}`, `{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}`. +`color_dic` maps colour-tag names to CSS values, falling back to +`class="color-"`. + +--- + +## 3. LSP server + +### Advertised capabilities + +| Capability | Notes | +|---|---| +| `textDocumentSync` | full sync (open/close/change/save) | +| `semanticTokensProvider` | full + range; custom vimwiki token legend | +| `documentSymbolProvider` / `workspaceSymbolProvider` | outline + workspace search (tags included) | +| `definitionProvider` / `referencesProvider` | link follow + backlinks | +| `hoverProvider` | link preview | +| `completionProvider` | trigger `[` | +| `renameProvider` | cross-document link rewrite | +| `foldingRangeProvider` | heading blocks + top-level lists | +| `executeCommandProvider` | see command surface below | +| `workspace.fileOperations` | `did_rename` + `did_delete` for `**/*.wiki`, `**/*.md` | +| `positionEncoding` | UTF-8 when client supports LSP 3.17+, else UTF-16 | + +Workspace indexing runs as a background tokio task reporting via +`window/workDoneProgress`; nav features answer with partial data until the +scan completes. The document store is an `Arc>` +holding text + AST + version. + +### executeCommand surface + +All editing commands return a `WorkspaceEdit` (applied via +`workspace/applyEdit`) built through `edits::WorkspaceEditBuilder`, so the AST +stays the source of truth. Navigation commands return a `Location`. + +| Namespace | Commands | +|---|---| +| `list` | `toggleCheckbox` `cycleCheckbox` `rejectCheckbox` `nextTask` `removeDone` `renumber` `changeSymbol` `changeLevel` | +| `table` | `insert` `align` `moveColumn` | +| `heading` | `addLevel` `removeLevel` | +| `link` | `pasteWikilink` `pasteUrl` | +| `toc` / `links` | `toc.generate` · `links.generate` | +| `workspace` | `checkLinks` `findOrphans` | +| `diary` | `openToday` `openYesterday` `openTomorrow` `openIndex` `generateIndex` `next` `prev` `listEntries` `openForDate` | +| `tags` | `search` `generateLinks` `rebuild` | +| `export` | `currentToHtml` `allToHtml` `allToHtmlForce` `browse` `rss` | +| `wiki` | `listAll` `select` `openIndex` `tabOpenIndex` `gotoPage` | +| `file` | `delete` | + +`workspace/rename` emits a `RenameFile` op plus `[[A]]` → `[[B]]` rewrites +across every linking page (anchors and descriptions preserved). Closed +documents are re-parsed on demand so cross-document edits use current source. + +### Diagnostics + +Composable collector chains parse errors (`ErrorNode` walk) and broken-link +checks (wiki target / anchor / `file:` / `local:` existence). Per-source +severity is config-driven (`diagnostic.link_severity`: `off|hint|warn|error`); +raw and external URLs are never diagnosed. + +--- + +## 4. Editor integration + +### Installation + +Pre-built binaries are published as Gitea release assets +(`nuwiki-ls-{version}-{target}.tar.gz`) and fetched at install time by +`lua/nuwiki/install.lua` (Neovim) or `scripts/download_bin.vim` (Vim build +hooks), installed to `{plugin_dir}/bin/nuwiki-ls`. Falls back to +`cargo build --release` when download fails or +`g:nuwiki_build_from_source = 1`. + +`plugin/nuwiki.vim` dispatches: Neovim → `require('nuwiki').setup()`; Vim → +`nuwiki#lsp#start()` (prefers `vim-lsp`, falls back to `coc.nvim`). On Neovim +the server is registered via `vim.lsp.config{}` + `vim.lsp.enable`, with a +`vim.lsp.start` autocmd fallback. + +### Configuration + +Single-wiki (v1.0) and multi-wiki forms both work; the scalar form desugars to +a one-entry `wikis` list. + +```lua +require('nuwiki').setup({ + -- single-wiki shorthand + wiki_root = '~/vimwiki', file_extension = '.wiki', syntax = 'vimwiki', + log_level = 'warn', + + -- or multi-wiki + wikis = { + { name = 'personal', root = '~/vimwiki', diary_rel_path = 'diary', + diary_frequency = 'daily', html = { html_path = '~/vimwiki/_html' } }, + }, + + diagnostic = { link_severity = 'warn' }, + mappings = { -- all default true except mouse + enabled = true, wiki_prefix = true, links = true, lists = true, + headers = true, table_editing = true, diary = true, html_export = true, + text_objects = true, mouse = false, + }, + folding = 'lsp', -- lsp | expr | off +}) +``` + +Per-wiki keys include `index`, `diary_rel_path`/`diary_index`/ +`diary_frequency`/`diary_start_week_day`/`diary_sort`/`diary_caption_level`/ +`diary_header`, `listsyms`/`listsyms_propagate`/`list_margin`, +`links_space_char`, `nested_syntaxes`, `auto_toc`, `maxhi`, and an `html` +table (`html_path`, `template_path`/`template_default`/`template_ext`/ +`template_date_format`, `css_name`, `auto_export`, +`html_filename_parameterization`, `exclude_files`, `color_dic`). + +### Command surface + +`ftplugin/vimwiki.vim` (Vim) and `lua/nuwiki/commands.lua` (Neovim) define the +vimwiki command set with `:Vimwiki*` names plus `:Nuwiki*` aliases — index/tab +index/UI select, diary (note/yesterday/tomorrow/next/prev/index/generate), +link follow/backlinks/next/prev, goto/delete/rename, list toggle/reject/ +remove-done, TOC, generate/check links, find orphans, tags rebuild/search/ +generate, HTML 2HTML/browse/all/rss, table insert/move/colorize, and paste +link/url. Search uses `lvimgrep` and backlinks/follow use built-in LSP +requests; the rest route to `executeCommand`. + +### Default keymaps + +Buffer-local, gated per subgroup by `mappings.*`: + +- **Wiki** `ww`/`wt`/`ws`/`wi`, `ww|y|t|m|i`, + `wn`/`wd`/`wr`/`wc` +- **Links** `` (follow / wrap-word), ``/``/`` + (split/vsplit/tab), `` (back), ``/`` (next/prev link), `+` + (wrap as wikilink) +- **Lists** `` (toggle), `gnt` (next task), `gln`/`glp` (cycle + forward/back), `glx` (reject), `gll`/`glh` + `gLl`/`gLh` (indent/dedent, + item vs subtree), `glr`/`gLr` (renumber list/all), `gl`/`gL` + (remove done, list/whole-doc), `o`/`O` (open with bullet); insert-mode + ``/``, ``/``, ``; smart ``/`` +- **Headers** `=`/`-` (deeper/shallower), `]]`/`[[` (next/prev), `]=`/`[=` + (sibling), `]u`/`[u` (parent) — pure VimL/Lua, no LSP round-trip +- **Tables** `gqq`/`gww` (align), ``/`` (move column) +- **Diary** ``/`` (next/prev entry) +- **HTML** `wh`/`whh`/`wha` (export/browse/all) +- **Mouse** (opt-in) `<2-LeftMouse>` follow + shift/ctrl variants + +**Text objects:** `ah`/`ih` (heading section), `aH`/`iH` (heading + subtree), +`al`/`il` (list item), `a\`/`i\` (table cell), `ac`/`ic` (table column). Pure +VimL/Lua. + +### Folding + +Primary: LSP `textDocument/foldingRange` (`folding::folding_ranges`, heading +blocks + top-level lists). Fallback: a pure Lua `foldexpr` in +`lua/nuwiki/folding.lua` for clients without `foldingRange`. + +### Health check + +`:checkhealth nuwiki` verifies the binary exists and responds to `--version`, +the LSP client is attached, `.wiki` filetype detection works, and reports +registered commands and per-wiki index state. + +--- + +## 5. Vimwiki syntax support + +Parsed and highlighted (custom semantic-token legend; `syntax/nuwiki.vim` +provides a no-LSP fallback): + +- **Typefaces** bold `*…*`, italic `_…_`, bold-italic, strikethrough `~~…~~`, + inline code, super `^…^` / sub `,,…,,`, keywords (TODO/DONE/STARTED/FIXME/ + FIXED/XXX/STOPPED) +- **Links** plain/described/subdir/root/absolute wikilinks, `[[dir/]]`, + interwiki (`wiki1:` / `wn.Name:`), diary, anchor-only and `Page#anchor`, + raw URLs, `file:`/`local:`, transclusion `{{url|alt|attrs}}` +- **Headers** levels 1–6, centered +- **Lists** unordered (`-` `*` `#`), ordered (`1.` `1)` `a)` `A)` `i)` `I)`), + nested/mixed, multi-line items, definition lists, all six checkbox states +- **Tables** `|`-delimited, header separator, column alignment markers, + colspan `>` / rowspan `\/`, inline formatting in cells +- **Blocks** preformatted `{{{ … }}}` (optional language), inline math `$…$`, + block math `{{$ … }}$` (optional environment), blockquotes, comments + (`%% …`), horizontal rule `----` +- **Tags** `:tag1:tag2:` (file / heading / standalone scope), tag-as-anchor +- **Placeholders** `%title` `%nohtml` `%template` `%date` + +--- + +## 6. CI/CD + +`.gitea/workflows/ci.yaml` runs on every push and PR: + +| Job | Command | +|---|---| +| fmt | `cargo fmt --all -- --check` | +| clippy | `cargo clippy --workspace --all-targets -- -D warnings` | +| test | `cargo test --workspace --all-targets` | +| keymaps | `development/tests/test-keymaps.sh` (Neovim 0.11) + `test-keymaps-vim.sh` (Vim) | + +`.gitea/workflows/release.yaml` triggers on `v*` tags: cross-compiles the four +Linux targets (gnu/musl × x86_64/aarch64) via `cross`, packages `.tar.gz`, and +creates a Gitea release using `RELEASE_TOKEN`. macOS and Windows binaries are +built manually and attached to the same release. crates.io publishing is not +wired up. + +A change is semver-breaking if it alters `nuwiki-core` AST nodes, the +`SyntaxPlugin`/`Renderer` traits, the user-config schema, or removes an LSP +capability.