Files
nuwiki/SPEC.md
T
gffranco d1b807b7d1
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m26s
CI / cargo test (push) Successful in 1m16s
phase 17: html export commands + extended template vars
HtmlRenderer (nuwiki-core):
- `with_var(k, v)` / `with_vars(map)` add arbitrary `{{key}}`
  substitution alongside the existing `{{title}}` / `{{content}}`.
  Substitution order: content → title → user vars, so a rendered body
  that happens to contain a literal `{{title}}` isn't double-processed.

WikiConfig (nuwiki-lsp/config):
- New `html: HtmlConfig` field with per-wiki export options matching
  vimwiki's `g:vimwiki_*` keys: `html_path`, `template_path`,
  `template_default`, `template_ext`, `template_date_format`,
  `css_name`, `auto_export`, `html_filename_parameterization`,
  `exclude_files`. Defaults: `<root>/_html` and `<root>/_templates`.
- `RawWiki` accepts all of the above through `initializationOptions`
  (and `workspace/didChangeConfiguration` reload).

New `export` module (pure helpers):
- `output_path_for` — page name → on-disk HTML path. Slugifies only
  the final segment when `html_filename_parameterization = true` so
  the `diary/` subdir survives.
- `template_for` — primary/fallback path lookup tuple.
- `fallback_template` — minimal in-memory page used when neither
  template file exists.
- `format_date` — strftime subset (`%Y`/`%m`/`%d`/`%%`) so we don't
  depend on `chrono`/`time`.
- `build_toc_html` — nested `<ul class="toc">` from the doc's
  headings, with HTML escaping.
- `compute_root_path` — `../` prefix per directory depth for
  stylesheet hrefs in nested pages.
- `is_excluded` — glob matcher supporting `*`, `**`, `?`. Used by
  the `allToHtml*` walker against `exclude_files`.
- `render_page_html` — wires the above into HtmlRenderer with
  `{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}` populated.
- `DEFAULT_CSS` — minimal stylesheet bundled for first-time exports.

New commands:
- `nuwiki.export.currentToHtml` — render the current buffer; honours
  `%nohtml` and `%template`; ensures CSS exists.
- `nuwiki.export.allToHtml` — walk the wiki root, skip pages matched
  by `exclude_files` and `%nohtml`, and only re-export pages whose
  source is newer than the existing HTML (incremental behaviour
  matching `:VimwikiAll2HTML`).
- `nuwiki.export.allToHtmlForce` — same but unconditional re-export.
- `nuwiki.export.browse` — alias of currentToHtml that returns the
  `file://` URL of the rendered page in the response so the client
  can open it.
- `nuwiki.export.rss` — write `<html_path>/rss.xml` listing the
  wiki's diary entries newest-first.
- `did_save` handler — runs the auto-export equivalent when the
  resolved wiki has `auto_export = true`.

LSP capability:
- `text_document_sync` switched from `Kind(FULL)` to `Options(...)`
  so the server can register a `save` notification (otherwise
  clients never push `did_save` events).

Tests: 33 new in `phase17_html_export.rs` covering HtmlConfig
defaults + JSON-shape config, the `format_date` subset (including
the `%%` escape and pass-through for unsupported specifiers),
root-path computation, output-path slugification rules, the glob
matcher (single-star, globstar, question-mark, non-match),
template_for primary/fallback selection, render_page_html for the
title/content/date/root_path triad and the override-by-extras flow,
and side-effecting disk paths (write_page creates parents, picks up
on-disk template, RSS is newest-first, ensure_css is idempotent).
Total 357 tests pass; fmt + clippy clean.

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

55 KiB
Raw Blame History

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:

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 chars 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:

{
  "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.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

" 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

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.xv0.x+1 or v1.xv2.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
  • 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.
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 1317) 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:

  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.

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 TextEdits 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 APage B:

  1. Server emits a WorkspaceEdit containing a RenameFile op (A.wikiB.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.

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

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
  • gntnuwiki.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

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-blockedpasteUrl 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 phases 1819

Tracked in §10 with status . Not "missing" from earlier phases — they're future work in the v1.1 roadmap:

  • 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 LSP foldingRange + foldexpr fallback).

Phase 17 (HTML export commands) shipped with the focused command surface (currentToHtml / allToHtml[Force] / browse / rss), template-file + fallback resolution, the {{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 (it's only useful once colorize from §13.1 lands), as is the §13.1 link.pasteUrl follow-up that depends on the same export config.

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.