Files
nuwiki/SPEC.md
T
gffranco 63e5b6d514
CI / cargo fmt --check (push) Successful in 21s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m18s
phase 19: editor glue v2
Server-side:
- New `folding.rs` module with `folding_ranges(ast, total_lines)`.
  Emits one fold per top-level heading (start → line before the
  next same-or-higher-level heading, or EOF) plus one per top-level
  list block. Nested headings fold inside their parent thanks to the
  level-aware end-line computation; sublists fold implicitly via
  their parent item's source span. `FoldingRangeKind::Region` is set
  on every fold so collapsing UIs render them as section folds.
- `Backend::folding_range` handler wires the module into
  `textDocument/foldingRange`; `ServerCapabilities.folding_range_provider`
  advertises it.
- `folding::line_count` exposed for the handler and tests; treats a
  trailing newline as a virtual empty line (the line LSP positions
  use for EOF).

Editor glue (Neovim primary, Vim minimal):
- `lua/nuwiki/commands.lua` — async `workspace/executeCommand`
  wrappers for every server command shipped through Phase 18. The
  open-* family handles `{ uri }` responses by opening the file
  (with `tab` / `split` variants). `check_links` and `find_orphans`
  hand results to `setqflist` + `:copen` for quickfix-style review.
  §13.1-deferred commands (`list.changeSymbol`, table rewriters,
  `link.pasteWikilink/pasteUrl`, `colorize`) stub out with
  `vim.notify` so users get a clear "not yet implemented" signal
  instead of an LSP "unknown command" error.
- `lua/nuwiki/keymaps.lua` — buffer-local default mappings. Subgroups
  (`list_editing`, `header_nav`, `diary`, `html_export`,
  `text_objects`) flip independently via the new
  `mappings.<group>` config. Heading promote/demote uses `g=`/`g-`
  to avoid clobbering Vim's built-in `=`/`-` operators.
- `lua/nuwiki/textobjects.lua` — `ah`/`ih` (around/inside heading)
  using a buffer scan for the heading-block boundary. The four
  remaining text objects from SPEC §12.10 wait until §13.1 lands
  the table/list rewriters they share infrastructure with.
- `lua/nuwiki/folding.lua` — pure regex `foldexpr` + `foldtext`
  fallback for clients without `foldingRange`. Same heading-block
  model as the server.
- `lua/nuwiki/ftplugin.lua` — single per-buffer attach entry point.
  `folding = 'lsp'` (default) uses Neovim 0.11+'s `vim.lsp.foldexpr`,
  falling back to the regex on older versions; `'expr'` forces the
  fallback; `'off'` skips folding setup.
- `lua/nuwiki/config.lua` — extends defaults with `mappings = {...}`
  (P10 keymap layer) and `folding` (P14 resolved).
- `ftplugin/nuwiki.vim` — declares every `:Vimwiki*` / `:Nuwiki*`
  command from SPEC §12.10 by inlining `lua require(...).fn()`
  bodies (no script-local function indirection so commands stay
  callable after re-source). Plain-Vim users get only the buffer
  options; they're expected to drive the LSP via vim-lsp / coc's
  built-in commands.

Health check (§12.10 additions):
- `:checkhealth nuwiki` now reports the count of `executeCommand`
  entries the server advertises, whether `foldingRange` capability
  was negotiated, and whether the configured HTML output directory
  exists + is writable. Default keymap subgroup status is also
  surfaced.

Tests: 8 new in `phase19_folding.rs` covering empty docs, single
heading → EOF, sibling headings each getting their own fold,
nested heading bounded by parent, top-level list folds, single-line
heading non-fold, fold-kind classification, and `line_count`
semantics. Total 377 tests pass; fmt + clippy clean.

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

