2026-05-10 16:01:58 +00:00
# nuwiki — Project Specification
2026-05-11 01:19:36 +00:00
> 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.
2026-05-10 16:01:58 +00:00
---
## 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
2026-05-11 01:19:36 +00:00
- Multi-wiki configurations * (addressed in v1.1, see §12) *
2026-05-10 16:01:58 +00:00
- Incremental / tree-sitter parsing
- Browser-based or WASM build
2026-05-11 01:19:36 +00:00
**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
2026-05-10 16:01:58 +00:00
---
## 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 |
2026-05-10 17:58:41 +00:00
| 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. |
2026-05-10 16:01:58 +00:00
| 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: 1– 6 | 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
2026-05-11 00:47:40 +00:00
- 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.
2026-05-10 16:01:58 +00:00
### 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 |
2026-05-11 00:47:40 +00:00
| ~~`CARGO_REGISTRY_TOKEN`~~ | ~~crates.io API token for publishing `nuwiki-core`~~ — deferred (see §8.2) |
2026-05-10 16:01:58 +00:00
### 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 <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 |
2026-05-11 01:19:36 +00:00
| * * — 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 |
2026-05-10 16:01:58 +00:00
---
## 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) ** | |
2026-05-10 16:25:24 +00:00
| ~~P3~~ | ~~**String representation in AST**~~ | ~~Phase 1~~ | ✅ * * `String` (owned)** | |
| ~~P4~~ | ~~**Visitor pattern**~~ | ~~Phase 1~~ | ✅ **Defined in Phase 1 ** | Open-recursion `Visitor` trait + `walk_*` helpers |
2026-05-11 00:38:09 +00:00
| ~~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` . |
2026-05-10 21:14:01 +00:00
| ~~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. |
2026-05-11 00:27:06 +00:00
| ~~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` . |
2026-05-11 00:47:40 +00:00
| ~~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. |
2026-05-11 01:19:36 +00:00
| **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 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.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.