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

Spec additions:

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

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

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

43 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 Tags :tag: lex + parse, TagNode, index, tag-as-anchor resolution
12 Workspace edits + executeCommand workspace/rename with cross-doc link rewrite, DeleteFile ops, executeCommand infrastructure
13 List & table edit commands Server commands returning WorkspaceEdit for checkbox toggle, list-symbol/level change, renumber, table align, column move, table insert
14 Link health + link/TOC generation Broken-link diagnostics, nuwiki.toc.generate, nuwiki.links.generate, nuwiki.workspace.checkLinks
15 Diary Diary subpath, today/yesterday/tomorrow open commands, diary index generation, calendar hook API
16 HTML export commands nuwiki.export.* commands, %template resolution, CSS file management, auto-export option
17 Multi-wiki wikis = [...] config, per-wiki WorkspaceIndex, URI → wiki resolution, wiki-picker commands
18 Editor glue v2 Full :Vimwiki* command compat layer, default keymaps, text objects (ah/aH/a\\/ac/al), foldexpr folding

11. Pending Decisions

These decisions are required before or during the phase indicated.

# Decision Needed by Options Notes
P1 License Phase 0 Dual MIT/Apache-2.0
P2 MSRV Phase 0 stable-2 (Rust 1.83)
P3 String representation in AST Phase 1 String (owned)
P4 Visitor pattern Phase 1 Defined in Phase 1 Open-recursion Visitor trait + walk_* helpers
P5 Minimum Neovim version Phase 9 Neovim 0.11+ Server registered via vim.lsp.config{} + vim.lsp.enable; a vim.lsp.start autocmd fallback is wired up but only fires when the declarative API is unavailable.
P6 Minimum Vim version Phase 9 Vim 9.1+ autoload glue assumes 9.1 idioms; uses vim-lsp first, then falls back to coc.nvim.
P7 Semantic token type mapping Phase 7 Custom vimwiki-specific token types ~20 types (vimwikiHeading, vimwikiBold, …) + level1..level6 + centered modifiers. Phase 9 ships default highlight groups in syntax/nuwiki.vim and the Lua glue.
P8 Workspace indexing strategy Phase 8 Lazy + background with progress Initial scan runs as a background tokio task; nav features answer with partial data until complete; progress reported via window/workDoneProgress.
P9 macOS runner Phase 10 Stay manual Release workflow cross-compiles the 4 Linux targets via cross; macOS + Windows binaries are produced ad hoc by maintainers and uploaded to the same Gitea release. Revisit when a Mac runner is available.
v1.1 decisions
P10 Command namespace Phase 12 :Vimwiki* only · :Nuwiki* only · both Both → easy migration and discoverable native commands; one canonical name + alias.
P11 Tag syntax Phase 11 Strict vimwiki :tag: only · Add markdown #tag as a second flavour Strict keeps parity; adding #tag collides with hash list markers and ordered-list #.
P12 List-edit transport Phase 13 Server returns WorkspaceEdit from executeCommand · Editor-side text manipulation in VimL/Lua WorkspaceEdit keeps logic in Rust + testable, but adds latency per keystroke for <C-Space>.
P13 Multi-wiki config shape Phase 17 Lua list (Neovim-native) · JSON config file (cross-editor) Lua is ergonomic for Neovim; JSON works for Vim + arbitrary clients.
P14 Folding mechanism Phase 18 LSP textDocument/foldingRange · Vim-side foldexpr in ftplugin LSP is correct (server knows AST); foldexpr is what vimwiki users expect and works offline.
P15 Diary path scheme Phase 15 Fixed <wiki_root>/diary/ · Per-wiki configurable diary_rel_path Vimwiki has diary_rel_path; matching keeps migrations clean.
P16 Backwards-compat for v1.0 config Phase 17 Preserve old single-wiki_root shape · Force migration to wikis = [...] Single-wiki should keep working without re-config; wiki_root becomes sugar for wikis = [{ root = wiki_root }].
P17 Markdown syntax in v1.1 Phase 11+ Include · Defer to v1.2 Architecture already supports it (SyntaxPlugin); cost is parser work + per-syntax differences in commands.

12. v1.1 — Full vimwiki Replacement

v1.0 shipped the parsing + highlighting + navigation foundation. A vimwiki user transitioning to nuwiki today gets accurate syntax recognition and modern LSP nav, but loses the dense interactive layer that makes vimwiki ergonomic for daily authoring: ≈40 :Vimwiki* commands, ≈60 keymaps, the diary, the tag system, link health, HTML export commands, and multi-wiki configuration.