1438 lines
56 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 | 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 `<C-Space>`. |
| P13 | **Multi-wiki config shape** | Phase 18 | Lua list (Neovim-native) · JSON config file (cross-editor) | Lua is ergonomic for Neovim; JSON works for Vim + arbitrary clients. |
| ~~P14~~ | ~~**Folding mechanism**~~ | ~~Phase 19~~ | ✅ **LSP `textDocument/foldingRange` (primary) + `foldexpr` fallback** | Server already has the AST + heading boundaries — duplicating that knowledge as regex in VimL would be lossy. `foldexpr` ships in `ftplugin` as a fallback for when the server hasn't attached. |
| P15 | **Diary path scheme** | Phase 16 | Fixed `<wiki_root>/diary/` · Per-wiki configurable `diary_rel_path` | Vimwiki has `diary_rel_path`; matching keeps migrations clean. |
| P16 | **Backwards-compat for v1.0 config** | Phase 18 | Preserve old single-`wiki_root` shape · Force migration to `wikis = [...]` | Single-wiki should keep working without re-config; `wiki_root` becomes sugar for `wikis = [{ root = wiki_root }]`. |
| P17 | **Markdown syntax in v1.1** | Phase 12+ | Include · Defer to v1.2 | Architecture already supports it (`SyntaxPlugin`); cost is parser work + per-syntax differences in commands. |
| P18 | **Server-config transport** | Phase 11 | `initializationOptions` only · `workspace/didChangeConfiguration` only · both | Both → simple boot path *and* live reload. Initialise at startup, accept change notifications afterwards. |
---
## 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
1219 mechanical instead of full of "oh we also need to refactor X".
#### 12.2.1 Server config receipt (P18)
Per P18: both `initializationOptions` and `workspace/didChangeConfiguration`
land in Phase 11.
- `initialize.initializationOptions` carries the full v1.1 config (§12.11
schema) verbatim. Parsed into a server-side `Config` struct.
- `workspace/didChangeConfiguration` re-applies the config for live reload.
- Every handler reads from a single `Arc<RwLock<Config>>` field on
`Backend`.
```rust
pub struct Config {
pub log_level: LogLevel,
pub diagnostic: DiagnosticConfig,
pub wikis: Vec<WikiConfig>, // always ≥ 1; v1.0-shape desugars
// …
}
impl Config {
pub fn from_init_options(value: serde_json::Value) -> Self;
pub fn from_v1_0_compat(legacy: LegacyConfig) -> Self; // P16 path
}
```
#### 12.2.2 `WorkspaceEditBuilder` + span helpers
New module `crates/nuwiki-lsp/src/edits.rs`:
```rust
pub fn text_edit_replace(span: Span, replacement: String, text: &str, utf8: bool) -> TextEdit;
pub fn text_edit_insert(pos: Position, content: String) -> TextEdit;
pub fn text_edit_delete(span: Span, text: &str, utf8: bool) -> TextEdit;
pub fn op_rename(old: Url, new: Url) -> DocumentChangeOperation;
pub fn op_delete(uri: Url) -> DocumentChangeOperation;
pub struct WorkspaceEditBuilder {
changes: HashMap<Url, Vec<TextEdit>>,
file_ops: Vec<DocumentChangeOperation>,
}
impl WorkspaceEditBuilder {
pub fn edit(&mut self, uri: Url, edit: TextEdit) -> &mut Self;
pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self;
pub fn build(self) -> WorkspaceEdit;
}
```
All v1.1 commands that produce `WorkspaceEdit` (Phases 1317) go through
the builder. Standardises UTF-8/UTF-16 conversion and document-change
ordering.
#### 12.2.3 Closed-doc loader
```rust
pub struct DocSnapshot {
pub text: String,
pub ast: DocumentNode,
pub in_memory: bool, // held in `documents` DashMap
}
impl Backend {
pub async fn read_or_open(&self, uri: &Url) -> io::Result<DocSnapshot>;
}
```
If the URI is in `documents`, return live state. Otherwise read the file
from disk and re-parse so cross-document operations (Phase 13
`workspace/rename`) can compute accurate spans + UTF-16 conversions
instead of trusting stale `WorkspaceIndex` data.
#### 12.2.4 Diagnostic source chain
Today's `ast_diagnostics(&ast, &text, utf8)` is replaced by a composable
collector:
```rust
pub fn collect_diagnostics(
ast: &DocumentNode,
text: &str,
index: Option<&WorkspaceIndex>,
page_name: Option<&str>,
cfg: &DiagnosticConfig,
utf8: bool,
) -> Vec<Diagnostic>;
```
Composes:
1. Parse errors (`ErrorNode` walk; already shipped)
2. Broken-link warnings (Phase 15)
3. (future) other sources
Each gated by `cfg`. Per-source severity is config-driven.
#### 12.2.5 `Wiki` aggregate
Single-wiki state becomes a `Wiki` aggregate, even though Phase 11 only
ever builds one entry. Phase 18 then changes config shape, not data
structures.
```rust
pub struct WikiId(u32);
pub struct WikiConfig {
pub name: String,
pub root: PathBuf,
pub file_extension: String,
pub syntax: String,
pub diary_rel_path: String,
pub html_path: PathBuf,
pub template_path: PathBuf,
// … all per-wiki options
}
pub struct Wiki {
pub id: WikiId,
pub config: WikiConfig,
pub index: Arc<RwLock<WorkspaceIndex>>,
}
struct Backend {
// …
wikis: Arc<RwLock<Vec<Wiki>>>,
}
impl Backend {
fn resolve_uri_to_wiki(&self, uri: &Url) -> Option<WikiId>; // longest-prefix root match
fn wiki(&self, id: WikiId) -> Option<Arc<Wiki>>;
fn default_wiki(&self) -> Option<Arc<Wiki>>;
}
```
`WorkspaceIndex` itself doesn't gain any new responsibilities in this
phase — it just lives inside a `Wiki`. All v1.0 handlers that today read
the single `index` field get refactored to call `default_wiki()` (or
`resolve_uri_to_wiki()` where the URI is known).
#### 12.2.6 What does *not* change
Explicitly out of scope for Phase 11:
- AST shape (no new node kinds — Tags wait for Phase 12)
- LSP capability set (no new providers — capabilities expand from Phase 13)
- HtmlRenderer surface (Phase 17 extends the template substitution set)
- Anything user-visible (this phase is invisible to clients)
#### 12.2.7 Tests
Phase 11 is fully covered by unit tests on the new helpers:
- `WorkspaceEditBuilder`: multi-uri accumulation, file-op ordering,
UTF-16 conversion for non-ASCII spans.
- `read_or_open`: live doc returns `in_memory: true`; missing path
returns `Err`; on-disk path returns `in_memory: false` with parsed AST.
- `collect_diagnostics`: parse errors emitted; link-health stub returns
empty until Phase 15 wires it.
- `Config::from_init_options`: legacy single-wiki shape desugars to
one-entry `wikis = [...]`.
- `Backend::resolve_uri_to_wiki`: longest-prefix match wins.
### 12.3 Tags (Phase 12)
#### Syntax
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.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<BrokenLink>` 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
```
<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.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` | `<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.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<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.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 `<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.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 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.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
Phase 14 shipped a focused subset (checkbox toggle/cycle/reject,
nextTask navigation, heading promote/demote). The structural-rewriter
commands below were deferred — each needs its own AST-aware rewriter,
and they don't share enough scaffolding to land together cheaply.
| Command | SPEC | What it does | Sketch |
|---|---|---|---|
| `nuwiki.list.changeSymbol` | §12.5 | Rewrite list marker (`-``*``#``1.` …), single item or whole list (`whole_list: bool` arg). | Locate item span via existing `find_list_item_at`; rewrite the marker token; if `whole_list` and numeric, renumber. |
| `nuwiki.list.changeLevel` | §12.5 | Indent/dedent one item or a subtree (`delta: i32`, `whole_subtree: bool`). | Compute leading-whitespace delta per line; re-indent each line of the item span; if subtree, descend into `ListItemNode.sublist`. |
| `nuwiki.list.renumber` | §12.5 | Re-sequence numeric markers across a list or a whole file (`whole_file: bool`). | Walk every `ListNode` whose `symbol` is `Numeric`/`NumericParen`/`AlphaParen`/…; rewrite the marker substrings in source order. |
| `nuwiki.list.removeDone` | §12.5 | Strip `[X]` / `[-]` items and their checked children. | Recursive list walker emitting delete spans for matching items; also delete the trailing newline so the surrounding list stays valid. |
| `nuwiki.table.align` | §12.5 | Reformat columns to max width. | Two-pass: width-per-column scan, then re-render every row with padding; respect `>` (col span) and `\/` (row span) markers. |
| `nuwiki.table.moveColumn` | §12.5 | Swap column with neighbour (`dir: "left" \| "right"`). | Locate `TableNode` under cursor; for each row, swap `cells[col]` and `cells[col±1]`; re-emit. |
| `nuwiki.table.insert` | §12.5 | Insert blank table at cursor (`cols`, `rows`). | Pure string templating; align outputs to current indentation. |
| `nuwiki.link.normalize` | §12.5 | Convert word/selection to `[[wikilink]]`, add description if missing. | Inspect cursor selection; if a plain word, wrap in `[[…]]`; if already a wikilink, ensure `\|description` is present. |
| `nuwiki.link.pasteWikilink` | §12.5 | Paste current page name as an absolute wikilink. | One-line insert; page name from `index::page_name_from_uri`. |
| `nuwiki.link.pasteUrl` | §12.5 | Paste the corresponding HTML output URL. | Blocked on Phase 17 (`html_path` / `html_filename_parameterization` config). |
| `nuwiki.colorize` | §12.5 | Wrap range in colour tag per `color_tag_template`. | Blocked on Phase 17 (`color_dic` config) — until then the template isn't reachable. |
#### Estimated sequencing
- **Cluster A (list rewriters)** — `changeSymbol`, `changeLevel`,
`renumber`, `removeDone` share a list-walker pattern. Likely one
PR with a shared `ops::list_walker` helper and four command handlers.
- **Cluster B (table rewriters)** — `align`, `moveColumn`, `insert`
share a column-width pass and a row-by-row re-emitter. Likely a
second PR.
- **Cluster C (link helpers)** — `normalize` and `pasteWikilink` are
small and don't share much; can land independently.
- **Phase-17-blocked** — `pasteUrl` and `colorize` wait until the
HTML export commands land the necessary config keys.
### 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<N>:Page]]` and
`[[wn.<Name>: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 `<C-Up>`/`<C-Down>`, HTML export under `<Leader>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.
---