Files
nuwiki/SPEC.md
T
gffranco f435679814
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m18s
CI / cargo test (push) Successful in 1m27s
spec: define v1.1 — full vimwiki replacement (Phases 11–18)
v1.0 shipped parsing + highlighting + navigation; the gap-analysis vs
github.com/vimwiki/vimwiki shows nuwiki covers ~95% of the syntax but
0% of the daily-authoring command surface (≈40 :Vimwiki* commands and
≈60 keymaps), the diary, tags, link health, HTML export commands, and
multi-wiki. v1.1 closes that gap without architectural changes — every
new editing operation lands as an LSP executeCommand returning a
WorkspaceEdit, so nuwiki-core stays the single source of truth.

Spec additions:

- Status line: v1.0 shipped; v1.1 in design.
- §1: v1.1 scope summarised; multi-wiki moved out of "non-goals (v1.0)"
  and into v1.1.
- §10: 8 new phase rows (11–18) under a v1.1 divider.
  - 11 Tags, 12 Workspace edits + executeCommand, 13 List/table edit
    commands, 14 Link health + TOC/index generation, 15 Diary,
    16 HTML export commands, 17 Multi-wiki, 18 Editor glue v2.
- §11: 8 new pending decisions (P10–P17): command namespace, tag
  syntax, list-edit transport, multi-wiki config shape, folding
  mechanism, diary path scheme, v1.0 config back-compat, markdown
  syntax inclusion.
- §12: new top-level v1.1 plan with 12 subsections covering stability
  commitment, tags spec (AST + index), workspace-edit semantics,
  ~15 nuwiki.* edit commands, link-health diagnostics, diary path
  conventions + commands, HTML export commands + per-wiki options,
  multi-wiki schema, full editor-glue v2 (:Vimwiki* compatibility
  table, default keymaps, text objects, folding, insert-mode behaviour,
  health-check additions), updated user-config schema example,
  §9 syntax checklist additions (tags, hex colour, table alignment,
  multi-line lists), CI/CD notes.

v1.0 stability is preserved: AST + Visitor + LSP capabilities +
HtmlRenderer output + v1.0 config keys are all backward-compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:19:36 +00:00

1141 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# nuwiki — Project Specification
> Last updated: 2026-05-11
> Status: v1.0 (Phases 010) shipped; v1.1 (Phases 1118) 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<BlockNode>
metadata: PageMetadata -- title, nohtml, template, date
```
#### Block Nodes
```
HeadingNode level: 16 | children: Vec<InlineNode> | centered: bool
ParagraphNode children: Vec<InlineNode>
HorizontalRuleNode
BlockquoteNode children: Vec<BlockNode>
PreformattedNode content: String | language: Option<String>
MathBlockNode content: String | environment: Option<String>
ListNode ordered: bool | symbol: ListSymbol | items: Vec<ListItemNode>
DefinitionListNode items: Vec<DefinitionItemNode>
TableNode rows: Vec<TableRowNode> | has_header: bool
CommentNode content: String
ErrorNode raw: String | message: String -- resilient parse failure
```
#### Inline Nodes
```
TextNode content: String
BoldNode children: Vec<InlineNode>
ItalicNode children: Vec<InlineNode>
BoldItalicNode children: Vec<InlineNode>
StrikethroughNode children: Vec<InlineNode>
CodeNode content: String
SuperscriptNode children: Vec<InlineNode>
SubscriptNode children: Vec<InlineNode>
MathInlineNode content: String
KeywordNode keyword: Keyword -- TODO|DONE|STARTED|FIXME|FIXED|XXX
ColorNode color: String | children: Vec<InlineNode>
WikiLinkNode target: LinkTarget | description: Option<Vec<InlineNode>>
ExternalLinkNode url: String | description: Option<Vec<InlineNode>>
TransclusionNode url: String | alt: Option<String> | attrs: HashMap<String,String>
RawUrlNode url: String
```
#### Supporting Types
```
ListItemNode
symbol: ListSymbol
level: usize
checkbox: Option<CheckboxState>
children: Vec<InlineNode>
sublist: Option<ListNode>
DefinitionItemNode
term: Option<Vec<InlineNode>>
definitions: Vec<Vec<InlineNode>>
TableRowNode cells: Vec<TableCellNode> | is_header: bool
TableCellNode children: Vec<InlineNode> | col_span: bool | row_span: bool
LinkTarget
kind: LinkKind -- Wiki|Interwiki|Diary|File|Local|Raw|AnchorOnly
path: Option<String>
wiki_index: Option<usize>
wiki_name: Option<String>
anchor: Option<String>
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<Token>` 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<DashMap<Url, DocumentState>>
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 16: `= 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 <text>`
- [ ] `%nohtml`
- [ ] `%template <name>`
- [ ] `%date <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 | 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. 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 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. |
---
## 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 Tags (Phase 11)
#### 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<String>
scope: TagScope -- File | Heading(idx) | Standalone
PageMetadata (extend)
tags: Vec<String> -- 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>
TagInfo
name: String
scope: TagScope
span: Span
```
Plus a `tags_by_name: HashMap<String, Vec<(Url, Span)>>` 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.3 Workspace edits + `executeCommand` (Phase 12)
#### 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.4 List & table editing commands (Phase 13)
| 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.5 Link health + TOC/index generation (Phase 14)
#### 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<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)
#### Path conventions
```
<wiki_root>/<diary_rel_path>/ # 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.7 HTML export commands (Phase 16)
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` | `<wiki_root>/_html` | output directory |
| `template_path` | `<wiki_root>/_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.8 Multi-wiki (Phase 17)
#### 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<DashMap<WikiId, Arc<RwLock<WorkspaceIndex>>>>
```
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.9 Editor glue v2 (Phase 18)
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 `<C-o>` (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:
- `<Leader>ww` / `<Leader>wt` / `<Leader>ws` / `<Leader>wi` /
`<Leader>w<Leader>w` etc. — wiki navigation
- `<CR>` / `<S-CR>` / `<C-CR>` / `<M-CR>` / `<C-S-CR>` — link follow variants
- `<Backspace>` / `<Tab>` / `<S-Tab>` — 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`
- `<C-Space>``nuwiki.list.toggleCheckbox`
- `gnt``nuwiki.list.nextTask`
- `gl<Space>` / `gL<Space>` — 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
- `<A-Left>` / `<A-Right>` — table move column
- `<C-Up>` / `<C-Down>` — diary prev/next day
- `<Leader>wh` / `<Leader>whh` — HTML export current / browse
- `<Leader>wc` — colorize (asks for colour)
- `<Leader>wn` — goto/create new page
- `<Leader>wd` / `<Leader>wr` — delete / rename current page
- Mouse: `<2-LeftMouse>`, `<S-2-LeftMouse>`, `<C-2-LeftMouse>`,
`<RightMouse><LeftMouse>` (opt-in via `g:nuwiki_mouse_mappings`)
#### Insert-mode behaviour
- Table cells: `<CR>`/`<Tab>`/`<S-Tab>` navigate cells, create rows
on overflow; implemented in Lua/VimL via a small autocmd
- Lists: `<CR>` inserts the next bullet/number; `<S-CR>` continues without
a new bullet; `<C-T>`/`<C-D>` indent/dedent; `<C-L><C-J>`/`<C-L><C-K>`
cycle the list symbol; `<C-L><C-M>` 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.10 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.11 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 12 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.12 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.