Backfill scope reminder for the audit that surfaced the Phase 14 deferred surface. §13 consolidates the items still owed from earlier phases so a future "what's left" check doesn't have to re-derive scope from the per-phase prose. - §13.1 lists the 11 deferred Phase 14 commands (list/table/link rewriters + colorize), each with an implementation sketch and a suggested cluster (list rewriters, table rewriters, link helpers, Phase-17-blocked). - §13.2 documents the deliberately-deferred `will_rename` / `will_delete` file-operation variants from the Phase 13 backfill. - §13.3 reminds readers that Phases 17–19 are roadmap, not missing-from-earlier-phases. - §13.4 clarifies that §9 syntax checklist `[ ]` rows are parser coverage rather than missing features. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 KiB
nuwiki — Project Specification
Last updated: 2026-05-11 Status: v1.0 (Phases 0–10) shipped; v1.1 (Phases 11–18) in design
Versioning convention
- v1.0 — the parsing + highlighting + navigation foundation. LSP server speaks the spec's §6.9 method set; editor glue provides install + setup
- health check.
- v1.1 — full vimwiki replacement. Adds tags, diary, the text-manipulation command surface (toggle checkbox, change list symbol, format table, rename page, …), multi-wiki, HTML export commands, link-health diagnostics, and the keymap/text-object layer that matches vimwiki's daily-authoring ergonomics.
1. Overview
nuwiki is a Vim/Neovim plugin that provides full vimwiki syntax support, implemented as a Rust-based Language Server (LSP). It is a spiritual successor to vimwiki, architecturally distinct from it, designed for correctness, performance, and extensibility.
Goals:
- Full vimwiki syntax support (highlighting, navigation, link following, diagnostics)
- First-class support for both Vim 9+ and Neovim 0.5+
- Installable via standard plugin managers (lazy.nvim, vim-plug, Dein)
- Extensible to Markdown syntax without changes to core architecture
- Independently testable core library with no editor dependencies
Non-goals (v1.0):
- MediaWiki syntax support
- Multi-wiki configurations (addressed in v1.1, see §12)
- Incremental / tree-sitter parsing
- Browser-based or WASM build
v1.1 scope (see §12 for the full plan):
- Vimwiki-parity command surface (
:Vimwiki*compat aliases over LSPexecuteCommand) - 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-coremust never depend onnuwiki-lspornuwiki-lsnuwiki-lspmust never depend onnuwiki-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:
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 aTokenStreamnewtype (changeable to lazy later) - Operates on Rust
chars for correctness; spans stored as byte offsets
6.7 Parser Strategy
- Uses
winnowparser combinator library - Resilient: on malformed input, emits
ErrorNodeand continues — never fails the whole document - Inline marker precedence rules documented explicitly in code and tests
6.8 Renderer
Renderertrait: writer-based (fn render(&self, doc: &DocumentNode, w: &mut dyn Write))HtmlRendereris 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:
{
"gffranco/nuwiki",
build = "lua require('nuwiki').install()",
ft = { "vimwiki" },
opts = {},
}
vim-plug:
Plug 'gffranco/nuwiki', { 'do': 'vim -e -s -c "source scripts/download_bin.vim" -c "q"' }
Dein:
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.luaorscripts/download_bin.vim - Fallback:
cargo build --releaseif download fails org:nuwiki_build_from_source = 1 - Binary installed to
{plugin_dir}/bin/nuwiki-ls[.exe]
7.3 Editor Detection
" 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)
vim-lsp(matoto/vim-lsp)coc.nvim- Error message if neither found
7.5 User Config Schema
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
.wikifiles
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 --checkcargo clippy -- -D warningscargo 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_TOKENsecret - crates.io publish is deferred — workflow ships the Gitea release only. Re-enable by adding a
cargo publish -p nuwiki-corejob 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 |
nuwiki-core |
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 SyntaxPluginorRenderertrait 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:
TODODONESTARTEDFIXMEFIXEDXXX
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 |
| — 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 |
|---|---|---|---|---|
| ✅ Dual MIT/Apache-2.0 | ||||
| ✅ stable-2 (Rust 1.83) | ||||
✅ String (owned) |
||||
| ✅ Defined in Phase 1 | Open-recursion Visitor trait + walk_* helpers |
|||
| ✅ 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. |
|||
| ✅ Vim 9.1+ | autoload glue assumes 9.1 idioms; uses vim-lsp first, then falls back to coc.nvim. |
|||
| ✅ 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. |
|||
| ✅ 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. |
|||
| ✅ 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. |
✅ 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-corepublic AST + Visitor (additions only; no field renames)- LSP capabilities advertised in v1.0
Renderertrait +HtmlRendererdefault output shape- v1.0 user-config keys (
wiki_root,file_extension,syntax,log_level)
New AST nodes (e.g. TagNode) and new LSP capabilities are opt-in by
inspection. The Visitor trait gains default-bodied visit_* methods for new
node kinds so existing Visitor implementations don't break.
12.2 Plumbing prerequisites (Phase 11)
Cross-cutting infrastructure that every later v1.1 phase depends on. Landing it as Phase 11 — before any user-visible feature — keeps Phases 12–19 mechanical instead of full of "oh we also need to refactor X".
12.2.1 Server config receipt (P18)
Per P18: both initializationOptions and workspace/didChangeConfiguration
land in Phase 11.
initialize.initializationOptionscarries the full v1.1 config (§12.11 schema) verbatim. Parsed into a server-sideConfigstruct.workspace/didChangeConfigurationre-applies the config for live reload.- Every handler reads from a single
Arc<RwLock<Config>>field onBackend.
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:
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 13–17) go through
the builder. Standardises UTF-8/UTF-16 conversion and document-change
ordering.
12.2.3 Closed-doc loader
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:
pub fn collect_diagnostics(
ast: &DocumentNode,
text: &str,
index: Option<&WorkspaceIndex>,
page_name: Option<&str>,
cfg: &DiagnosticConfig,
utf8: bool,
) -> Vec<Diagnostic>;
Composes:
- Parse errors (
ErrorNodewalk; already shipped) - Broken-link warnings (Phase 15)
- (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.
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 returnsin_memory: true; missing path returnsErr; on-disk path returnsin_memory: falsewith 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-entrywikis = [...].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-anchorsextendstextDocument/definition:[[Page#some-tag]]resolves to the tag's location in the target page (in addition to headings).workspace/symbolresults include tags (withSymbolKind::PROPERTYor a custom kind).- New
executeCommandoperations: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/renamerewrites 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. StoredWorkspaceIndexspans are byte-accurate but may have drifted if the on-disk file changed outside the editor; the re-parse guarantees we emitTextEdits 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:
- Server emits a
WorkspaceEditcontaining aRenameFileop (A.wiki→B.wiki). - For every page in the index that links to
A, the edit also rewrites[[A]]→[[B]](preserving description, anchor, kind). - 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 (defaultWarning) - Anchor target → page exists but anchor doesn't →
Warning file:/local:→ file exists on disk →Warningif 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(andYYYY-Www,YYYY-MM,YYYYfor non-daily frequencies). v1.1 ships a small hand-rolled parser/formatter innuwiki-corefor these formats — nochrono/timedependency. 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 entriesnuwiki.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 viatemplate_date_format){{root_path}}(relative path from output tohtml_path— for stylesheet hrefs in nested pages){{toc}}(rendered TOC for current page)
12.9 Multi-wiki (Phase 18)
Data-structure pre-work. The
Wikiaggregate,WorkspaceIndexper-wiki ownership, andBackend::resolve_uri_to_wikialready landed in Phase 11 with a single-entrywikislist. Phase 18 is therefore a config-shape change (acceptwikis = [...]frominitializationOptions) and resolution-policy change (interwiki links resolve through the matching wiki's index) — not a refactor of the data flow.
Config shape
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>wetc. — 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.toggleCheckboxgnt—nuwiki.list.nextTaskgl<Space>/gL<Space>— checkbox remove (single / list)gln/glp— checkbox cycle up/downgll/gLl/glh/gLh— list level change (single / subtree)glr/gLr— list renumbergl*/gl#/gl-/gl+/gl1/gla/glA/gli/glI— list symbol change (single / subtree variantsgL*)glx—:VimwikiToggleRejectedListItemgqq/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 viag: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_ftpluginand 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 LSPfoldingRangedriving 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
require('nuwiki').setup({
-- single-wiki shorthand (v1.0 compat)
wiki_root = '~/vimwiki',
file_extension = '.wiki',
syntax = 'vimwiki',
log_level = 'warn',
-- or the v1.1 multi-wiki form
wikis = {
{
name = 'personal',
root = '~/vimwiki',
file_extension = '.wiki',
syntax = 'vimwiki',
diary_rel_path = 'diary',
diary_frequency = 'daily', -- daily | weekly | monthly | yearly
diary_start_week_day = 'monday',
template_path = '~/vimwiki/_templates',
template_default = 'default',
template_ext = '.tpl',
css_name = 'style.css',
html_path = '~/vimwiki/_html',
auto_export = false,
auto_toc = false,
auto_tags = false,
exclude_files = {},
bullet_types = { '-', '*', '#' },
listsyms = ' .oOX',
listsym_rejected = '-',
listsyms_propagate = true,
color_dic = { red = 'red', green = 'green' },
},
},
-- workspace-wide behaviour
diagnostic = {
link_severity = 'warn', -- off | hint | warn | error
},
mappings = {
enabled = true,
table_editing = true,
list_editing = true,
header_nav = true,
diary = true,
html_export = true,
mouse = false,
},
folding = 'syntax', -- off | syntax | list | custom
})
12.12 Updated §9 syntax checklist (additions)
The following items extend the v1.0 checklist:
Tags
- Inline tag sequence:
:tag-one:tag-two: - File-level tag (line 1–2 of a wiki page)
- Header-level tag (within 2 lines after a heading)
- Tag-as-anchor target (
[[Page#some-tag]]) - Hex-colour code rendering in code blocks (
`#ffe119`)
Tables (additions)
- Column-alignment markers in header separator:
|:--|/|:--:|/|--:|
Lists (parser extensions)
- Multi-line list item (indented continuation lines)
- Mixed-symbol lists at the same level
12.13 Updated CI/CD
No new workflow files. The existing ci.yaml continues to gate
fmt/clippy/test. New phases land behind feature additions in the
existing crates; the test suite grows accordingly.
13. Pending Implementation
Items specified in earlier phases but not yet implemented. Tracked here so a future audit doesn't have to re-derive the scope from the per-phase prose. Each entry includes the originating SPEC section, why it was deferred, and a sketch of what the implementation needs.
13.1 Phase 14 — list/table/link/colorize commands
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,removeDoneshare a list-walker pattern. Likely one PR with a sharedops::list_walkerhelper and four command handlers. - Cluster B (table rewriters) —
align,moveColumn,insertshare a column-width pass and a row-by-row re-emitter. Likely a second PR. - Cluster C (link helpers) —
normalizeandpasteWikilinkare small and don't share much; can land independently. - Phase-17-blocked —
pasteUrlandcolorizewait 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 phases 17–19
Tracked in §10 with status ⏳. Not "missing" from earlier phases —
they're future work in the v1.1 roadmap:
- Phase 17 — HTML export commands (
nuwiki.export.*), template resolution, CSS file management, auto-export. Brings inhtml_path/template_path/color_dic/ etc. config keys. - Phase 18 — Multi-wiki. Data structures (Wiki aggregate, per-wiki index) already landed in Phase 11; this phase is the config-shape change and the picker commands.
- Phase 19 — Editor glue v2:
:Vimwiki*compat layer, default keymaps, text objects, folding (P14 already resolved in favour of LSPfoldingRange+foldexprfallback).
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.