v1.1 closes that gap. The architecture is unchanged — every new editing operation lands as an LSP executeCommand returning a WorkspaceEdit, so the nuwiki-core AST stays the single source of truth and the editor glue stays pure wiring.

12.1 Stability commitment

v1.1 is additive. The following stay backward-compatible:

  • nuwiki-core public AST + Visitor (additions only; no field renames)
  • LSP capabilities advertised in v1.0
  • Renderer trait + HtmlRenderer default output shape
  • v1.0 user-config keys (wiki_root, file_extension, syntax, log_level)

New AST nodes (e.g. TagNode) and new LSP capabilities are opt-in by inspection. The Visitor trait gains default-bodied visit_* methods for new node kinds so existing Visitor implementations don't break.

12.2 Tags (Phase 11)

Syntax

Vimwiki tags are colon-delimited sequences of non-space characters:

:tag-one:tag-two:other-tag:

Placement rules (matching vimwiki):

  • On line 1 or 2 of a file → file-level tag
  • Within 2 lines after a = Heading = → header-level tag
  • Otherwise → standalone anchor at the source line

AST additions

TagNode (block)
  span:    Span
  tags:    Vec<String>
  scope:   TagScope          -- File | Heading(idx) | Standalone

PageMetadata (extend)
  tags:    Vec<String>       -- file-level tags, accumulated

TagNode slots into BlockNode as a new variant. The Visitor gains visit_tag (default-bodied).

Indexing

WorkspaceIndex extends IndexedPage with:

IndexedPage (extend)
  tags:           Vec<TagInfo>

TagInfo
  name:    String
  scope:   TagScope
  span:    Span

Plus a tags_by_name: HashMap<String, Vec<(Url, Span)>> reverse map. The existing backlink machinery in §6 stays as-is.

LSP behaviour

  • tags-as-anchors extends textDocument/definition: [[Page#some-tag]] resolves to the tag's location in the target page (in addition to headings).
  • workspace/symbol results include tags (with SymbolKind::PROPERTY or a custom kind).
  • New executeCommand operations: nuwiki.tags.search, nuwiki.tags.generateLinks(tag, ...), nuwiki.tags.rebuild (force re-index).

12.3 Workspace edits + executeCommand (Phase 12)

Capability

ServerCapabilities {
  execute_command_provider: Some(ExecuteCommandOptions {
    commands: vec!["nuwiki.*", …],
  }),
  rename_provider: Some(true),
  workspace: Some(WorkspaceServerCapabilities {
    file_operations: Some(FileOperationOptions {
      will_rename: Some(...),
      did_rename: Some(...),
      will_delete: Some(...),
      did_delete: Some(...),
    }),
  }),
}

workspace/rename semantics

When a user renames Page 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.4 List & table editing commands (Phase 13)

Command Operates on Result
nuwiki.list.toggleCheckbox list item under cursor [ ][X], propagates parent state
nuwiki.list.cycleCheckbox list item [ ][.][o][O][X] cycle
nuwiki.list.rejectCheckbox list item toggle [-] rejected state
nuwiki.list.changeSymbol list item, args: symbol: ListSymbol, whole_list: bool rewrite marker, renumber if numeric
nuwiki.list.changeLevel list item, args: delta: i32, whole_subtree: bool re-indent + adjust child levels
nuwiki.list.renumber list, args: whole_file: bool re-sequence numeric markers
nuwiki.list.removeDone range or current list remove every [X] / [-] item and its checked children
nuwiki.list.nextTask document navigate to next unfinished task (returns Location, not edit)
nuwiki.table.align table under cursor reformat columns to max width
nuwiki.table.moveColumn cell, args: dir: "left"|"right" swap column with neighbour
nuwiki.table.insert cursor, args: cols, rows insert blank table at cursor
nuwiki.heading.addLevel heading promote (=====)
nuwiki.heading.removeLevel heading demote
nuwiki.link.normalize word/selection convert to [[wikilink]], add description if missing
nuwiki.link.pasteWikilink cursor paste current page name as an absolute wikilink
nuwiki.link.pasteUrl cursor paste the corresponding HTML output URL
nuwiki.colorize range, args: color: String wrap in colour tag per color_tag_template

All operations resolve to a textual diff over the existing source so the AST stays the source of truth. None of them rely on editor-side state.

A new diagnostic source nuwiki.link runs after each parse:

  • Wiki target → not in WorkspaceIndex → severity from config (default Warning)
  • Anchor target → page exists but anchor doesn't → Warning
  • file: / local: → file exists on disk → Warning if missing
  • Raw URL / external → never diagnosed (we don't fetch)
  • Configurable filtering via diagnostic.link.severity (off|hint|warn|error)

Commands

Command Behaviour
nuwiki.toc.generate inserts a nested list of headings in the current page; replaces existing %toc placeholder if present
nuwiki.links.generate inserts a flat list of all wiki pages (vimwiki's :VimwikiGenerateLinks)
nuwiki.workspace.checkLinks returns a Vec<BrokenLink> for the client to render in a quickfix-style view; also emits diagnostics
nuwiki.workspace.findOrphans returns pages with no incoming links

12.6 Diary (Phase 15)

Path conventions

<wiki_root>/<diary_rel_path>/         # default: "diary"
  diary.wiki                          # configurable: diary_index
  2026-05-11.wiki                     # daily: YYYY-MM-DD
  2026-W19.wiki                       # weekly: YYYY-Www (configurable)
  2026-05.wiki                        # monthly
  2026.wiki                           # yearly

Custom commands

Command Behaviour
nuwiki.diary.openToday open / create today's diary page
nuwiki.diary.openYesterday analogous
nuwiki.diary.openTomorrow analogous
nuwiki.diary.openIndex open the diary index page
nuwiki.diary.generateIndex rebuild the diary index page with current entries grouped by year/month
nuwiki.diary.next navigate to the chronologically next diary entry from the current one
nuwiki.diary.prev analogous

Calendar hook

External plugins (Calendar.vim, neorg-style calendars) can query:

  • nuwiki.diary.listEntries(year, month) → list of dates with entries
  • nuwiki.diary.openForDate(date) → opens that date's entry

These are executeCommand operations, not LSP standard methods.

12.7 HTML export commands (Phase 16)

The HtmlRenderer from Phase 5 stays as the engine. v1.1 wraps it in user-facing commands and the per-wiki HTML lifecycle.

Per-wiki HTML options

Key Default Description
html_path <wiki_root>/_html output directory
template_path <wiki_root>/_templates template lookup root
template_default default base template name
template_ext .tpl template extension
template_date_format %Y-%m-%d format passed to {{date}}
css_name style.css CSS file copied/created at export
auto_export false export on save
html_filename_parameterization false URL-safe slugs in output filenames
exclude_files [] globs to skip

Commands

Command Behaviour
nuwiki.export.currentToHtml render current page; honours %template, copies CSS if missing
nuwiki.export.allToHtml export every page in the wiki
nuwiki.export.allToHtmlForce :VimwikiAll2HTML! equivalent
nuwiki.export.browse export current + open default browser to result
nuwiki.export.rss emit rss.xml of recent diary entries (when diary enabled)

Template variables

The HtmlRenderer template substitution gains:

  • {{title}} (already shipped)
  • {{content}} (already shipped)
  • {{date}} (formatted via template_date_format)
  • {{root_path}} (relative path from output to html_path — for stylesheet hrefs in nested pages)
  • {{toc}} (rendered TOC for current page)

12.8 Multi-wiki (Phase 17)

Config shape

require('nuwiki').setup({
  wikis = {
    {
      name           = 'personal',
      root           = '~/vimwiki',
      file_extension = '.wiki',
      syntax         = 'vimwiki',
      diary_rel_path = 'diary',
      html_path      = '~/vimwiki/_html',
      -- … any per-wiki option
    },
    {
      name           = 'work',
      root           = '~/work-notes',
      syntax         = 'vimwiki',
    },
  },
  log_level = 'warn',
})

The v1.0 single-wiki shape continues to work; internally it desugars to wikis = [{ root = wiki_root, file_extension = file_extension, syntax = syntax }].

Per-wiki context

WorkspaceIndex becomes one per wiki. Backend holds:

indexes: Arc<DashMap<WikiId, Arc<RwLock<WorkspaceIndex>>>>

URI → wiki resolution walks the URI's filesystem path upward, matching against each registered root. Cross-wiki links (interwiki) resolve through the matching wiki's index.

Custom commands

Command Behaviour
nuwiki.wiki.select client-side picker over wikis; resolves to a wiki id
nuwiki.wiki.openIndex open the selected wiki's index.wiki
nuwiki.wiki.tabOpenIndex open in new tab (client-side)
nuwiki.wiki.listAll returns the registered wikis for editor UIs

12.9 Editor glue v2 (Phase 18)

The thickest user-facing layer. Per SPEC §6.3 it still contains no logic — every command body is a wrapper that issues workspace/executeCommand.

:Vimwiki* command compatibility

ftplugin/nuwiki.vim defines every command listed in §4 of vimwiki's help, mapped to the corresponding executeCommand:

Vimwiki command nuwiki implementation
:VimwikiIndex [count] nuwiki.wiki.openIndex(count)
:VimwikiTabIndex tab-open variant
:VimwikiUISelect nuwiki.wiki.select
:VimwikiDiaryIndex nuwiki.diary.openIndex
:VimwikiMakeDiaryNote nuwiki.diary.openToday
:VimwikiMakeYesterdayDiaryNote nuwiki.diary.openYesterday
:VimwikiMakeTomorrowDiaryNote nuwiki.diary.openTomorrow
:VimwikiFollowLink client vim.lsp.buf.definition()
:VimwikiGoBackLink client <C-o> (jumplist)
:VimwikiSplitLink / :VimwikiVSplitLink client-side :split/:vsplit + definition
:VimwikiTabnewLink / :VimwikiTabDropLink client-side :tabnew + definition
:VimwikiNextLink / :VimwikiPrevLink nuwiki.cursor.nextLink / prevLink
:VimwikiGoto {name} nuwiki.wiki.gotoPage(name)
:VimwikiDeleteFile server-side delete via executeCommand
:VimwikiRenameFile client prompt → workspace/rename
:VimwikiRemoveDone nuwiki.list.removeDone
:VimwikiNextTask nuwiki.list.nextTask
:Vimwiki2HTML nuwiki.export.currentToHtml
:Vimwiki2HTMLBrowse nuwiki.export.browse
:VimwikiAll2HTML[!] nuwiki.export.allToHtml[Force]
:VimwikiRss nuwiki.export.rss
:VimwikiToggleListItem nuwiki.list.toggleCheckbox
:VimwikiToggleRejectedListItem nuwiki.list.rejectCheckbox
:VimwikiListChangeLvl CMD nuwiki.list.changeLevel / changeSymbol
:VimwikiSearch / :VWS client :lvimgrep wrapper over wiki root
:VimwikiBacklinks / :VWB client vim.lsp.buf.references()
:VimwikiTable [cols] [rows] nuwiki.table.insert
:VimwikiTableMoveColumnLeft/Right nuwiki.table.moveColumn
:VimwikiGenerateLinks nuwiki.links.generate
:VimwikiDiaryGenerateLinks nuwiki.diary.generateIndex
:VimwikiDiaryNextDay / :VimwikiDiaryPrevDay nuwiki.diary.next/prev
:VimwikiTOC nuwiki.toc.generate
:VimwikiCheckLinks nuwiki.workspace.checkLinks
:VimwikiRebuildTags nuwiki.tags.rebuild
:VimwikiSearchTags {tag} nuwiki.tags.search
:VimwikiGenerateTagLinks nuwiki.tags.generateLinks
:VimwikiColorize {color} nuwiki.colorize
:VimwikiPasteLink nuwiki.link.pasteWikilink
:VimwikiPasteUrl nuwiki.link.pasteUrl

Each :Vimwiki* form ships with a matching :Nuwiki* alias (P10 resolution).

Default keymaps

g:nuwiki_key_mappings (Vim) / mappings = { … } (Neovim) controls opt-in keymap registration. Default: enabled; group flags let users disable the table/list/heading/diary subsets independently.

Buffer-local maps mirror vimwiki:

  • <Leader>ww / <Leader>wt / <Leader>ws / <Leader>wi / <Leader>w<Leader>w etc. — wiki navigation
  • <CR> / <S-CR> / <C-CR> / <M-CR> / <C-S-CR> — link follow variants
  • <Backspace> / <Tab> / <S-Tab> — link history + per-page link navigation
  • = / - / [[ / ]] / [= / ]= / ]u / [u — header manipulation + navigation (pure VimL/Lua, regex over current buffer — no LSP round-trip)
  • +nuwiki.link.normalize
  • <C-Space>nuwiki.list.toggleCheckbox
  • 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.10 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.11 Updated §9 syntax checklist (additions)

The following items extend the v1.0 checklist:

Tags

  • Inline tag sequence: :tag-one:tag-two:
  • File-level tag (line 12 of a wiki page)
  • Header-level tag (within 2 lines after a heading)
  • Tag-as-anchor target ([[Page#some-tag]])
  • Hex-colour code rendering in code blocks (`#ffe119`)

Tables (additions)

  • Column-alignment markers in header separator: |:--| / |:--:| / |--:|

Lists (parser extensions)

  • Multi-line list item (indented continuation lines)
  • Mixed-symbol lists at the same level

12.12 Updated CI/CD

No new workflow files. The existing ci.yaml continues to gate fmt/clippy/test. New phases land behind feature additions in the existing crates; the test suite grows accordingly.