Docs #1
@@ -93,6 +93,6 @@ jobs:
|
||||
sudo apt-get install -y --no-install-recommends vim
|
||||
vim --version | head -1
|
||||
- name: run Neovim keymap harness
|
||||
run: ./scripts/test-keymaps.sh
|
||||
run: ./development/tests/test-keymaps.sh
|
||||
- name: run Vim keymap harness
|
||||
run: ./scripts/test-keymaps-vim.sh
|
||||
run: ./development/tests/test-keymaps-vim.sh
|
||||
|
||||
@@ -122,8 +122,8 @@ no extra plugin.
|
||||
### Try it without touching your config
|
||||
|
||||
```sh
|
||||
./start-nvim.sh # spawns Neovim against an isolated sample wiki
|
||||
./start-vim.sh # same, for plain Vim (clones vim-lsp on first run)
|
||||
./development/start-nvim.sh # spawns Neovim against an isolated sample wiki
|
||||
./development/start-vim.sh # same, for plain Vim (clones vim-lsp on first run)
|
||||
```
|
||||
|
||||
Both scripts build the LSP binary, seed a scratch wiki, and launch the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
" autoload/nuwiki/commands.vim — Vim-side wrappers around the LSP
|
||||
" commands the server advertises (Phase 19).
|
||||
" commands the server advertises.
|
||||
"
|
||||
" Lives in autoload/ so the cost is paid only when a `:Vimwiki*` /
|
||||
" `:Nuwiki*` command is actually invoked. Dispatches via vim-lsp's
|
||||
@@ -107,7 +107,7 @@ endfunction
|
||||
|
||||
function! s:notify_deferred(name) abort
|
||||
echohl WarningMsg
|
||||
echom 'nuwiki: ' . a:name . ' is not yet implemented — see SPEC §13.1'
|
||||
echom 'nuwiki: ' . a:name . ' is not yet implemented'
|
||||
echohl None
|
||||
endfunction
|
||||
|
||||
@@ -478,6 +478,13 @@ endfunction
|
||||
function! nuwiki#commands#cycle_list_item() abort
|
||||
call s:exec_pos('nuwiki.list.cycleCheckbox')
|
||||
endfunction
|
||||
" `glp` cycles backward — same command, `reverse` flag in the payload.
|
||||
function! nuwiki#commands#cycle_list_item_back() abort
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{ 'uri': l:p['textDocument']['uri'],
|
||||
\ 'position': l:p['position'], 'reverse': v:true }]
|
||||
call s:exec('nuwiki.list.cycleCheckbox', l:args)
|
||||
endfunction
|
||||
function! nuwiki#commands#reject_list_item() abort
|
||||
call s:exec_pos('nuwiki.list.rejectCheckbox')
|
||||
endfunction
|
||||
@@ -627,11 +634,20 @@ function! nuwiki#commands#rename_file() abort
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" ===== §13.1 deferred =====
|
||||
" ===== Deferred =====
|
||||
|
||||
" §13.1 Cluster A — list rewriters.
|
||||
" Cluster A — list rewriters.
|
||||
|
||||
" `gl<Space>` — remove done items from the current list only. Passing the
|
||||
" cursor position scopes the server to the contiguous list block under it.
|
||||
function! nuwiki#commands#list_remove_done() abort
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }]
|
||||
call s:exec('nuwiki.list.removeDone', l:args)
|
||||
endfunction
|
||||
|
||||
" `gL<Space>` — remove done items across the whole buffer (no position).
|
||||
function! nuwiki#commands#list_remove_done_all() abort
|
||||
let l:args = [{ 'uri': s:buf_uri() }]
|
||||
call s:exec('nuwiki.list.removeDone', l:args)
|
||||
endfunction
|
||||
@@ -721,7 +737,7 @@ function! nuwiki#commands#list_toggle_or_add_checkbox() abort
|
||||
let l:new = strpart(l:line, 0, l:end) . '[ ] ' . strpart(l:line, l:end)
|
||||
call setline('.', l:new)
|
||||
endfunction
|
||||
" §13.1 Cluster B — table rewriters.
|
||||
" Cluster B — table rewriters.
|
||||
|
||||
function! nuwiki#commands#table_insert(...) abort
|
||||
let l:cols = a:0 >= 1 ? str2nr(a:1) : 3
|
||||
@@ -799,7 +815,7 @@ function! nuwiki#commands#colorize(...) abort
|
||||
\ . strpart(l:line, l:right - 1)
|
||||
call setline('.', l:new)
|
||||
endfunction
|
||||
" §13.1 Cluster C — link helpers.
|
||||
" Cluster C — link helpers.
|
||||
|
||||
function! nuwiki#commands#paste_link() abort
|
||||
let l:p = s:cursor_position()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
" autoload/nuwiki/lsp.vim — Vim (non-Neovim) LSP client wiring.
|
||||
"
|
||||
" Spec §7.4 preference order:
|
||||
" Preference order:
|
||||
" 1. vim-lsp (matoto/vim-lsp)
|
||||
" 2. coc.nvim
|
||||
" 3. Print an error if neither is available.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
//! Block-level AST nodes and their supporting types.
|
||||
//!
|
||||
//! See SPEC.md §6.4.
|
||||
|
||||
use super::inline::InlineNode;
|
||||
use super::span::Span;
|
||||
@@ -17,7 +15,7 @@ pub enum BlockNode {
|
||||
DefinitionList(DefinitionListNode),
|
||||
Table(TableNode),
|
||||
Comment(CommentNode),
|
||||
/// Vimwiki tag line — `:tag1:tag2:`. Phase 12 (v1.1). The placement
|
||||
/// Vimwiki tag line — `:tag1:tag2:`. The placement
|
||||
/// rule (file / heading / standalone) is captured in `TagScope` so
|
||||
/// LSP handlers and renderers can treat the three differently.
|
||||
Tag(TagNode),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Inline AST nodes.
|
||||
//!
|
||||
//! See SPEC.md §6.4. The link-related variants (`WikiLink`, `ExternalLink`,
|
||||
//! The link-related variants (`WikiLink`, `ExternalLink`,
|
||||
//! `Transclusion`, `RawUrl`) wrap structs defined in `super::link`.
|
||||
|
||||
use super::link::{ExternalLinkNode, RawUrlNode, TransclusionNode, WikiLinkNode};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Link-related AST nodes and supporting types.
|
||||
//!
|
||||
//! See SPEC.md §6.4. All nodes here are inline (`InlineNode` variants);
|
||||
//! All nodes here are inline (`InlineNode` variants);
|
||||
//! they live in their own module because the link model has enough surface
|
||||
//! area (kinds, anchors, interwiki indirection) to warrant separation.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! AST types for nuwiki documents.
|
||||
//!
|
||||
//! Types here are syntax-agnostic — both vimwiki and (eventually) markdown
|
||||
//! produce the same node shapes. See SPEC.md §6.4.
|
||||
//! produce the same node shapes.
|
||||
|
||||
pub mod block;
|
||||
pub mod inline;
|
||||
@@ -28,7 +28,7 @@ pub use visit::{
|
||||
walk_table, walk_table_row, Visitor,
|
||||
};
|
||||
|
||||
/// Root of an AST. See SPEC.md §6.4.
|
||||
/// Root of an AST.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DocumentNode {
|
||||
pub span: Span,
|
||||
@@ -37,8 +37,7 @@ pub struct DocumentNode {
|
||||
}
|
||||
|
||||
/// Document-level placeholders parsed from `%title` / `%nohtml` / `%template`
|
||||
/// / `%date` directives, plus the v1.1 file-level tag accumulator
|
||||
/// (SPEC §12.3). See SPEC.md §9.
|
||||
/// / `%date` directives, plus the file-level tag accumulator.
|
||||
///
|
||||
/// New fields are added at the bottom; consumers should construct with
|
||||
/// `..Default::default()` to stay forward-compatible.
|
||||
@@ -48,7 +47,7 @@ pub struct PageMetadata {
|
||||
pub nohtml: bool,
|
||||
pub template: Option<String>,
|
||||
pub date: Option<String>,
|
||||
/// File-scope tags parsed by the v1.1 tag lexer (Phase 12). Convenience
|
||||
/// File-scope tags parsed by the tag lexer. Convenience
|
||||
/// projection of the `BlockNode::Tag` nodes whose scope is `File`.
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Source positions and spans carried on every AST node.
|
||||
//!
|
||||
//! See SPEC.md §6.5. Positions are 0-indexed; `column` is a byte offset
|
||||
//! Positions are 0-indexed; `column` is a byte offset
|
||||
//! within a line; `offset` is a byte offset from the start of the document.
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
//! Override any method to short-circuit or augment traversal — call the
|
||||
//! matching `walk_*` from your override to keep descending.
|
||||
//!
|
||||
//! Pattern follows `rustc`'s and `syn`'s visitors. See SPEC.md §6.4 + P4.
|
||||
//! Pattern follows `rustc`'s and `syn`'s visitors.
|
||||
//!
|
||||
//! Read-only for now. A `VisitorMut` flavor can be added when transformations
|
||||
//! become relevant (post-Phase 5).
|
||||
//! become relevant.
|
||||
|
||||
use super::block::{
|
||||
BlockNode, BlockquoteNode, CommentNode, DefinitionItemNode, DefinitionListNode, ErrorNode,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Date primitives for the diary subsystem (Phase 16).
|
||||
//! Date primitives for the diary subsystem.
|
||||
//!
|
||||
//! v1.1 deliberately doesn't pull in `chrono` or `time`. The diary feature
|
||||
//! This crate deliberately doesn't pull in `chrono` or `time`. The diary feature
|
||||
//! only needs `YYYY-MM-DD` parsing/formatting and ±1 day arithmetic, which
|
||||
//! is small enough that the dependency cost outweighs the convenience.
|
||||
//!
|
||||
@@ -56,7 +56,7 @@ impl DiaryDate {
|
||||
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
|
||||
}
|
||||
|
||||
/// UTC "today" computed from `SystemTime::now()`. Phase 16's diary
|
||||
/// UTC "today" computed from `SystemTime::now()`. The diary
|
||||
/// commands intentionally use UTC — local-time semantics depend on a
|
||||
/// timezone DB we don't ship and would surprise users crossing DST
|
||||
/// boundaries.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Core parser, AST, and renderer for nuwiki.
|
||||
//!
|
||||
//! This crate is editor-independent and must never depend on `nuwiki-lsp` or
|
||||
//! `nuwiki-ls`. See SPEC.md §6.2 for the dependency rules.
|
||||
//! `nuwiki-ls`.
|
||||
|
||||
pub mod ast;
|
||||
pub mod date;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
//! Walks a `DocumentNode` and writes HTML5 fragments. The renderer is
|
||||
//! configured with a link resolver (callback that turns a `LinkTarget` into
|
||||
//! a URL string) and, optionally, an enclosing template with substitution
|
||||
//! placeholders (SPEC.md §6.8 + §12.8).
|
||||
//! placeholders.
|
||||
//!
|
||||
//! Substitution model: `{{content}}` and `{{title}}` are computed from
|
||||
//! the rendered body and the document's metadata; `with_var(k, v)` /
|
||||
//! `with_vars(map)` add arbitrary key→value pairs (Phase 17 ships
|
||||
//! `{{date}}`, `{{root_path}}`, `{{toc}}`). Order: `{{content}}` is
|
||||
//! `with_vars(map)` add arbitrary key→value pairs (`{{date}}`,
|
||||
//! `{{root_path}}`, `{{toc}}`). Order: `{{content}}` is
|
||||
//! substituted first so a body that happens to contain a literal
|
||||
//! `{{title}}` isn't itself rewritten in the next pass.
|
||||
//!
|
||||
@@ -44,7 +44,7 @@ pub struct HtmlRenderer {
|
||||
/// `color_dic`-style override: vimwiki colour-tag names → CSS
|
||||
/// values (`"red"` / `"#ffe119"` / `"oklch(…)"`). Unspecified
|
||||
/// names fall through to the default `class="color-<name>"`
|
||||
/// rendering. Matches SPEC §12.11 `color_dic`.
|
||||
/// rendering. Matches vimwiki's `color_dic`.
|
||||
colors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@ fn table_spans(table: &TableNode) -> Vec<Vec<CellLayout>> {
|
||||
|
||||
// ===== Default link resolver =====
|
||||
|
||||
/// Default `LinkResolver`. Mirrors the conventions in SPEC.md §9:
|
||||
/// Default `LinkResolver`. Mirrors the vimwiki link conventions:
|
||||
/// wiki pages become `path.html`; interwiki links land in sibling
|
||||
/// directories; diary entries land under `diary/`; file/local schemes
|
||||
/// use their literal path; anchor-only links collapse to a fragment.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Document renderers.
|
||||
//!
|
||||
//! SPEC.md §6.8: a `Renderer` is writer-based, takes a `DocumentNode`, and
|
||||
//! A `Renderer` is writer-based, takes a `DocumentNode`, and
|
||||
//! emits its representation into any `Write`. Errors propagate through
|
||||
//! `io::Result` — there's no separate per-renderer error type so the trait
|
||||
//! stays object-safe (`Box<dyn Renderer>` is useful for the LSP later).
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
//! a `Lexer` produces a `TokenStream`, a `Parser` consumes it and produces a
|
||||
//! `DocumentNode`. A `SyntaxPlugin` is the type-erased facade that the LSP
|
||||
//! layer holds in a `SyntaxRegistry`.
|
||||
//!
|
||||
//! See SPEC.md §6.3 (interface) and §6.6 (lexer strategy).
|
||||
|
||||
pub mod registry;
|
||||
pub mod vimwiki;
|
||||
@@ -15,7 +13,7 @@ pub use registry::SyntaxRegistry;
|
||||
use crate::ast::DocumentNode;
|
||||
|
||||
/// Eager token buffer produced by a `Lexer`. The newtype keeps the door
|
||||
/// open to a lazy/streaming variant later (SPEC.md §6.6) without breaking
|
||||
/// open to a lazy/streaming variant later without breaking
|
||||
/// callers.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct TokenStream<T> {
|
||||
@@ -89,7 +87,7 @@ pub trait Lexer {
|
||||
}
|
||||
|
||||
/// Parser half of a syntax plugin. Consumes a `TokenStream` and produces a
|
||||
/// `DocumentNode`. Per SPEC.md §6.7 parsing is resilient — malformed input
|
||||
/// `DocumentNode`. Parsing is resilient — malformed input
|
||||
/// becomes `BlockNode::Error(ErrorNode)`, never a hard failure.
|
||||
pub trait Parser {
|
||||
type Token;
|
||||
@@ -104,13 +102,13 @@ pub trait Parser {
|
||||
/// hold heterogeneous plugins behind a single trait object.
|
||||
///
|
||||
/// `Send + Sync` is required — the LSP server stores plugins behind an `Arc`
|
||||
/// and shares them across request handler tasks (SPEC.md §6.3).
|
||||
/// and shares them across request handler tasks.
|
||||
pub trait SyntaxPlugin: Send + Sync {
|
||||
fn id(&self) -> &str;
|
||||
fn display_name(&self) -> &str;
|
||||
|
||||
/// File extensions associated with this syntax. Each entry includes the
|
||||
/// leading dot, e.g. `[".wiki"]`. See SPEC.md §6.3.
|
||||
/// leading dot, e.g. `[".wiki"]`.
|
||||
fn file_extensions(&self) -> &[&str];
|
||||
|
||||
fn parse(&self, text: &str) -> DocumentNode;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Registry of syntax plugins. See SPEC.md §6.3.
|
||||
//! Registry of syntax plugins.
|
||||
//!
|
||||
//! Plugins are stored behind `Arc<dyn SyntaxPlugin>` so the LSP layer can
|
||||
//! hand a cheap clone to per-document tasks without re-locking the registry.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Vimwiki lexer.
|
||||
//!
|
||||
//! Hand-rolled, two-pass per SPEC.md §6.6:
|
||||
//! Hand-rolled, two-pass:
|
||||
//!
|
||||
//! - **Block pass:** scan the source line by line, recognise structural
|
||||
//! constructs (headings, lists, tables, fences, comments, etc.), emit
|
||||
@@ -12,7 +12,7 @@
|
||||
//! Both passes share one `Vec<VimwikiToken>` so the parser sees a single,
|
||||
//! ordered stream. The lexer is permissive: every delimiter is emitted; the
|
||||
//! parser pairs them and falls back to literal text on mismatches. Spans are
|
||||
//! stored as 0-indexed byte offsets per SPEC.md §6.5.
|
||||
//! stored as 0-indexed byte offsets.
|
||||
//!
|
||||
//! Multi-line constructs (`{{{ }}}`, `{{$ }}$`, `%%+ +%%`) flip a
|
||||
//! `BlockMode` so subsequent lines are treated as raw content until the
|
||||
@@ -97,13 +97,13 @@ pub enum VimwikiTokenKind {
|
||||
CommentMultiClose,
|
||||
CommentMultiLine(String),
|
||||
|
||||
// ----- Tags (SPEC §12.3) -----
|
||||
// ----- Tags -----
|
||||
/// `:tag1:tag2:` — colon-delimited tag line. The lexer treats this as
|
||||
/// block-level (must be the entire trimmed content of the line).
|
||||
/// Scope (file / heading / standalone) is the parser's job.
|
||||
Tag(Vec<String>),
|
||||
|
||||
// ----- Page placeholders (SPEC §9) -----
|
||||
// ----- Page placeholders -----
|
||||
PlaceholderTitle(Option<String>),
|
||||
PlaceholderNohtml,
|
||||
PlaceholderTemplate(Option<String>),
|
||||
@@ -129,7 +129,7 @@ pub enum VimwikiTokenKind {
|
||||
TransclusionSep,
|
||||
RawUrl(String),
|
||||
|
||||
/// Lex error — never aborts the whole document (SPEC §6.7).
|
||||
/// Lex error — never aborts the whole document.
|
||||
Error(String),
|
||||
}
|
||||
|
||||
@@ -481,7 +481,7 @@ impl<'src> LexState<'src> {
|
||||
if trimmed.len() < 4 || !trimmed.bytes().all(|b| b == b'-') {
|
||||
return false;
|
||||
}
|
||||
// SPEC §9: four or more dashes. Allow surrounding whitespace.
|
||||
// Four or more dashes. Allow surrounding whitespace.
|
||||
let span = Span::new(self.line_start_pos(), self.line_end_pos(line));
|
||||
self.push(VimwikiTokenKind::HorizontalRule, span);
|
||||
self.emit_newline(line);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::ast::DocumentNode;
|
||||
use crate::syntax::{Lexer, Parser, SyntaxPlugin};
|
||||
|
||||
/// SyntaxPlugin facade: chains `VimwikiLexer` + `VimwikiParser` so the
|
||||
/// registry can hand out a single trait object per SPEC.md §6.3.
|
||||
/// registry can hand out a single trait object.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct VimwikiSyntax;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Vimwiki parser.
|
||||
//!
|
||||
//! Hand-rolled recursive-descent over `Vec<VimwikiToken>` (SPEC.md §6.7).
|
||||
//! Hand-rolled recursive-descent over `Vec<VimwikiToken>`.
|
||||
//! Resilient: never aborts the document. Anything the dispatcher can't
|
||||
//! claim becomes a `BlockNode::Error(ErrorNode)` so progress is
|
||||
//! guaranteed.
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! `[[ ]]` payloads are inspected to choose between `WikiLinkNode`
|
||||
//! (the default) and `ExternalLinkNode` (when the target is a URL). The
|
||||
//! `LinkTarget` enum follows SPEC §9: leading `/` is root-relative,
|
||||
//! `LinkTarget` enum follows the vimwiki conventions: leading `/` is root-relative,
|
||||
//! `//` is filesystem-absolute, `wiki<N>:` / `wn.<Name>:` are interwiki,
|
||||
//! `diary:` / `file:` / `local:` are their own kinds, and a trailing `/`
|
||||
//! marks a directory link.
|
||||
@@ -69,10 +69,10 @@ impl Parser for VimwikiParser {
|
||||
struct ParseState<'a> {
|
||||
toks: &'a [VimwikiToken],
|
||||
pos: usize,
|
||||
/// Phase 12: count of `HeadingNode`s already pushed to `children`.
|
||||
/// Count of `HeadingNode`s already pushed to `children`.
|
||||
/// `headings_seen - 1` is the most recent heading's index.
|
||||
headings_seen: usize,
|
||||
/// Phase 12: source line of the most recently-emitted heading.
|
||||
/// Source line of the most recently-emitted heading.
|
||||
/// `tag_line - last_heading_line ∈ {1, 2}` is a header-scope tag.
|
||||
last_heading_line: Option<u32>,
|
||||
}
|
||||
@@ -141,7 +141,7 @@ impl<'a> ParseState<'a> {
|
||||
}
|
||||
let before = self.pos;
|
||||
let block = self.parse_block();
|
||||
// Phase 12: file-scope tags get accumulated into metadata so
|
||||
// File-scope tags get accumulated into metadata so
|
||||
// callers can read page-level tag membership without walking
|
||||
// the AST. The TagNode itself stays in `children` so renderers
|
||||
// see it too.
|
||||
@@ -280,7 +280,7 @@ impl<'a> ParseState<'a> {
|
||||
let inline = parse_inline_seq(&self.toks[inline_start..self.pos]);
|
||||
self.advance();
|
||||
self.eat_newline();
|
||||
// Record for tag-scope resolution (Phase 12).
|
||||
// Record for tag-scope resolution.
|
||||
self.headings_seen += 1;
|
||||
self.last_heading_line = Some(span_end.line);
|
||||
return BlockNode::Heading(HeadingNode {
|
||||
|
||||
@@ -136,7 +136,7 @@ fn registry_can_parse_through_trait_object() {
|
||||
}
|
||||
|
||||
/// Compile-time check: SyntaxRegistry must be Send + Sync so the LSP server
|
||||
/// can share it across handler tasks (SPEC.md §6.3).
|
||||
/// can share it across handler tasks.
|
||||
#[test]
|
||||
fn registry_is_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 12 tag tests: lexer + parser + renderer behaviour on
|
||||
//! Tag tests: lexer + parser + renderer behaviour on
|
||||
//! `:tag:` lines, plus the scope-detection rules.
|
||||
|
||||
use nuwiki_core::ast::{BlockNode, TagScope};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! `workspace/executeCommand` dispatcher.
|
||||
//!
|
||||
//! Phase 13 introduced the router + `nuwiki.file.delete`. Phase 14 added
|
||||
//! the cheap surgical edits — checkbox toggle/cycle/reject, heading
|
||||
//! promote/demote, "next task" navigation. Phase 15 layers on the
|
||||
//! generation + workspace-query commands: `toc.generate`,
|
||||
//! `links.generate`, `workspace.checkLinks`, `workspace.findOrphans`.
|
||||
//! The router handles `nuwiki.file.delete`, the cheap surgical edits —
|
||||
//! checkbox toggle/cycle/reject, heading promote/demote, "next task"
|
||||
//! navigation — and the generation + workspace-query commands:
|
||||
//! `toc.generate`, `links.generate`, `workspace.checkLinks`,
|
||||
//! `workspace.findOrphans`.
|
||||
//!
|
||||
//! Every command resolves to either a `WorkspaceEdit` (the server then
|
||||
//! calls `apply_edit`) or a JSON `Value` (returned to the client) — see
|
||||
@@ -87,7 +87,17 @@ pub(crate) async fn execute(
|
||||
Ok(list_checkbox(backend, args, ops::toggle_state)?.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.list.cycleCheckbox" => {
|
||||
Ok(list_checkbox(backend, args, ops::cycle_state)?.map(CommandOutcome::Edit))
|
||||
let reverse = args
|
||||
.first()
|
||||
.and_then(|v| v.get("reverse"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let mutate = if reverse {
|
||||
ops::cycle_state_back
|
||||
} else {
|
||||
ops::cycle_state
|
||||
};
|
||||
Ok(list_checkbox(backend, args, mutate)?.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.list.rejectCheckbox" => {
|
||||
Ok(list_checkbox(backend, args, ops::reject_state)?.map(CommandOutcome::Edit))
|
||||
@@ -653,7 +663,7 @@ fn tags_rebuild(backend: &Backend, args: Vec<Value>) -> Result<Option<Value>, St
|
||||
})))
|
||||
}
|
||||
|
||||
// ===== Phase 18: multi-wiki picker commands =====
|
||||
// ===== multi-wiki picker commands =====
|
||||
|
||||
fn wiki_list_all(backend: &Backend) -> Result<Option<Value>, String> {
|
||||
let wikis = backend.wikis_snapshot();
|
||||
@@ -811,7 +821,7 @@ fn wiki_goto_page(backend: &Backend, args: Vec<Value>) -> Result<Option<Value>,
|
||||
})))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster A: list rewriters =====
|
||||
// ===== Cluster A: list rewriters =====
|
||||
|
||||
fn list_remove_done(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
let p = parse_optional_uri_arg(args.clone())?;
|
||||
@@ -944,7 +954,7 @@ fn list_change_level(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
||||
))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
// ===== Cluster B: table rewriters =====
|
||||
|
||||
fn table_insert(_backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
@@ -1020,7 +1030,7 @@ fn table_move_column(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
||||
))
|
||||
}
|
||||
|
||||
// ===== Phase 17: HTML export commands =====
|
||||
// ===== HTML export commands =====
|
||||
|
||||
fn export_current(
|
||||
backend: &Backend,
|
||||
@@ -1233,6 +1243,20 @@ pub mod ops {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact inverse of [`cycle_state`] — used by the `glp` mapping
|
||||
/// ("cycle the checkbox state backward").
|
||||
pub fn cycle_state_back(current: &str) -> Option<&'static str> {
|
||||
match current {
|
||||
"[ ]" => Some("[X]"),
|
||||
"[X]" => Some("[O]"),
|
||||
"[O]" => Some("[o]"),
|
||||
"[o]" => Some("[.]"),
|
||||
"[.]" => Some("[ ]"),
|
||||
"[-]" => Some("[ ]"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the `[-]` "rejected" state.
|
||||
pub fn reject_state(current: &str) -> Option<&'static str> {
|
||||
match current {
|
||||
@@ -1647,7 +1671,7 @@ pub mod ops {
|
||||
None
|
||||
}
|
||||
|
||||
// ===== Phase 15: TOC + Links generation, workspace queries =====
|
||||
// ===== TOC + Links generation, workspace queries =====
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -1973,7 +1997,7 @@ pub mod ops {
|
||||
/// on the diagnostics module's internals.
|
||||
pub use crate::diagnostics::collect_wiki_links;
|
||||
|
||||
// ===== §13.1 Cluster A: list rewriters =====
|
||||
// ===== Cluster A: list rewriters =====
|
||||
|
||||
use crate::edits::text_edit_delete;
|
||||
use nuwiki_core::ast::ListSymbol;
|
||||
@@ -2095,7 +2119,9 @@ pub mod ops {
|
||||
|
||||
/// Delete every checkbox item whose state is `Done` or `Rejected`,
|
||||
/// cascading into their sublists. When `pos` is `Some`, restrict to
|
||||
/// the list containing `pos`'s line; otherwise sweep the whole doc.
|
||||
/// the contiguous list block containing `pos`'s line (the "current
|
||||
/// list" — including its nested sublists); otherwise sweep the whole
|
||||
/// doc.
|
||||
pub fn remove_done_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
@@ -2103,16 +2129,8 @@ pub mod ops {
|
||||
pos: Option<LspPosition>,
|
||||
utf8: bool,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let restrict_line = pos.map(|p| p.line);
|
||||
let mut victims: Vec<Span> = Vec::new();
|
||||
walk_list_items(&ast.children, &mut |item| {
|
||||
let matches_scope = match restrict_line {
|
||||
None => true,
|
||||
Some(line) => (item.span.start.line..=item.span.end.line).contains(&line),
|
||||
};
|
||||
if !matches_scope {
|
||||
return;
|
||||
}
|
||||
let mut collect = |item: &ListItemNode| {
|
||||
if matches!(
|
||||
item.checkbox,
|
||||
Some(nuwiki_core::ast::CheckboxState::Done)
|
||||
@@ -2120,7 +2138,19 @@ pub mod ops {
|
||||
) {
|
||||
victims.push(extend_to_line_end(text, item.span));
|
||||
}
|
||||
});
|
||||
};
|
||||
match pos {
|
||||
None => walk_list_items(&ast.children, &mut collect),
|
||||
Some(p) => {
|
||||
// "Current list" = the top-level contiguous list block the
|
||||
// cursor sits in. Walk just that block (and its sublists),
|
||||
// leaving done items in sibling lists untouched.
|
||||
let list = find_top_list_at_line(ast, p.line)?;
|
||||
for item in &list.items {
|
||||
walk_list_items_in_item(item, &mut collect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if victims.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -2131,6 +2161,40 @@ pub mod ops {
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
/// Find the top-level (contiguous) list block whose span contains
|
||||
/// `line`. Unlike [`find_list_at_line`], this does not descend into
|
||||
/// sublists — it returns the outermost list so callers operate on the
|
||||
/// whole list block the cursor belongs to.
|
||||
fn find_top_list_at_line(ast: &DocumentNode, line: u32) -> Option<&ListNode> {
|
||||
for block in &ast.children {
|
||||
if let Some(list) = find_top_list_in_block(block, line) {
|
||||
return Some(list);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_top_list_in_block(block: &BlockNode, line: u32) -> Option<&ListNode> {
|
||||
match block {
|
||||
BlockNode::List(list) => {
|
||||
if (list.span.start.line..=list.span.end.line).contains(&line) {
|
||||
Some(list)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
BlockNode::Blockquote(BlockquoteNode { children, .. }) => {
|
||||
for c in children {
|
||||
if let Some(l) = find_top_list_in_block(c, line) {
|
||||
return Some(l);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend a span to include the trailing newline (so deletion removes
|
||||
/// the empty line that would otherwise be left behind).
|
||||
fn extend_to_line_end(text: &str, span: Span) -> Span {
|
||||
@@ -2480,7 +2544,7 @@ pub mod ops {
|
||||
Some((span, marker_str, i))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
// ===== Cluster B: table rewriters =====
|
||||
|
||||
use nuwiki_core::ast::{TableNode, TableRowNode};
|
||||
|
||||
@@ -2738,7 +2802,7 @@ pub mod ops {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
// ===== Phase 16: diary helpers =====
|
||||
// ===== diary helpers =====
|
||||
|
||||
use crate::edits::op_create;
|
||||
|
||||
@@ -2792,10 +2856,10 @@ pub mod ops {
|
||||
b.build()
|
||||
}
|
||||
|
||||
// ===== Tag ops (Phase 12 commands) =====
|
||||
// ===== Tag ops =====
|
||||
//
|
||||
// SPEC §12.3 calls for three tag-driven commands beyond the indexing
|
||||
// that Phase 12 already shipped. `tag_hits` powers `nuwiki.tags.search`;
|
||||
// Three tag-driven commands beyond the indexing itself.
|
||||
// `tag_hits` powers `nuwiki.tags.search`;
|
||||
// `tag_links_edit` powers `nuwiki.tags.generateLinks`; the rebuild
|
||||
// command is a direct re-walk of the wiki root in the dispatcher.
|
||||
|
||||
@@ -2984,7 +3048,7 @@ pub mod ops {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disk-touching helpers for the Phase 17 `nuwiki.export.*` commands.
|
||||
/// Disk-touching helpers for the `nuwiki.export.*` commands.
|
||||
/// Kept out of `ops` because everything here is side-effecting — pure
|
||||
/// rendering / templating lives in [`crate::export`].
|
||||
pub mod export_ops {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//! Server-side runtime config.
|
||||
//!
|
||||
//! Populated from `InitializeParams.initialization_options` at boot and
|
||||
//! refreshed via `workspace/didChangeConfiguration` (P18). v1.0 single-wiki
|
||||
//! shapes are accepted and desugared into the v1.1 `wikis = [...]` form so
|
||||
//! existing setups keep working (P16).
|
||||
//! refreshed via `workspace/didChangeConfiguration`. Legacy single-wiki
|
||||
//! shapes are accepted and desugared into the `wikis = [...]` form so
|
||||
//! existing setups keep working.
|
||||
//!
|
||||
//! Only the fields actually consumed in Phase 11 land here. Phases 13–19
|
||||
//! extend `WikiConfig` (diary path, html path, template options, …) and
|
||||
//! the top-level `Config` (diagnostic severity, mappings opt-in, …) as
|
||||
//! they need them.
|
||||
//! `WikiConfig` (diary path, html path, template options, …) and the
|
||||
//! top-level `Config` (diagnostic severity, mappings opt-in, …) carry the
|
||||
//! fields actually consumed by the server.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -39,9 +38,8 @@ pub struct WikiConfig {
|
||||
/// The full path is `<root>/<diary_rel_path>/<diary_index>.wiki`.
|
||||
pub diary_index: String,
|
||||
/// `daily`/`weekly`/`monthly`/`yearly`. Picks the date format the
|
||||
/// diary commands use. v1.1 ships `daily`; the others fall through
|
||||
/// to vimwiki-style names but the commands are no-ops until
|
||||
/// Cluster 4 lands.
|
||||
/// diary commands use. `daily` is supported; the others fall through
|
||||
/// to vimwiki-style names but the commands are no-ops for them.
|
||||
pub diary_frequency: String,
|
||||
/// First day of the week (`monday`..`sunday`). Used by weekly
|
||||
/// diary entry naming.
|
||||
@@ -74,7 +72,7 @@ pub struct WikiConfig {
|
||||
pub nested_syntaxes: std::collections::HashMap<String, String>,
|
||||
/// Rebuild the page's TOC on save when set.
|
||||
pub auto_toc: bool,
|
||||
/// Phase 17 HTML export options. See SPEC §12.8.
|
||||
/// HTML export options.
|
||||
pub html: HtmlConfig,
|
||||
}
|
||||
|
||||
@@ -104,7 +102,7 @@ pub struct HtmlConfig {
|
||||
/// to skip during `allToHtml*`.
|
||||
pub exclude_files: Vec<String>,
|
||||
/// `color_dic`: vimwiki colour-tag name → CSS value mapping used
|
||||
/// by the HTML renderer (SPEC §12.11). Empty by default; the
|
||||
/// by the HTML renderer. Empty by default; the
|
||||
/// renderer falls back to `class="color-<name>"` when a name
|
||||
/// isn't in the dict.
|
||||
pub color_dic: std::collections::HashMap<String, String>,
|
||||
@@ -130,7 +128,7 @@ impl Default for HtmlConfig {
|
||||
impl HtmlConfig {
|
||||
/// Synthesise default paths under `root` when the user hasn't
|
||||
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
||||
/// and `template_path` to `<root>/_templates` per SPEC §12.8.
|
||||
/// and `template_path` to `<root>/_templates`.
|
||||
pub fn from_root(root: &Path) -> Self {
|
||||
Self {
|
||||
html_path: root.join("_html"),
|
||||
@@ -284,7 +282,7 @@ fn default_links_space_char() -> String {
|
||||
" ".to_string()
|
||||
}
|
||||
|
||||
/// Fill in the v1.1 per-wiki keys that none of the constructors care
|
||||
/// Fill in the per-wiki keys that none of the constructors care
|
||||
/// about. Centralising the defaults here keeps `empty()`, `from_root()`,
|
||||
/// the legacy single-wiki path, and `From<RawWiki>` in sync.
|
||||
fn wiki_defaults() -> WikiDefaults {
|
||||
@@ -329,9 +327,9 @@ impl Config {
|
||||
/// Build a `Config` from `InitializeParams`.
|
||||
///
|
||||
/// Priority for the wikis list:
|
||||
/// 1. `initialization_options.wikis = [...]` (v1.1 shape)
|
||||
/// 1. `initialization_options.wikis = [...]` (multi-wiki shape)
|
||||
/// 2. `initialization_options.wiki_root + file_extension + syntax`
|
||||
/// (v1.0 single-wiki shape, P16)
|
||||
/// (legacy single-wiki shape)
|
||||
/// 3. `workspace_folders[0]` if present
|
||||
/// 4. deprecated `root_uri` if present
|
||||
/// 5. empty list (server stays alive but won't index anything)
|
||||
@@ -445,7 +443,7 @@ mod opt_bool_or_int {
|
||||
#[serde(default, rename_all = "snake_case")]
|
||||
struct InitOptions {
|
||||
wikis: Option<Vec<RawWiki>>,
|
||||
// v1.0 single-wiki compat fields
|
||||
// legacy single-wiki compat fields
|
||||
wiki_root: Option<String>,
|
||||
file_extension: Option<String>,
|
||||
syntax: Option<String>,
|
||||
@@ -466,7 +464,7 @@ struct RawWiki {
|
||||
diary_rel_path: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_index: Option<String>,
|
||||
// Phase 17 HTML keys — all optional, fall back to per-root defaults.
|
||||
// HTML keys — all optional, fall back to per-root defaults.
|
||||
#[serde(default)]
|
||||
html_path: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -487,7 +485,7 @@ struct RawWiki {
|
||||
exclude_files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
color_dic: Option<std::collections::HashMap<String, String>>,
|
||||
// v1.1 per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// existing single-wiki configs migrate without touching anything.
|
||||
#[serde(default)]
|
||||
index: Option<String>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Diagnostic sources beyond parse-time `ErrorNode`s.
|
||||
//!
|
||||
//! Phase 15 lands the `nuwiki.link` source — broken-link warnings. Walks
|
||||
//! The `nuwiki.link` source emits broken-link warnings. Walks
|
||||
//! every `WikiLinkNode` in the AST and emits one diagnostic per:
|
||||
//! - `Wiki` target that doesn't resolve to a page in the workspace index,
|
||||
//! - `Wiki`/`AnchorOnly` target with an anchor that matches neither a
|
||||
@@ -8,7 +8,7 @@
|
||||
//! - `File`/`Local` target whose resolved path doesn't exist on disk.
|
||||
//!
|
||||
//! Interwiki, Diary, Raw, and external links are not diagnosed —
|
||||
//! interwiki resolution is Phase 18's job, diary is Phase 16's, and we
|
||||
//! interwiki resolution and diary handling live elsewhere, and we
|
||||
//! don't fetch URLs.
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -332,8 +332,7 @@ fn resolve_file_path(
|
||||
index.root.as_ref().map(|r| r.join(&candidate))
|
||||
}
|
||||
|
||||
/// `nuwiki.link` diagnostics for a single document. Phase 11 left this as
|
||||
/// a stub; Phase 15 fills it in.
|
||||
/// `nuwiki.link` diagnostics for a single document.
|
||||
pub fn link_health(
|
||||
ast: &DocumentNode,
|
||||
text: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Diary subsystem (Phase 16) — pure helpers used by the
|
||||
//! Diary subsystem — pure helpers used by the
|
||||
//! `nuwiki.diary.*` `executeCommand` handlers.
|
||||
//!
|
||||
//! All path/URI math lives here so the command dispatcher stays a thin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Helpers for building `WorkspaceEdit`s.
|
||||
//!
|
||||
//! Phase 13+ (rename, list/table edits, link/TOC generation) all return
|
||||
//! Rename, list/table edits, and link/TOC generation all return
|
||||
//! `WorkspaceEdit`. The builder centralises the byte-offset → LSP-position
|
||||
//! conversion (UTF-8 or UTF-16, matching whatever was negotiated in
|
||||
//! `initialize`) so each call site doesn't have to redo it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! HTML export — pure helpers shared by the Phase 17 `nuwiki.export.*`
|
||||
//! HTML export — pure helpers shared by the `nuwiki.export.*`
|
||||
//! command handlers. Side-effecting work (reading the template from
|
||||
//! disk, writing the rendered HTML, copying CSS) lives in `commands.rs`
|
||||
//! since the dispatcher is the one with a `&Backend` and async context.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `textDocument/foldingRange` provider — SPEC §12.10 / P14.
|
||||
//! `textDocument/foldingRange` provider.
|
||||
//!
|
||||
//! Strategy: one fold per heading (line → line before the next heading
|
||||
//! of same-or-higher level) plus one fold per top-level list block.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Workspace index — the in-memory model of every `.wiki` page nuwiki
|
||||
//! has seen, used to power Phase 8 nav features.
|
||||
//! has seen, used to power nav features.
|
||||
//!
|
||||
//! Per P8 (SPEC §11), the initial scan runs as a background tokio task so
|
||||
//! The initial scan runs as a background tokio task so
|
||||
//! the server stays responsive on startup. Per-document updates land here
|
||||
//! synchronously via `upsert` whenever the LSP backend re-parses on
|
||||
//! `didOpen` / `didChange`.
|
||||
@@ -29,10 +29,10 @@ pub struct IndexedPage {
|
||||
pub title: Option<String>,
|
||||
pub headings: Vec<HeadingInfo>,
|
||||
pub outgoing: Vec<OutgoingLink>,
|
||||
/// Phase 12: every `TagNode` on the page. `tags_by_name` on the
|
||||
/// Every `TagNode` on the page. `tags_by_name` on the
|
||||
/// containing `WorkspaceIndex` is the reverse map.
|
||||
pub tags: Vec<TagInfo>,
|
||||
/// Phase 16: `Some(date)` when this page is a *daily* diary entry —
|
||||
/// `Some(date)` when this page is a *daily* diary entry —
|
||||
/// stem parses as `YYYY-MM-DD`. Equivalent to
|
||||
/// `diary_period.and_then(|p| if let DiaryPeriod::Day(d) = p { Some(d) } else { None })`.
|
||||
/// Kept as its own field for back-compat with prev/next/list callers
|
||||
@@ -97,7 +97,7 @@ pub struct Backlink {
|
||||
}
|
||||
|
||||
/// A tag's location, used by `WorkspaceIndex::tags_for` for cross-page
|
||||
/// `[[Page#tag]]` resolution and `nuwiki.tags.search` (Phase 13).
|
||||
/// `[[Page#tag]]` resolution and `nuwiki.tags.search`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TagOccurrence {
|
||||
pub uri: Url,
|
||||
@@ -108,7 +108,7 @@ pub struct TagOccurrence {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WorkspaceIndex {
|
||||
pub root: Option<PathBuf>,
|
||||
/// Phase 16: subdir relative to `root` that holds diary entries.
|
||||
/// Subdir relative to `root` that holds diary entries.
|
||||
/// Mirrors `WikiConfig::diary_rel_path`; stored here so `upsert` can
|
||||
/// classify each indexed URI without a back-reference to the config.
|
||||
pub diary_rel_path: Option<String>,
|
||||
@@ -119,9 +119,9 @@ pub struct WorkspaceIndex {
|
||||
pub pages_by_uri: HashMap<Url, IndexedPage>,
|
||||
pub pages_by_name: HashMap<String, Url>,
|
||||
pub backlinks: HashMap<String, Vec<Backlink>>,
|
||||
/// Phase 12: tag name → every occurrence across the workspace. Used by
|
||||
/// the v1.1 nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
||||
/// inclusion) and the Phase 13 `nuwiki.tags.search` command.
|
||||
/// Tag name → every occurrence across the workspace. Used by
|
||||
/// the nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
||||
/// inclusion) and the `nuwiki.tags.search` command.
|
||||
pub tags_by_name: HashMap<String, Vec<TagOccurrence>>,
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ impl WorkspaceIndex {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 12: maintain the reverse tag map alongside the per-page list.
|
||||
// Maintain the reverse tag map alongside the per-page list.
|
||||
for tag in &page.tags {
|
||||
self.tags_by_name
|
||||
.entry(tag.name.clone())
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! LSP protocol bridge for nuwiki.
|
||||
//!
|
||||
//! `tower-lsp` server that maintains a `DocumentStore` (SPEC §6.10) plus a
|
||||
//! workspace-wide index (SPEC §6.10 + P8) and exposes every LSP method
|
||||
//! through Phase 8:
|
||||
//! `tower-lsp` server that maintains a `DocumentStore` plus a
|
||||
//! workspace-wide index and exposes every LSP method:
|
||||
//!
|
||||
//! - `initialize` / `initialized` / `shutdown`
|
||||
//! - `textDocument/didOpen`, `didChange` (full sync), `didClose`
|
||||
@@ -14,12 +13,12 @@
|
||||
//! - `workspace/symbol` — search across indexed headings
|
||||
//!
|
||||
//! Position encoding is negotiated as UTF-8 when the client supports LSP
|
||||
//! 3.17+ encodings (SPEC §6.11). When the client only supports the legacy
|
||||
//! 3.17+ encodings. When the client only supports the legacy
|
||||
//! UTF-16 default, positions get translated through a per-line lookup.
|
||||
//!
|
||||
//! Re-parses are full per change (incremental parsing deferred post-v1).
|
||||
//! Re-parses are full per change (no incremental parsing).
|
||||
//! The workspace scan runs in a background tokio task spawned from
|
||||
//! `initialize` (P8) with `window/workDoneProgress` begin/end events.
|
||||
//! `initialize` with `window/workDoneProgress` begin/end events.
|
||||
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
@@ -111,8 +110,8 @@ pub async fn run_stdio() -> io::Result<()> {
|
||||
struct DocumentState {
|
||||
text: String,
|
||||
ast: DocumentNode,
|
||||
/// Last version we observed from the client. Held per SPEC §6.10 even
|
||||
/// though the foundation phase doesn't yet need to read it back.
|
||||
/// Last version we observed from the client. Held even
|
||||
/// though nothing reads it back yet.
|
||||
#[allow(dead_code)]
|
||||
version: i32,
|
||||
}
|
||||
@@ -124,8 +123,8 @@ struct Backend {
|
||||
/// True after `initialize` if the client opted into UTF-8 encoding.
|
||||
/// Otherwise we keep the LSP 3.16 default of UTF-16.
|
||||
use_utf8: Arc<AtomicBool>,
|
||||
/// Per-wiki state (P11). Vector holds one entry until Phase 18
|
||||
/// (multi-wiki) lets users add more.
|
||||
/// Per-wiki state. Vector holds one entry until multi-wiki
|
||||
/// support lets users add more.
|
||||
wikis: Arc<RwLock<Vec<Wiki>>>,
|
||||
/// Server config received via `initializationOptions` and refreshed
|
||||
/// via `workspace/didChangeConfiguration` (P18).
|
||||
@@ -151,7 +150,7 @@ impl Backend {
|
||||
wikis.iter().find(|w| w.id == id).cloned()
|
||||
}
|
||||
|
||||
/// Used by Phase 15 workspace-level commands (`checkLinks`, `findOrphans`)
|
||||
/// Used by workspace-level commands (`checkLinks`, `findOrphans`)
|
||||
/// when no URI is supplied — they fall back to the first registered wiki.
|
||||
fn default_wiki(&self) -> Option<Wiki> {
|
||||
let wikis = self.wikis.read().ok()?;
|
||||
@@ -290,7 +289,7 @@ impl Backend {
|
||||
|
||||
/// Return live state if `uri` is open in the editor, otherwise read
|
||||
/// the file from disk and re-parse. The closed-doc path is used by
|
||||
/// cross-document operations (Phase 13 `workspace/rename` and link
|
||||
/// cross-document operations (`workspace/rename` and link
|
||||
/// health) that need accurate spans + text without trusting cached
|
||||
/// `WorkspaceIndex` data.
|
||||
pub async fn read_or_open(&self, uri: &Url) -> io::Result<DocSnapshot> {
|
||||
@@ -318,7 +317,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
async fn update_document(&self, uri: Url, text: String, version: i32) {
|
||||
// For Phase 6 we always treat unknown buffers as vimwiki — if/when
|
||||
// We always treat unknown buffers as vimwiki — if/when
|
||||
// multi-syntax dispatch lands the pick should follow the URI's ext.
|
||||
let plugin = match self.registry.get("vimwiki") {
|
||||
Some(p) => p,
|
||||
@@ -373,7 +372,7 @@ impl Backend {
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||
// SPEC §6.11: prefer UTF-8 if the client advertises support.
|
||||
// Prefer UTF-8 if the client advertises support.
|
||||
let supports_utf8 = params
|
||||
.capabilities
|
||||
.general
|
||||
@@ -404,7 +403,7 @@ impl LanguageServer for Backend {
|
||||
change: Some(TextDocumentSyncKind::FULL),
|
||||
will_save: None,
|
||||
will_save_wait_until: None,
|
||||
// Phase 17 `auto_export` listens on `did_save`. Asking
|
||||
// `auto_export` listens on `did_save`. Asking
|
||||
// for `include_text: false` since the document store
|
||||
// already mirrors the live buffer.
|
||||
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
||||
@@ -502,9 +501,9 @@ impl LanguageServer for Backend {
|
||||
|
||||
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||
// Apply the new settings to `Config`. Re-indexing on root changes
|
||||
// is out of scope for Phase 11; for now we just accept the new
|
||||
// is out of scope here; for now we just accept the new
|
||||
// values so later commands see the right diagnostic settings etc.
|
||||
// Phase 18 (multi-wiki) revisits this with proper rebuild semantics.
|
||||
// Multi-wiki support revisits this with proper rebuild semantics.
|
||||
let mut cfg = self.config.write().expect("config lock poisoned");
|
||||
cfg.apply_change(¶ms.settings);
|
||||
}
|
||||
@@ -530,7 +529,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||
// Phase 13 advertises this capability so the editor pushes
|
||||
// We advertise this capability so the editor pushes
|
||||
// out-of-band renames (file explorer drags, `:VimwikiRenameFile`
|
||||
// when the client opts to surface the new URI). Update the index
|
||||
// so backlinks/headings track the new URI without waiting for
|
||||
@@ -576,7 +575,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
// Phase 17: when `auto_export` is set on the resolved wiki, run
|
||||
// When `auto_export` is set on the resolved wiki, run
|
||||
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
||||
// hits disk. Best-effort — failures are logged but do not bubble
|
||||
// to the client. Skips pages with the `%nohtml` directive.
|
||||
@@ -934,7 +933,7 @@ impl LanguageServer for Backend {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Phase 12: surface tags too, named as `:tag:` so editors
|
||||
// Surface tags too, named as `:tag:` so editors
|
||||
// can distinguish them from headings at a glance.
|
||||
for t in &page.tags {
|
||||
if q.is_empty() || t.name.to_lowercase().contains(&q) {
|
||||
@@ -1121,9 +1120,9 @@ fn utf16_column(text: &str, line: u32, byte_col: u32) -> u32 {
|
||||
.sum::<usize>() as u32
|
||||
}
|
||||
|
||||
/// Backward-compat wrapper kept for v1.0 test surface. New call sites
|
||||
/// Backward-compat wrapper kept for the legacy test surface. New call sites
|
||||
/// should use `collect_diagnostics` so they can opt into link-health
|
||||
/// (Phase 15) and other sources.
|
||||
/// and other sources.
|
||||
pub fn ast_diagnostics(ast: &DocumentNode, text: &str, utf8: bool) -> Vec<Diagnostic> {
|
||||
collect_diagnostics(
|
||||
ast,
|
||||
@@ -1136,12 +1135,12 @@ pub fn ast_diagnostics(ast: &DocumentNode, text: &str, utf8: bool) -> Vec<Diagno
|
||||
)
|
||||
}
|
||||
|
||||
/// Composable diagnostic collector (P11 §12.2.4).
|
||||
/// Composable diagnostic collector.
|
||||
///
|
||||
/// Sources, each gated by `cfg`:
|
||||
///
|
||||
/// 1. Parse errors (always on).
|
||||
/// 2. Broken-link warnings (Phase 15) — emitted when an index, source
|
||||
/// 2. Broken-link warnings — emitted when an index, source
|
||||
/// page name, and `link_severity != Off` are all available.
|
||||
pub fn collect_diagnostics(
|
||||
ast: &DocumentNode,
|
||||
@@ -1317,8 +1316,8 @@ fn keyword_str(k: nuwiki_core::ast::Keyword) -> &'static str {
|
||||
|
||||
// ===== Workspace + navigation helpers =====
|
||||
//
|
||||
// `workspace_root_from_params` lived here through Phase 10; Phase 11 lifted
|
||||
// that logic into `Config::from_init_params` (see `config.rs`) so workspace
|
||||
// `workspace_root_from_params` logic now lives in
|
||||
// `Config::from_init_params` (see `config.rs`) so workspace
|
||||
// folders and `root_uri` are handled alongside `initializationOptions`.
|
||||
|
||||
/// Background workspace scan (P8). Walks every `.wiki` file under `root`,
|
||||
@@ -1469,7 +1468,7 @@ fn heading_range_in(idx: &WorkspaceIndex, uri: &Url, anchor: &str, _utf8: bool)
|
||||
if let Some(h) = page.find_heading_by_anchor(anchor) {
|
||||
return Some(span_to_lsp_range_no_text(&h.span));
|
||||
}
|
||||
// Phase 12: tags-as-anchors. `[[Page#tag-name]]` lands on the
|
||||
// Tags-as-anchors. `[[Page#tag-name]]` lands on the
|
||||
// matching `TagNode` on the target page.
|
||||
let t = page.tags.iter().find(|t| t.name == anchor)?;
|
||||
Some(span_to_lsp_range_no_text(&t.span))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Helpers shared by the Phase 8 navigation handlers (definition,
|
||||
//! Helpers shared by the navigation handlers (definition,
|
||||
//! references, hover, completion).
|
||||
//!
|
||||
//! Two responsibilities:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! `textDocument/rename` — server-side rename with cross-document link
|
||||
//! rewriting.
|
||||
//!
|
||||
//! Phase 13 implementation:
|
||||
//! Implementation:
|
||||
//! 1. Resolve the rename target — cursor on a wikilink renames the linked
|
||||
//! page; otherwise rename the current file.
|
||||
//! 2. Build the new URI from the wiki root + the user-supplied path.
|
||||
//! 3. For every page in the wiki's index with an outgoing link to the
|
||||
//! old page name, read its source via the Phase 11 closed-doc loader
|
||||
//! old page name, read its source via the closed-doc loader
|
||||
//! so cached spans are validated against current text, then emit a
|
||||
//! `TextEdit` that swaps the `[[old]]` for `[[new]]` while preserving
|
||||
//! anchors and descriptions.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Semantic-token emission for the vimwiki AST.
|
||||
//!
|
||||
//! Token-type scheme settled in P7: custom `vimwiki*` types plus
|
||||
//! `level1`..`level6` + `centered` modifiers (SPEC §11). Default highlight
|
||||
//! groups for these arrive in Phase 9.
|
||||
//! Token-type scheme: custom `vimwiki*` types plus
|
||||
//! `level1`..`level6` + `centered` modifiers.
|
||||
//!
|
||||
//! Pipeline:
|
||||
//!
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! `Wiki` aggregate — per-wiki state, even when there's only one.
|
||||
//!
|
||||
//! v1.0 shipped with a single `Arc<RwLock<WorkspaceIndex>>` on `Backend`.
|
||||
//! Phase 11 lifts that into a `Wiki` aggregate so Phase 18 (multi-wiki)
|
||||
//! only changes config shape, not data flow. Phases 12–17 always operate
|
||||
//! on a `Wiki`; in practice the `wikis` `Vec` has one entry until 18.
|
||||
//! The single `Arc<RwLock<WorkspaceIndex>>` on `Backend` is lifted into a
|
||||
//! `Wiki` aggregate so multi-wiki support only changes config shape, not
|
||||
//! data flow. Handlers always operate on a `Wiki`; in practice the `wikis`
|
||||
//! `Vec` has one entry until multi-wiki support lands.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
@@ -47,6 +47,24 @@ fn cycle_state_walks_progression() {
|
||||
assert_eq!(ops::cycle_state("[-]"), Some("[ ]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_state_back_is_exact_inverse() {
|
||||
// `glp` walks the progression in reverse — every step must undo the
|
||||
// matching `cycle_state` step.
|
||||
assert_eq!(ops::cycle_state_back("[ ]"), Some("[X]"));
|
||||
assert_eq!(ops::cycle_state_back("[X]"), Some("[O]"));
|
||||
assert_eq!(ops::cycle_state_back("[O]"), Some("[o]"));
|
||||
assert_eq!(ops::cycle_state_back("[o]"), Some("[.]"));
|
||||
assert_eq!(ops::cycle_state_back("[.]"), Some("[ ]"));
|
||||
assert_eq!(ops::cycle_state_back("[-]"), Some("[ ]"));
|
||||
// Composing forward then backward returns to the start for every
|
||||
// in-cycle marker.
|
||||
for s in ["[ ]", "[.]", "[o]", "[O]", "[X]"] {
|
||||
let fwd = ops::cycle_state(s).unwrap();
|
||||
assert_eq!(ops::cycle_state_back(fwd), Some(s));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_state_toggles_dash_marker() {
|
||||
assert_eq!(ops::reject_state("[ ]"), Some("[-]"));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! §13.1 §13.3 — `color_dic` → `HtmlRenderer` integration.
|
||||
//! `color_dic` → `HtmlRenderer` integration.
|
||||
//!
|
||||
//! Drives the renderer directly so we don't have to spin up the full
|
||||
//! export pipeline. The end-to-end path (export command → renderer
|
||||
//! with `cfg.color_dic` plumbed through) is integration-tested via the
|
||||
//! Phase 17 export tests.
|
||||
//! export tests.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
//! Command-coverage suite: at least one behavioural test for every
|
||||
//! documented `:Nuwiki*` / `:Vimwiki*` command (see the COMMANDS section
|
||||
//! of `doc/nuwiki.txt`).
|
||||
//!
|
||||
//! The user-facing commands funnel through `workspace/executeCommand`
|
||||
//! into the Backend dispatcher, or — for follow/backlinks/rename — through
|
||||
//! the standard LSP requests. The Backend itself owns a live `Client` and
|
||||
//! can't be built in an integration test, so each case drives the *pure*
|
||||
//! building-block the command relies on (the same functions the dispatcher
|
||||
//! wraps). This file is organised to mirror the doc's command groups so a
|
||||
//! reader can confirm every command has coverage.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nuwiki_core::ast::{InlineNode, LinkKind, LinkTarget};
|
||||
use nuwiki_core::date::DiaryDate;
|
||||
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||||
use nuwiki_core::syntax::SyntaxPlugin;
|
||||
use nuwiki_lsp::commands::{export_ops, ops, COMMANDS};
|
||||
use nuwiki_lsp::config::WikiConfig;
|
||||
use nuwiki_lsp::index::WorkspaceIndex;
|
||||
use nuwiki_lsp::nav::find_inline_at;
|
||||
use nuwiki_lsp::rename::{build_new_uri, rewrite_wikilink_target};
|
||||
use nuwiki_lsp::wiki::{build_wikis, resolve_uri_to_wiki};
|
||||
use nuwiki_lsp::{diary, export};
|
||||
use tower_lsp::lsp_types::Url;
|
||||
|
||||
fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
||||
VimwikiSyntax::new().parse(src)
|
||||
}
|
||||
|
||||
fn wiki_cfg(root: &str) -> WikiConfig {
|
||||
WikiConfig::from_root(PathBuf::from(root))
|
||||
}
|
||||
|
||||
/// Build a workspace index rooted at `root` with the given `name -> source`
|
||||
/// pages, mirroring the helper used by the other command suites.
|
||||
fn build_index(root: &str, pages: &[(&str, &str)]) -> WorkspaceIndex {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from(root)));
|
||||
for (name, src) in pages {
|
||||
let uri = Url::from_file_path(format!("{root}/{name}.wiki")).unwrap();
|
||||
idx.upsert(uri, &parse(src));
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
fn diary_index(root: &str, dates: &[&str]) -> WorkspaceIndex {
|
||||
let mut idx =
|
||||
WorkspaceIndex::new(Some(PathBuf::from(root))).with_diary_rel_path(Some("diary".into()));
|
||||
for d in dates {
|
||||
let uri = Url::from_file_path(format!("{root}/diary/{d}.wiki")).unwrap();
|
||||
idx.upsert(uri, &parse("= entry =\n"));
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 1: Wiki / navigation
|
||||
// =====================================================================
|
||||
|
||||
// :NuwikiIndex — open wiki N's index page.
|
||||
#[test]
|
||||
fn nuwiki_index_resolves_index_page() {
|
||||
let idx = build_index("/wiki", &[("index", "= Home =\n"), ("Other", "= O =\n")]);
|
||||
let page = idx.page_by_name("index").expect("index page indexed");
|
||||
assert!(page.uri.as_str().ends_with("/wiki/index.wiki"));
|
||||
}
|
||||
|
||||
// :NuwikiTabIndex — same target as :NuwikiIndex, opened in a new tab.
|
||||
// The tab-vs-current split is an editor concern; the resolved target is
|
||||
// identical, and both verbs are advertised as executeCommands.
|
||||
#[test]
|
||||
fn nuwiki_tab_index_advertised_alongside_index() {
|
||||
assert!(COMMANDS.contains(&"nuwiki.wiki.openIndex"));
|
||||
assert!(COMMANDS.contains(&"nuwiki.wiki.tabOpenIndex"));
|
||||
}
|
||||
|
||||
// :NuwikiUISelect — pick a wiki from the configured list.
|
||||
#[test]
|
||||
fn nuwiki_ui_select_lists_configured_wikis() {
|
||||
let cfgs = vec![wiki_cfg("/a/personal"), wiki_cfg("/b/work")];
|
||||
let wikis = build_wikis(&cfgs);
|
||||
assert_eq!(wikis.len(), 2);
|
||||
assert_eq!(wikis[0].config.name, "personal");
|
||||
assert_eq!(wikis[1].config.name, "work");
|
||||
|
||||
// Selection by URI picks the wiki that actually owns the file.
|
||||
let uri = Url::from_file_path("/b/work/Page.wiki").unwrap();
|
||||
assert_eq!(resolve_uri_to_wiki(&wikis, &uri), Some(wikis[1].id));
|
||||
}
|
||||
|
||||
// :NuwikiGoto {page} — open `{page}.wiki` by name.
|
||||
#[test]
|
||||
fn nuwiki_goto_opens_page_by_name() {
|
||||
let idx = build_index("/wiki", &[("index", "= H =\n"), ("Recipes", "= R =\n")]);
|
||||
let page = idx
|
||||
.page_by_name("Recipes")
|
||||
.expect("page resolvable by name");
|
||||
assert!(page.uri.as_str().ends_with("/wiki/Recipes.wiki"));
|
||||
assert!(idx.page_by_name("Missing").is_none());
|
||||
}
|
||||
|
||||
// :NuwikiFollowLink — follow the link under the cursor.
|
||||
#[test]
|
||||
fn nuwiki_follow_link_resolves_target_under_cursor() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[("Home", "see [[About]]\n"), ("About", "= A =\n")],
|
||||
);
|
||||
let doc = parse("see [[About]]\n");
|
||||
// Byte col 8 sits inside "[[About]]" (starts at byte 4).
|
||||
let node = find_inline_at(&doc, 0, 8).expect("wikilink under cursor");
|
||||
let InlineNode::WikiLink(w) = node else {
|
||||
panic!("expected a wikilink, got {node:?}");
|
||||
};
|
||||
let about = Url::from_file_path("/wiki/About.wiki").unwrap();
|
||||
assert_eq!(idx.resolve(&w.target, "Home"), Some(&about));
|
||||
}
|
||||
|
||||
// :NuwikiBacklinks — every reference to the current page.
|
||||
#[test]
|
||||
fn nuwiki_backlinks_lists_referrers() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[
|
||||
("Home", "[[About]]\n"),
|
||||
("Contact", "see [[About]] too\n"),
|
||||
("About", "= A =\n"),
|
||||
],
|
||||
);
|
||||
let back = idx.backlinks_for("About");
|
||||
assert_eq!(back.len(), 2);
|
||||
assert!(idx.backlinks_for("Home").is_empty());
|
||||
}
|
||||
|
||||
// :NuwikiNextLink / :NuwikiPrevLink — jump to the next / previous wikilink.
|
||||
// The jump itself is editor-side cursor movement; what the server provides
|
||||
// is the ordered set of links on the page.
|
||||
#[test]
|
||||
fn nuwiki_next_prev_link_walk_links_in_document_order() {
|
||||
let doc = parse("intro [[First]] mid [[Second]] end [[Third]]\n");
|
||||
let links = ops::collect_wiki_links(&doc);
|
||||
let targets: Vec<&str> = links
|
||||
.iter()
|
||||
.filter_map(|l| l.target.path.as_deref())
|
||||
.collect();
|
||||
assert_eq!(targets, vec!["First", "Second", "Third"]);
|
||||
}
|
||||
|
||||
// :NuwikiBaddLink — add the target of the link under the cursor to the
|
||||
// buffer list. The editor :badds whatever URI the link resolves to.
|
||||
#[test]
|
||||
fn nuwiki_badd_link_resolves_target_uri() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[("Home", "[[Notes/Todo]]\n"), ("Notes/Todo", "= T =\n")],
|
||||
);
|
||||
let target = LinkTarget {
|
||||
kind: LinkKind::Wiki,
|
||||
path: Some("Notes/Todo".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let expected = Url::from_file_path("/wiki/Notes/Todo.wiki").unwrap();
|
||||
assert_eq!(idx.resolve(&target, "Home"), Some(&expected));
|
||||
}
|
||||
|
||||
// :NuwikiRenameFile — rename the page and rewrite every inbound link.
|
||||
#[test]
|
||||
fn nuwiki_rename_file_builds_uri_and_rewrites_links() {
|
||||
let new_uri = build_new_uri(&PathBuf::from("/wiki"), "New Name", ".wiki").unwrap();
|
||||
assert!(new_uri.as_str().ends_with("/wiki/New%20Name.wiki"));
|
||||
|
||||
assert_eq!(
|
||||
rewrite_wikilink_target("[[Old Name|caption]]", "New Name"),
|
||||
Some("[[New Name|caption]]".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// :NuwikiDeleteFile — delete the current page and its on-disk file.
|
||||
#[test]
|
||||
fn nuwiki_delete_file_emits_delete_workspace_edit() {
|
||||
use tower_lsp::lsp_types::{DocumentChangeOperation, DocumentChanges, ResourceOp};
|
||||
let uri = Url::parse("file:///wiki/Doomed.wiki").unwrap();
|
||||
let mut b = nuwiki_lsp::edits::WorkspaceEditBuilder::new();
|
||||
b.file_op(nuwiki_lsp::edits::op_delete(uri.clone()));
|
||||
let we = b.build();
|
||||
|
||||
let DocumentChanges::Operations(ops) = we.document_changes.unwrap() else {
|
||||
panic!("expected resource operations");
|
||||
};
|
||||
assert!(matches!(
|
||||
&ops[0],
|
||||
DocumentChangeOperation::Op(ResourceOp::Delete(d)) if d.uri == uri
|
||||
));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 2: Diary
|
||||
// =====================================================================
|
||||
|
||||
// :NuwikiMakeDiaryNote — open today's entry.
|
||||
#[test]
|
||||
fn nuwiki_make_diary_note_targets_todays_file() {
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
let today = DiaryDate::from_ymd(2026, 5, 12).unwrap();
|
||||
let uri = diary::uri_for_date(&cfg, &today).unwrap();
|
||||
assert!(uri.as_str().ends_with("/diary/2026-05-12.wiki"));
|
||||
}
|
||||
|
||||
// :NuwikiMakeYesterdayDiaryNote / :NuwikiMakeTomorrowDiaryNote — step the
|
||||
// diary back / forward by one period at the daily cadence.
|
||||
#[test]
|
||||
fn nuwiki_yesterday_and_tomorrow_target_adjacent_files() {
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
let yesterday = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||||
let tomorrow = DiaryDate::from_ymd(2026, 5, 13).unwrap();
|
||||
assert!(diary::uri_for_date(&cfg, &yesterday)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.ends_with("/diary/2026-05-11.wiki"));
|
||||
assert!(diary::uri_for_date(&cfg, &tomorrow)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.ends_with("/diary/2026-05-13.wiki"));
|
||||
}
|
||||
|
||||
// :NuwikiDiaryIndex — open the diary index page.
|
||||
#[test]
|
||||
fn nuwiki_diary_index_resolves_index_uri() {
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
let uri = diary::index_uri(&cfg).unwrap();
|
||||
assert!(uri.as_str().ends_with("/diary/diary.wiki"));
|
||||
}
|
||||
|
||||
// :NuwikiDiaryGenerateLinks — rebuild the diary index from disk entries.
|
||||
#[test]
|
||||
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
||||
let entries = diary::list_entries(&idx);
|
||||
let body = diary::build_index_body(&entries, "diary", "Diary");
|
||||
assert!(body.starts_with("= Diary ="));
|
||||
assert!(body.contains("2026-05-12"));
|
||||
assert!(body.contains("2026-05-10"));
|
||||
assert!(body.contains("2026-04-30"));
|
||||
// Newest entry appears before the older one within the same month.
|
||||
let p12 = body.find("2026-05-12").unwrap();
|
||||
let p10 = body.find("2026-05-10").unwrap();
|
||||
assert!(p12 < p10, "expected newest-first ordering");
|
||||
}
|
||||
|
||||
// :NuwikiDiaryNextDay / :NuwikiDiaryPrevDay — walk indexed entries.
|
||||
#[test]
|
||||
fn nuwiki_diary_next_and_prev_day_walk_entries() {
|
||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-05-15"]);
|
||||
let from = DiaryDate::from_ymd(2026, 5, 12).unwrap();
|
||||
assert_eq!(
|
||||
diary::next_entry(&idx, &from).unwrap().date.format(),
|
||||
"2026-05-15"
|
||||
);
|
||||
assert_eq!(
|
||||
diary::prev_entry(&idx, &from).unwrap().date.format(),
|
||||
"2026-05-10"
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 3: Page generation
|
||||
// =====================================================================
|
||||
|
||||
// :NuwikiTOC — generate / refresh the table of contents.
|
||||
#[test]
|
||||
fn nuwiki_toc_generates_nested_contents() {
|
||||
let src = "= Top =\n== Sub ==\ntext\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &doc, &uri, true).expect("toc edit produced");
|
||||
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||
|
||||
let toc = ops::build_toc_text(
|
||||
&[
|
||||
(1, "Top".into(), "top".into()),
|
||||
(2, "Sub".into(), "sub".into()),
|
||||
],
|
||||
"Contents",
|
||||
);
|
||||
assert!(toc.contains("= Contents ="));
|
||||
assert!(toc.contains("- [[#top|Top]]"));
|
||||
assert!(toc.contains(" - [[#sub|Sub]]"));
|
||||
}
|
||||
|
||||
// :NuwikiGenerateLinks — insert a flat list of every page in the wiki.
|
||||
#[test]
|
||||
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
||||
let text = ops::build_links_text(&pages, "Links", Some("Home"));
|
||||
assert!(text.contains("= Links ="));
|
||||
assert!(text.contains("- [[About]]"));
|
||||
assert!(text.contains("- [[Notes]]"));
|
||||
assert!(!text.contains("[[Home]]"), "current page excluded");
|
||||
|
||||
let src = "= Existing =\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
assert!(ops::links_edit(src, &doc, &uri, "Home", &pages, true).is_some());
|
||||
}
|
||||
|
||||
// :NuwikiCheckLinks — surface broken links across the workspace.
|
||||
#[test]
|
||||
fn nuwiki_check_links_distinguishes_broken_from_resolvable() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[("Home", "[[About]] [[Ghost]]\n"), ("About", "= A =\n")],
|
||||
);
|
||||
let good = LinkTarget {
|
||||
kind: LinkKind::Wiki,
|
||||
path: Some("About".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let broken = LinkTarget {
|
||||
kind: LinkKind::Wiki,
|
||||
path: Some("Ghost".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(idx.resolve(&good, "Home").is_some());
|
||||
assert!(
|
||||
idx.resolve(&broken, "Home").is_none(),
|
||||
"broken link unresolved"
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 4: Tags
|
||||
// =====================================================================
|
||||
|
||||
// :NuwikiSearchTags {tag} — every `:tag:` occurrence.
|
||||
#[test]
|
||||
fn nuwiki_search_tags_finds_occurrences() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[("Home", ":alpha:beta:\n"), ("Work", ":alpha:\n")],
|
||||
);
|
||||
let hits = ops::tag_hits(&idx, "alpha");
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert!(hits.iter().all(|h| h.name == "alpha"));
|
||||
assert!(ops::tag_hits(&idx, "nonexistent").is_empty());
|
||||
}
|
||||
|
||||
// :NuwikiGenerateTagLinks [tag] — section linking every page with a tag.
|
||||
#[test]
|
||||
fn nuwiki_generate_tag_links_builds_section() {
|
||||
let idx = build_index(
|
||||
"/wiki",
|
||||
&[
|
||||
("Home", ":alpha:\n"),
|
||||
("Work", ":alpha:\n"),
|
||||
("Misc", ":beta:\n"),
|
||||
],
|
||||
);
|
||||
let by_tag = ops::tag_pages_snapshot(&idx);
|
||||
|
||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha")).unwrap();
|
||||
assert!(single.starts_with("= Tag: alpha ="));
|
||||
assert!(single.contains("- [[Home]]"));
|
||||
assert!(single.contains("- [[Work]]"));
|
||||
|
||||
// No-arg variant emits a section per tag.
|
||||
let all = ops::build_tag_links_text(&by_tag, None).unwrap();
|
||||
assert!(all.starts_with("= Tags ="));
|
||||
assert!(all.contains("== alpha =="));
|
||||
assert!(all.contains("== beta =="));
|
||||
|
||||
let src = "= Home =\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
assert!(ops::tag_links_edit(src, &doc, &uri, Some("alpha"), &by_tag, true).is_some());
|
||||
}
|
||||
|
||||
// :NuwikiRebuildTags — force a full workspace re-index. A rebuild must
|
||||
// reflect the current on-disk tags, dropping stale ones.
|
||||
#[test]
|
||||
fn nuwiki_rebuild_tags_reflects_current_state() {
|
||||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/wiki")));
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
idx.upsert(uri.clone(), &parse(":old:\n"));
|
||||
assert_eq!(idx.tags_for("old").len(), 1);
|
||||
|
||||
// Re-index after the page changed on disk.
|
||||
idx.upsert(uri, &parse(":new:\n"));
|
||||
assert!(idx.tags_for("old").is_empty(), "stale tag dropped");
|
||||
assert_eq!(idx.tags_for("new").len(), 1);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 5: HTML export
|
||||
// =====================================================================
|
||||
|
||||
// :Nuwiki2HTML — render the current page to HTML.
|
||||
#[test]
|
||||
fn nuwiki_2html_renders_page() {
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
let doc = parse("%title My Page\n= One =\nbody text\n");
|
||||
let html = export::render_page_html(
|
||||
&doc,
|
||||
Some("<title>{{title}}</title><main>{{content}}</main>".into()),
|
||||
"Home",
|
||||
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||
&cfg.html,
|
||||
Some(".wiki"),
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(html.contains("<title>My Page</title>"));
|
||||
assert!(html.contains("body text"));
|
||||
}
|
||||
|
||||
// :Nuwiki2HTMLBrowse — render then open in the browser. The render output
|
||||
// is identical to :Nuwiki2HTML; the browse step is an editor-side open.
|
||||
#[test]
|
||||
fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
||||
assert!(COMMANDS.contains(&"nuwiki.export.browse"));
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
let doc = parse("= Page =\nhi\n");
|
||||
let html = export::render_page_html(
|
||||
&doc,
|
||||
Some("{{content}}".into()),
|
||||
"Page",
|
||||
DiaryDate::today_utc(),
|
||||
&cfg.html,
|
||||
Some(".wiki"),
|
||||
HashMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(html.contains("hi"));
|
||||
}
|
||||
|
||||
// :NuwikiAll2HTML[!] — export every page; honour the exclude list and
|
||||
// per-page output paths.
|
||||
#[test]
|
||||
fn nuwiki_all2html_maps_output_paths_and_excludes() {
|
||||
let cfg = wiki_cfg("/wiki");
|
||||
assert!(export::output_path_for(&cfg.html, "Home").ends_with("Home.html"));
|
||||
assert!(
|
||||
export::output_path_for(&cfg.html, "diary/2026-05-12").ends_with("diary/2026-05-12.html")
|
||||
);
|
||||
|
||||
let patterns = vec!["_drafts/**".to_string()];
|
||||
assert!(export::is_excluded(&patterns, "_drafts/secret"));
|
||||
assert!(!export::is_excluded(&patterns, "Home"));
|
||||
|
||||
// Both incremental and forced (`!`) variants are advertised.
|
||||
assert!(COMMANDS.contains(&"nuwiki.export.allToHtml"));
|
||||
assert!(COMMANDS.contains(&"nuwiki.export.allToHtmlForce"));
|
||||
}
|
||||
|
||||
// :NuwikiRss — write `rss.xml` summarising recent diary entries.
|
||||
#[test]
|
||||
fn nuwiki_rss_writes_feed_file() {
|
||||
let root = std::env::temp_dir().join(format!("nuwiki-cov-rss-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let cfg = WikiConfig::from_root(root.clone());
|
||||
|
||||
let entries = vec![
|
||||
diary::DiaryEntry {
|
||||
date: DiaryDate::from_ymd(2026, 5, 10).unwrap(),
|
||||
uri: Url::from_file_path(root.join("diary/2026-05-10.wiki")).unwrap(),
|
||||
},
|
||||
diary::DiaryEntry {
|
||||
date: DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||
uri: Url::from_file_path(root.join("diary/2026-05-12.wiki")).unwrap(),
|
||||
},
|
||||
];
|
||||
|
||||
let path = export_ops::write_rss(&cfg, &entries).unwrap();
|
||||
assert!(path.ends_with("rss.xml"));
|
||||
let xml = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(xml.contains("<rss version=\"2.0\">"));
|
||||
// Newest entry listed first.
|
||||
let p12 = xml.find("2026-05-12").unwrap();
|
||||
let p10 = xml.find("2026-05-10").unwrap();
|
||||
assert!(p12 < p10);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Group 6: Other
|
||||
// =====================================================================
|
||||
|
||||
// :NuwikiInstall — install/re-install the `nuwiki-ls` binary. This is an
|
||||
// editor-side action (binary download / cargo build); it is deliberately
|
||||
// NOT an LSP executeCommand, so the server must not advertise it.
|
||||
#[test]
|
||||
fn nuwiki_install_is_editor_only_not_an_lsp_command() {
|
||||
assert!(!COMMANDS.iter().any(|c| c.contains("install")));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Command-surface cross-check: every documented command that maps to an
|
||||
// executeCommand handler is actually advertised by the server.
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn every_documented_lsp_command_is_advertised() {
|
||||
let expected = [
|
||||
"nuwiki.wiki.openIndex", // :NuwikiIndex
|
||||
"nuwiki.wiki.tabOpenIndex", // :NuwikiTabIndex
|
||||
"nuwiki.wiki.listAll", // :NuwikiUISelect
|
||||
"nuwiki.wiki.gotoPage", // :NuwikiGoto
|
||||
"nuwiki.file.delete", // :NuwikiDeleteFile
|
||||
"nuwiki.diary.openToday", // :NuwikiMakeDiaryNote
|
||||
"nuwiki.diary.openYesterday", // :NuwikiMakeYesterdayDiaryNote
|
||||
"nuwiki.diary.openTomorrow", // :NuwikiMakeTomorrowDiaryNote
|
||||
"nuwiki.diary.openIndex", // :NuwikiDiaryIndex
|
||||
"nuwiki.diary.generateIndex", // :NuwikiDiaryGenerateLinks
|
||||
"nuwiki.diary.next", // :NuwikiDiaryNextDay
|
||||
"nuwiki.diary.prev", // :NuwikiDiaryPrevDay
|
||||
"nuwiki.toc.generate", // :NuwikiTOC
|
||||
"nuwiki.links.generate", // :NuwikiGenerateLinks
|
||||
"nuwiki.workspace.checkLinks", // :NuwikiCheckLinks
|
||||
"nuwiki.tags.search", // :NuwikiSearchTags
|
||||
"nuwiki.tags.generateLinks", // :NuwikiGenerateTagLinks
|
||||
"nuwiki.tags.rebuild", // :NuwikiRebuildTags
|
||||
"nuwiki.export.currentToHtml", // :Nuwiki2HTML
|
||||
"nuwiki.export.browse", // :Nuwiki2HTMLBrowse
|
||||
"nuwiki.export.allToHtml", // :NuwikiAll2HTML
|
||||
"nuwiki.export.rss", // :NuwikiRss
|
||||
];
|
||||
for cmd in expected {
|
||||
assert!(COMMANDS.contains(&cmd), "server must advertise {cmd}");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Phase 13: pure-function tests for the rename helper + the file-delete
|
||||
//! Pure-function tests for the rename helper + the file-delete
|
||||
//! command. The async `Backend::rename` end-to-end (with `read_or_open`
|
||||
//! hitting disk) is exercised by Phase 14+ integration but covered here
|
||||
//! hitting disk) is exercised by integration tests but covered here
|
||||
//! at the building-block level — same logic, no async client.
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -95,7 +95,7 @@ async fn file_delete_returns_workspace_edit_with_delete_op() {
|
||||
//
|
||||
// Easier: shape the test as an integration test over the public
|
||||
// module surface — call `commands::execute` once we expose a thin
|
||||
// builder. For Phase 13 we test the JSON-shaped contract using the
|
||||
// builder. We test the JSON-shaped contract using the
|
||||
// edits module directly, since that's what `file_delete` returns.
|
||||
let uri = Url::parse("file:///tmp/note.wiki").unwrap();
|
||||
let mut b = nuwiki_lsp::edits::WorkspaceEditBuilder::new();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! §13.1 Cluster A — list rewriters: `removeDone`, `renumber`,
|
||||
//! Cluster A — list rewriters: `removeDone`, `renumber`,
|
||||
//! `changeSymbol`, `changeLevel`.
|
||||
|
||||
use nuwiki_core::ast::ListSymbol;
|
||||
@@ -86,10 +86,10 @@ fn remove_done_returns_none_when_no_lists() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_scoped_by_position_to_one_item() {
|
||||
// With `position` set, only items whose line range contains the
|
||||
// cursor line are considered. So a done item on line 1 is removed
|
||||
// by cursor on line 1, but a done item on line 3 is left alone.
|
||||
fn remove_done_scoped_by_position_to_current_list() {
|
||||
// With `position` set, the scope is the contiguous list block the
|
||||
// cursor sits in — every done item in that list is removed, not just
|
||||
// the one under the cursor.
|
||||
let src = "- [X] done one\n- [ ] todo\n- [X] done two\n";
|
||||
let ast = parse(src);
|
||||
let pos = Some(LspPosition {
|
||||
@@ -98,7 +98,37 @@ fn remove_done_scoped_by_position_to_one_item() {
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "only the line-0 item should match");
|
||||
assert_eq!(edits.len(), 2, "both done items in the current list match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_position_leaves_other_lists_untouched() {
|
||||
// Two separate list blocks split by a paragraph. Cursor in the first
|
||||
// list removes only that list's done items; the second list's done
|
||||
// item survives.
|
||||
let src = "- [X] done a\n- [ ] todo a\n\nparagraph\n\n- [ ] todo b\n- [X] done b\n";
|
||||
let ast = parse(src);
|
||||
let pos = Some(LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "only the first list's done item is removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_done_position_cascades_into_sublists() {
|
||||
// The current-list scope includes nested sublists of the block.
|
||||
let src = "- [ ] parent\n - [X] done child\n - [ ] open child\n";
|
||||
let ast = parse(src);
|
||||
let pos = Some(LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
});
|
||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
||||
let edits = &edit.changes.unwrap()[&uri()];
|
||||
assert_eq!(edits.len(), 1, "the nested done child is removed");
|
||||
}
|
||||
|
||||
// ===== renumber =====
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! §13.1 Cluster B — table rewriters: `insert`, `align`, `moveColumn`.
|
||||
//! Cluster B — table rewriters: `insert`, `align`, `moveColumn`.
|
||||
|
||||
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||||
use nuwiki_core::syntax::SyntaxPlugin;
|
||||
|
||||
@@ -337,7 +337,7 @@ fn diary_entry_serializes_to_date_string_and_uri() {
|
||||
// ===== COMMANDS list completeness =====
|
||||
|
||||
#[test]
|
||||
fn commands_list_includes_phase16_entries() {
|
||||
fn commands_list_includes_diary_entries() {
|
||||
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
||||
for name in [
|
||||
"nuwiki.diary.openToday",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 19: LSP folding-range provider.
|
||||
//! LSP folding-range provider.
|
||||
//!
|
||||
//! Drives `folding::folding_ranges` directly. The handler wiring is
|
||||
//! shallow — it just calls into this function — so we verify the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for the pure helpers that the LSP backend uses to translate
|
||||
//! between `nuwiki-core` ASTs and the LSP wire format. The async
|
||||
//! request-handling loop is exercised at integration time in Phase 8+
|
||||
//! (navigation), so this file stays focused on conversion correctness.
|
||||
//! request-handling loop is exercised at integration time by the
|
||||
//! navigation tests, so this file stays focused on conversion correctness.
|
||||
|
||||
use nuwiki_core::ast::{
|
||||
inline::InlineNode, BlockNode, BlockquoteNode, DocumentNode, ErrorNode, ParagraphNode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 17: HTML export commands.
|
||||
//! HTML export commands.
|
||||
//!
|
||||
//! Drives the pure `export::*` helpers directly. The side-effecting
|
||||
//! `export_ops::write_page` / `write_rss` paths get exercised via a
|
||||
@@ -474,7 +474,7 @@ fn write_rss_emits_valid_xml_with_entries() {
|
||||
// ===== COMMANDS list completeness =====
|
||||
|
||||
#[test]
|
||||
fn commands_list_includes_phase17_entries() {
|
||||
fn commands_list_includes_export_entries() {
|
||||
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
||||
for name in [
|
||||
"nuwiki.export.currentToHtml",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 15: link-health diagnostics + TOC/links/orphans queries.
|
||||
//! Link-health diagnostics + TOC/links/orphans queries.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -630,7 +630,7 @@ fn anchor_matches_unicode_heading() {
|
||||
// ===== COMMANDS completeness =====
|
||||
|
||||
#[test]
|
||||
fn commands_list_includes_phase15_entries() {
|
||||
fn commands_list_includes_link_health_entries() {
|
||||
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
||||
for name in [
|
||||
"nuwiki.toc.generate",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Phase 18: multi-wiki — interwiki resolution + picker commands.
|
||||
//! Multi-wiki — interwiki resolution + picker commands.
|
||||
//!
|
||||
//! The cross-wiki resolver methods on `Backend` are exercised indirectly
|
||||
//! through `tower_lsp::LspService`'s test harness via a synthetic
|
||||
//! `Backend` (the same pattern Phase 13 used). To keep this test suite
|
||||
//! `Backend`. To keep this test suite
|
||||
//! lightweight we drive the *pure* resolution primitives — the parser
|
||||
//! producing `LinkKind::Interwiki` targets, then the `WorkspaceIndex`
|
||||
//! lookup-by-name — and verify the multi-wiki config shape end-to-end.
|
||||
@@ -80,7 +80,7 @@ fn multi_wiki_config_loads_two_entries() {
|
||||
|
||||
#[test]
|
||||
fn v1_0_single_root_still_works_alongside_multi_wiki_shape() {
|
||||
// Single-wiki shorthand is the v1.0 form; ensure it still desugars.
|
||||
// Single-wiki shorthand is the legacy form; ensure it still desugars.
|
||||
let cfg = config_from_json(serde_json::json!({
|
||||
"wiki_root": "/tmp/legacy",
|
||||
}));
|
||||
@@ -135,7 +135,7 @@ fn nested_root_picks_longest_prefix_match() {
|
||||
|
||||
// ===== Cross-wiki resolution (uses WorkspaceIndex per wiki) =====
|
||||
|
||||
/// Verify that the cross-wiki resolution semantics match the SPEC:
|
||||
/// Verify the cross-wiki resolution semantics:
|
||||
/// `[[wiki1:Page]]` looks up `Page` in the *second* wiki's index, not
|
||||
/// the source wiki's. The pure-data check here mirrors what
|
||||
/// `Backend::resolve_target_uri` does.
|
||||
@@ -185,7 +185,7 @@ fn interwiki_resolution_falls_back_when_wiki_missing() {
|
||||
// ===== Wiki commands: COMMANDS list completeness =====
|
||||
|
||||
#[test]
|
||||
fn commands_list_includes_phase18_entries() {
|
||||
fn commands_list_includes_wiki_entries() {
|
||||
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
||||
for name in [
|
||||
"nuwiki.wiki.listAll",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for the Phase 8 navigation pieces: WorkspaceIndex queries,
|
||||
//! Tests for the navigation pieces: WorkspaceIndex queries,
|
||||
//! page-name derivation, anchor slugify, position lookup, and link
|
||||
//! resolution. The async LSP handlers are exercised through these
|
||||
//! helpers; end-to-end JSON-RPC drive lives in a follow-up phase.
|
||||
//! helpers; end-to-end JSON-RPC drive lives elsewhere.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# nuwiki Developer Onboarding
|
||||
|
||||
## Overview
|
||||
|
||||
nuwiki is a vimwiki-compatible Vim/Neovim plugin backed by a Rust language server. It provides full vimwiki syntax support while keeping the original file format, keymaps, and command surface intact. The substantive work happens in a Rust LSP daemon — Vim and Neovim are thin client layers that wire up keystrokes and display results.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
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/ # Syntax plugins (vimwiki, future markdown)
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── registry.rs
|
||||
│ │ │ └── vimwiki/
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── lexer.rs
|
||||
│ │ │ └── parser.rs
|
||||
│ │ ├── ast/ # Abstract Syntax Tree node definitions
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── block.rs
|
||||
│ │ │ ├── inline.rs
|
||||
│ │ │ └── link.rs
|
||||
│ │ └── render/ # Renderers (HTML, etc.)
|
||||
│ │ ├── 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/ # Plugin-runtime scripts only
|
||||
│ └── download_bin.vim # VimL binary download (used by Dein/vim-plug build hooks)
|
||||
│
|
||||
├── development/ # Developer-only tooling and docs (not shipped)
|
||||
│ ├── ONBOARDING.md # This file
|
||||
│ ├── SPEC.md # Technical reference (architecture, LSP, commands)
|
||||
│ ├── nuwiki-architecture.html
|
||||
│ ├── start-nvim.sh # Launch Neovim against a generated sample wiki
|
||||
│ ├── start-vim.sh # Launch Vim against a generated sample wiki
|
||||
│ ├── syntax-diag.vim # Dump highlighting state for debugging
|
||||
│ └── tests/ # Editor keymap/command harnesses (run in CI)
|
||||
│ ├── test-keymaps.sh # Neovim keymap harness
|
||||
│ ├── test-keymaps.lua
|
||||
│ ├── test-keymaps-vim.sh # Vim keymap harness
|
||||
│ └── test-keymaps-vim.vim
|
||||
│
|
||||
└── .gitea/
|
||||
└── workflows/
|
||||
├── ci.yaml # Lint, test, fmt check on every push/PR
|
||||
└── release.yaml # Cross-compile + release on v* tag
|
||||
```
|
||||
|
||||
## Key Crates
|
||||
|
||||
1. **nuwiki-core**: Contains the parser, lexer, AST, and renderer. This is the pure-Rust core with no editor dependencies.
|
||||
2. **nuwiki-lsp**: Bridges the core to the LSP protocol using tower-lsp. Handles LSP requests/responses and manages the document store.
|
||||
3. **nuwiki-ls**: Binary crate that starts the stdio LSP server. Very thin — just initializes the LSP server and runs it.
|
||||
|
||||
## Build & Installation
|
||||
|
||||
### Prerequisites
|
||||
- Rust toolchain (1.83+ stable)
|
||||
- Cargo
|
||||
- For editor integration: Vim 9+ or Neovim 0.5+ with an LSP client (vim-lsp recommended for Vim, built-in for Neovim 0.11+)
|
||||
|
||||
### Development Build
|
||||
```bash
|
||||
# From the repository root
|
||||
cargo build --release -p nuwiki-ls # Builds the LSP binary
|
||||
```
|
||||
|
||||
The binary will be placed at `target/release/nuwiki-ls`.
|
||||
|
||||
### Installation via Plugin Managers
|
||||
|
||||
#### lazy.nvim
|
||||
```lua
|
||||
{
|
||||
'gffranco/nuwiki',
|
||||
build = ":lua require('nuwiki').install()",
|
||||
ft = { 'vimwiki' },
|
||||
opts = {
|
||||
wiki_root = '~/vimwiki',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### vim-plug
|
||||
```vim
|
||||
Plug 'gffranco/nuwiki', { 'do': 'vim -e -s -c \"source scripts/download_bin.vim\" -c \"q\"' }
|
||||
```
|
||||
|
||||
#### Dein
|
||||
```vim
|
||||
call dein#add('gffranco/nuwiki', {
|
||||
\ 'build': 'vim -e -s -c \"source scripts/download_bin.vim\" -c \"q\"'
|
||||
\ })
|
||||
```
|
||||
|
||||
### Manual Installation (Plain Vim)
|
||||
```bash
|
||||
git clone https://code.gfran.co/gffranco/nuwiki ~/.vim/pack/gffranco/start/nuwiki
|
||||
cd ~/.vim/pack/gffranco/start/nuwiki
|
||||
cargo build --release -p nuwiki-ls
|
||||
mkdir -p bin && ln -s ../target/release/nuwiki-ls bin/nuwiki-ls
|
||||
```
|
||||
|
||||
Plain Vim users also need an LSP client — vim-lsp is recommended.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests (workspace)
|
||||
cargo test --workspace
|
||||
|
||||
# Run tests for a specific crate
|
||||
cargo test -p nuwiki-core
|
||||
cargo test -p nuwiki-lsp
|
||||
cargo test -p nuwiki-ls
|
||||
```
|
||||
|
||||
### Linting & Formatting
|
||||
```bash
|
||||
# Check formatting
|
||||
cargo fmt -- --check
|
||||
|
||||
# Run Clippy
|
||||
cargo clippy --workspace -- -D warnings
|
||||
```
|
||||
|
||||
### Running the LSP Server Manually (for debugging)
|
||||
```bash
|
||||
# Build and run the binary
|
||||
cargo run -p nuwiki-ls -- --help
|
||||
```
|
||||
|
||||
The LSP server communicates over stdio. You can test it with an LSP client like `lspci` or by connecting from Neovim.
|
||||
|
||||
### Health Check (Neovim)
|
||||
After installing the plugin, run:
|
||||
```
|
||||
:checkhealth nuwiki
|
||||
```
|
||||
This 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
|
||||
|
||||
## Editor Layers
|
||||
|
||||
### VimL Layer (`plugin/nuwiki.vim`, `autoload/`)
|
||||
- Universal entry point for all plugin managers
|
||||
- Detects whether running in Neovim or Vim
|
||||
- For Neovim: delegates to Lua layer
|
||||
- For Vim: starts the LSP server via `nuwiki#lsp#start()`
|
||||
|
||||
### Neovim Lua Layer (`lua/nuwiki/`)
|
||||
- `init.lua`: Main setup function
|
||||
- `config.lua`: Configuration schema and defaults
|
||||
- `lsp.lua`: Starts the LSP client using `vim.lsp.start()`
|
||||
- `install.lua`: Handles binary download/build and places it in the plugin's `bin/` directory
|
||||
|
||||
### Vim Compatibility Layer (`autoload/nuwiki/lsp.vim`)
|
||||
- Provides the `nuwiki#lsp#start()` function for Vim
|
||||
- Handles binary location and LSP client startup via vim-lsp or coc.nvim
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The core library (`nuwiki-core`) is completely editor-agnostic and can be tested independently.
|
||||
- The LSP layer (`nuwiki-lsp`) depends only on the core and tower-lsp.
|
||||
- The binary crate (`nuwiki-ls`) is intentionally thin — it just initializes and runs the LSP server.
|
||||
- Editor layers (VimL/Lua) contain zero logic — they are pure wiring only.
|
||||
- All syntax-specific code lives in `nuwiki-core/src/syntax/<syntax>/` making it easy to add new syntaxes (e.g., markdown) in the future.
|
||||
- The project uses a workspace Cargo.toml with resolver v2 (edition 2021).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://code.gfran.co/gffranco/nuwiki
|
||||
cd nuwiki
|
||||
```
|
||||
|
||||
2. Build the LSP binary:
|
||||
```bash
|
||||
cargo build --release -p nuwiki-ls
|
||||
```
|
||||
|
||||
3. Set up a test wiki directory:
|
||||
```bash
|
||||
mkdir -p ~/test-wiki
|
||||
echo "# Test Wiki" > ~/test-wiki/index.wiki
|
||||
```
|
||||
|
||||
4. Install the plugin in your Neovim/Vim configuration using your preferred plugin manager (see above examples).
|
||||
|
||||
5. Open a .wiki file and verify:
|
||||
- Syntax highlighting works
|
||||
- `:VimwikiIndex` opens the index page
|
||||
- LSP features (go-to-definition, hover, completions) work via `:checkhealth nuwiki`
|
||||
|
||||
## CI/CD
|
||||
|
||||
The project uses Gitea Actions:
|
||||
- CI (`.gitea/workflows/ci.yaml`): Runs on every push/PR — checks formatting, Clippy, and runs tests.
|
||||
- Release (`.gitea/workflows/release.yaml`): Triggers on `v*` tag — cross-compiles for Linux targets and creates a Gitea release.
|
||||
|
||||
## License
|
||||
|
||||
Dual-licensed under MIT or Apache-2.0 at your option.
|
||||
@@ -0,0 +1,339 @@
|
||||
# nuwiki — Technical Reference
|
||||
|
||||
A developer-facing description of how nuwiki is built and what it implements.
|
||||
For day-one setup see [ONBOARDING.md](ONBOARDING.md); for user-facing help see
|
||||
`doc/nuwiki.txt` (`:h nuwiki`).
|
||||
|
||||
nuwiki is a vimwiki-compatible Vim/Neovim plugin backed by a Rust language
|
||||
server. The editor layers are thin clients; all parsing, navigation, and
|
||||
editing logic lives in the Rust crates and is reached over LSP.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| License | Dual MIT / Apache-2.0 |
|
||||
| MSRV | Rust 1.83 |
|
||||
| Edition / resolver | 2021 / v2 |
|
||||
| Repo | `https://code.gfran.co/gffranco/nuwiki` |
|
||||
| VCS / CI | Gitea + Gitea Actions |
|
||||
| LSP library | `tower-lsp` (tokio, stdio transport) |
|
||||
| Min editors | Neovim 0.11+, Vim 9.1+ |
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
### Layer model
|
||||
|
||||
```
|
||||
Editor client (VimL / Lua) — pure wiring, no logic
|
||||
│ LSP over stdio
|
||||
▼
|
||||
nuwiki-lsp — protocol bridge, executeCommand, document store, config
|
||||
│
|
||||
▼
|
||||
nuwiki-core — lexer → parser → AST → renderer (no editor deps)
|
||||
```
|
||||
|
||||
### Crate dependency rules
|
||||
|
||||
```
|
||||
nuwiki-ls → nuwiki-lsp → nuwiki-core
|
||||
```
|
||||
|
||||
- `nuwiki-core` never depends on `nuwiki-lsp` / `nuwiki-ls`.
|
||||
- `nuwiki-lsp` never depends on `nuwiki-ls`.
|
||||
- The VimL and Lua layers contain no logic — every command body issues a
|
||||
`workspace/executeCommand` or a built-in LSP request.
|
||||
|
||||
### Repository layout
|
||||
|
||||
```
|
||||
crates/
|
||||
nuwiki-core/ # parser, AST, renderer — editor-independent
|
||||
src/
|
||||
ast/ # block.rs inline.rs link.rs span.rs visit.rs
|
||||
syntax/ # registry.rs + vimwiki/{lexer,parser}.rs
|
||||
render/ # html.rs
|
||||
date.rs # diary period math (no chrono dependency)
|
||||
nuwiki-lsp/ # LSP backend
|
||||
src/
|
||||
lib.rs # Backend, capabilities, textDocument handlers
|
||||
commands.rs # executeCommand dispatcher + pure edit ops
|
||||
config.rs # Config / WikiConfig / HtmlConfig
|
||||
diagnostics.rs index.rs nav.rs rename.rs semantic_tokens.rs
|
||||
diary.rs export.rs edits.rs folding.rs wiki.rs
|
||||
nuwiki-ls/ # thin binary: starts the stdio server
|
||||
|
||||
plugin/nuwiki.vim # universal entry point (detects Vim vs Neovim)
|
||||
lua/nuwiki/ # Neovim layer: init, config, lsp, keymaps, commands,
|
||||
# folding, install
|
||||
autoload/nuwiki/ # Vim layer: lsp.vim, commands.vim
|
||||
ftdetect/ ftplugin/ syntax/ # filetype detection, buffer setup, fallback HL
|
||||
doc/nuwiki.txt # :help
|
||||
scripts/download_bin.vim # plugin-runtime binary download (build hooks)
|
||||
development/ # dev-only tooling + this document (not shipped)
|
||||
.gitea/workflows/ # ci.yaml, release.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. nuwiki-core
|
||||
|
||||
### AST
|
||||
|
||||
Every node carries a `Span { start, end }` of `Position { line, column, offset }`
|
||||
(0-indexed; column and offset are byte-based). Spans drive diagnostics,
|
||||
semantic tokens, and go-to-definition.
|
||||
|
||||
**Block nodes:** `Heading` (level 1–6, `centered`), `Paragraph`,
|
||||
`HorizontalRule`, `Blockquote` (recursive), `Preformatted` (optional
|
||||
language), `MathBlock` (optional environment), `List`, `DefinitionList`,
|
||||
`Table`, `Comment`, `Tag`, `Error` (resilient parse failure).
|
||||
|
||||
**Inline nodes:** `Text`, `Bold`, `Italic`, `BoldItalic`, `Strikethrough`,
|
||||
`Code`, `Superscript`, `Subscript`, `MathInline`, `Keyword`, `Color`,
|
||||
`WikiLink`, `ExternalLink`, `Transclusion`, `RawUrl`.
|
||||
|
||||
**Enums:**
|
||||
- `ListSymbol` = `Dash | Star | Hash | Numeric | NumericParen | AlphaParen |
|
||||
AlphaUpperParen | RomanParen | RomanUpperParen`
|
||||
- `CheckboxState` = `Empty | Quarter | Half | ThreeQuarters | Done | Rejected`
|
||||
- `Keyword` = `Todo | Done | Started | Fixme | Fixed | Xxx | Stopped`
|
||||
- `LinkKind` = `Wiki | Interwiki | Diary | File | Local | Raw | AnchorOnly`
|
||||
- `TagScope` = `File | Heading(idx) | Standalone`
|
||||
|
||||
`PageMetadata` carries `title`, `nohtml`, `template`, `date`, and aggregated
|
||||
file-level `tags`.
|
||||
|
||||
### Lexer / parser
|
||||
|
||||
- Two-pass lexer (block pass, then inline pass per block); syntax-specific
|
||||
`VimwikiToken`s with byte-offset spans, collected eagerly into a
|
||||
`TokenStream`.
|
||||
- Hand-rolled recursive-descent parser (not a combinator library). Resilient:
|
||||
malformed input yields an `ErrorNode` and parsing continues — a document
|
||||
never fails to parse.
|
||||
- Full re-parse on every `didOpen` / `didChange` (no incremental parsing).
|
||||
|
||||
### Syntax plugin interface
|
||||
|
||||
Syntaxes register against a `SyntaxRegistry` keyed by id and file extension:
|
||||
|
||||
```
|
||||
SyntaxPlugin { id, display_name, file_extensions, lexer, parser }
|
||||
```
|
||||
|
||||
vimwiki (`.wiki`) is the only registered syntax; the registry exists so a
|
||||
markdown plugin can be added without touching the LSP or editor layers.
|
||||
|
||||
### Renderer
|
||||
|
||||
`Renderer` trait writes to a `dyn Write`; `HtmlRenderer` is the implementation.
|
||||
Link resolution is injected as a callback. Template substitution supports
|
||||
`{{title}}`, `{{content}}`, `{{date}}`, `{{root_path}}`, `{{toc}}`, `{{css}}`.
|
||||
`color_dic` maps colour-tag names to CSS values, falling back to
|
||||
`class="color-<name>"`.
|
||||
|
||||
---
|
||||
|
||||
## 3. LSP server
|
||||
|
||||
### Advertised capabilities
|
||||
|
||||
| Capability | Notes |
|
||||
|---|---|
|
||||
| `textDocumentSync` | full sync (open/close/change/save) |
|
||||
| `semanticTokensProvider` | full + range; custom vimwiki token legend |
|
||||
| `documentSymbolProvider` / `workspaceSymbolProvider` | outline + workspace search (tags included) |
|
||||
| `definitionProvider` / `referencesProvider` | link follow + backlinks |
|
||||
| `hoverProvider` | link preview |
|
||||
| `completionProvider` | trigger `[` |
|
||||
| `renameProvider` | cross-document link rewrite |
|
||||
| `foldingRangeProvider` | heading blocks + top-level lists |
|
||||
| `executeCommandProvider` | see command surface below |
|
||||
| `workspace.fileOperations` | `did_rename` + `did_delete` for `**/*.wiki`, `**/*.md` |
|
||||
| `positionEncoding` | UTF-8 when client supports LSP 3.17+, else UTF-16 |
|
||||
|
||||
Workspace indexing runs as a background tokio task reporting via
|
||||
`window/workDoneProgress`; nav features answer with partial data until the
|
||||
scan completes. The document store is an `Arc<DashMap<Url, DocumentState>>`
|
||||
holding text + AST + version.
|
||||
|
||||
### executeCommand surface
|
||||
|
||||
All editing commands return a `WorkspaceEdit` (applied via
|
||||
`workspace/applyEdit`) built through `edits::WorkspaceEditBuilder`, so the AST
|
||||
stays the source of truth. Navigation commands return a `Location`.
|
||||
|
||||
| Namespace | Commands |
|
||||
|---|---|
|
||||
| `list` | `toggleCheckbox` `cycleCheckbox` `rejectCheckbox` `nextTask` `removeDone` `renumber` `changeSymbol` `changeLevel` |
|
||||
| `table` | `insert` `align` `moveColumn` |
|
||||
| `heading` | `addLevel` `removeLevel` |
|
||||
| `link` | `pasteWikilink` `pasteUrl` |
|
||||
| `toc` / `links` | `toc.generate` · `links.generate` |
|
||||
| `workspace` | `checkLinks` `findOrphans` |
|
||||
| `diary` | `openToday` `openYesterday` `openTomorrow` `openIndex` `generateIndex` `next` `prev` `listEntries` `openForDate` |
|
||||
| `tags` | `search` `generateLinks` `rebuild` |
|
||||
| `export` | `currentToHtml` `allToHtml` `allToHtmlForce` `browse` `rss` |
|
||||
| `wiki` | `listAll` `select` `openIndex` `tabOpenIndex` `gotoPage` |
|
||||
| `file` | `delete` |
|
||||
|
||||
`workspace/rename` emits a `RenameFile` op plus `[[A]]` → `[[B]]` rewrites
|
||||
across every linking page (anchors and descriptions preserved). Closed
|
||||
documents are re-parsed on demand so cross-document edits use current source.
|
||||
|
||||
### Diagnostics
|
||||
|
||||
Composable collector chains parse errors (`ErrorNode` walk) and broken-link
|
||||
checks (wiki target / anchor / `file:` / `local:` existence). Per-source
|
||||
severity is config-driven (`diagnostic.link_severity`: `off|hint|warn|error`);
|
||||
raw and external URLs are never diagnosed.
|
||||
|
||||
---
|
||||
|
||||
## 4. Editor integration
|
||||
|
||||
### Installation
|
||||
|
||||
Pre-built binaries are published as Gitea release assets
|
||||
(`nuwiki-ls-{version}-{target}.tar.gz`) and fetched at install time by
|
||||
`lua/nuwiki/install.lua` (Neovim) or `scripts/download_bin.vim` (Vim build
|
||||
hooks), installed to `{plugin_dir}/bin/nuwiki-ls`. Falls back to
|
||||
`cargo build --release` when download fails or
|
||||
`g:nuwiki_build_from_source = 1`.
|
||||
|
||||
`plugin/nuwiki.vim` dispatches: Neovim → `require('nuwiki').setup()`; Vim →
|
||||
`nuwiki#lsp#start()` (prefers `vim-lsp`, falls back to `coc.nvim`). On Neovim
|
||||
the server is registered via `vim.lsp.config{}` + `vim.lsp.enable`, with a
|
||||
`vim.lsp.start` autocmd fallback.
|
||||
|
||||
### Configuration
|
||||
|
||||
Single-wiki (v1.0) and multi-wiki forms both work; the scalar form desugars to
|
||||
a one-entry `wikis` list.
|
||||
|
||||
```lua
|
||||
require('nuwiki').setup({
|
||||
-- single-wiki shorthand
|
||||
wiki_root = '~/vimwiki', file_extension = '.wiki', syntax = 'vimwiki',
|
||||
log_level = 'warn',
|
||||
|
||||
-- or multi-wiki
|
||||
wikis = {
|
||||
{ name = 'personal', root = '~/vimwiki', diary_rel_path = 'diary',
|
||||
diary_frequency = 'daily', html = { html_path = '~/vimwiki/_html' } },
|
||||
},
|
||||
|
||||
diagnostic = { link_severity = 'warn' },
|
||||
mappings = { -- all default true except mouse
|
||||
enabled = true, wiki_prefix = true, links = true, lists = true,
|
||||
headers = true, table_editing = true, diary = true, html_export = true,
|
||||
text_objects = true, mouse = false,
|
||||
},
|
||||
folding = 'lsp', -- lsp | expr | off
|
||||
})
|
||||
```
|
||||
|
||||
Per-wiki keys include `index`, `diary_rel_path`/`diary_index`/
|
||||
`diary_frequency`/`diary_start_week_day`/`diary_sort`/`diary_caption_level`/
|
||||
`diary_header`, `listsyms`/`listsyms_propagate`/`list_margin`,
|
||||
`links_space_char`, `nested_syntaxes`, `auto_toc`, `maxhi`, and an `html`
|
||||
table (`html_path`, `template_path`/`template_default`/`template_ext`/
|
||||
`template_date_format`, `css_name`, `auto_export`,
|
||||
`html_filename_parameterization`, `exclude_files`, `color_dic`).
|
||||
|
||||
### Command surface
|
||||
|
||||
`ftplugin/vimwiki.vim` (Vim) and `lua/nuwiki/commands.lua` (Neovim) define the
|
||||
vimwiki command set with `:Vimwiki*` names plus `:Nuwiki*` aliases — index/tab
|
||||
index/UI select, diary (note/yesterday/tomorrow/next/prev/index/generate),
|
||||
link follow/backlinks/next/prev, goto/delete/rename, list toggle/reject/
|
||||
remove-done, TOC, generate/check links, find orphans, tags rebuild/search/
|
||||
generate, HTML 2HTML/browse/all/rss, table insert/move/colorize, and paste
|
||||
link/url. Search uses `lvimgrep` and backlinks/follow use built-in LSP
|
||||
requests; the rest route to `executeCommand`.
|
||||
|
||||
### Default keymaps
|
||||
|
||||
Buffer-local, gated per subgroup by `mappings.*`:
|
||||
|
||||
- **Wiki** `<Leader>ww`/`wt`/`ws`/`wi`, `<Leader>w<Leader>w|y|t|m|i`,
|
||||
`<Leader>wn`/`wd`/`wr`/`wc`
|
||||
- **Links** `<CR>` (follow / wrap-word), `<S-CR>`/`<C-CR>`/`<C-S-CR>`
|
||||
(split/vsplit/tab), `<BS>` (back), `<Tab>`/`<S-Tab>` (next/prev link), `+`
|
||||
(wrap as wikilink)
|
||||
- **Lists** `<C-Space>` (toggle), `gnt` (next task), `gln`/`glp` (cycle
|
||||
forward/back), `glx` (reject), `gll`/`glh` + `gLl`/`gLh` (indent/dedent,
|
||||
item vs subtree), `glr`/`gLr` (renumber list/all), `gl<Space>`/`gL<Space>`
|
||||
(remove done, list/whole-doc), `o`/`O` (open with bullet); insert-mode
|
||||
`<C-T>`/`<C-D>`, `<C-L><C-J>`/`<C-L><C-K>`, `<C-L><C-M>`; smart `<CR>`/`<Tab>`
|
||||
- **Headers** `=`/`-` (deeper/shallower), `]]`/`[[` (next/prev), `]=`/`[=`
|
||||
(sibling), `]u`/`[u` (parent) — pure VimL/Lua, no LSP round-trip
|
||||
- **Tables** `gqq`/`gww` (align), `<A-Left>`/`<A-Right>` (move column)
|
||||
- **Diary** `<C-Down>`/`<C-Up>` (next/prev entry)
|
||||
- **HTML** `<Leader>wh`/`whh`/`wha` (export/browse/all)
|
||||
- **Mouse** (opt-in) `<2-LeftMouse>` follow + shift/ctrl variants
|
||||
|
||||
**Text objects:** `ah`/`ih` (heading section), `aH`/`iH` (heading + subtree),
|
||||
`al`/`il` (list item), `a\`/`i\` (table cell), `ac`/`ic` (table column). Pure
|
||||
VimL/Lua.
|
||||
|
||||
### Folding
|
||||
|
||||
Primary: LSP `textDocument/foldingRange` (`folding::folding_ranges`, heading
|
||||
blocks + top-level lists). Fallback: a pure Lua `foldexpr` in
|
||||
`lua/nuwiki/folding.lua` for clients without `foldingRange`.
|
||||
|
||||
### Health check
|
||||
|
||||
`:checkhealth nuwiki` verifies the binary exists and responds to `--version`,
|
||||
the LSP client is attached, `.wiki` filetype detection works, and reports
|
||||
registered commands and per-wiki index state.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vimwiki syntax support
|
||||
|
||||
Parsed and highlighted (custom semantic-token legend; `syntax/nuwiki.vim`
|
||||
provides a no-LSP fallback):
|
||||
|
||||
- **Typefaces** bold `*…*`, italic `_…_`, bold-italic, strikethrough `~~…~~`,
|
||||
inline code, super `^…^` / sub `,,…,,`, keywords (TODO/DONE/STARTED/FIXME/
|
||||
FIXED/XXX/STOPPED)
|
||||
- **Links** plain/described/subdir/root/absolute wikilinks, `[[dir/]]`,
|
||||
interwiki (`wiki1:` / `wn.Name:`), diary, anchor-only and `Page#anchor`,
|
||||
raw URLs, `file:`/`local:`, transclusion `{{url|alt|attrs}}`
|
||||
- **Headers** levels 1–6, centered
|
||||
- **Lists** unordered (`-` `*` `#`), ordered (`1.` `1)` `a)` `A)` `i)` `I)`),
|
||||
nested/mixed, multi-line items, definition lists, all six checkbox states
|
||||
- **Tables** `|`-delimited, header separator, column alignment markers,
|
||||
colspan `>` / rowspan `\/`, inline formatting in cells
|
||||
- **Blocks** preformatted `{{{ … }}}` (optional language), inline math `$…$`,
|
||||
block math `{{$ … }}$` (optional environment), blockquotes, comments
|
||||
(`%% …`), horizontal rule `----`
|
||||
- **Tags** `:tag1:tag2:` (file / heading / standalone scope), tag-as-anchor
|
||||
- **Placeholders** `%title` `%nohtml` `%template` `%date`
|
||||
|
||||
---
|
||||
|
||||
## 6. CI/CD
|
||||
|
||||
`.gitea/workflows/ci.yaml` runs on every push and PR:
|
||||
|
||||
| Job | Command |
|
||||
|---|---|
|
||||
| fmt | `cargo fmt --all -- --check` |
|
||||
| clippy | `cargo clippy --workspace --all-targets -- -D warnings` |
|
||||
| test | `cargo test --workspace --all-targets` |
|
||||
| keymaps | `development/tests/test-keymaps.sh` (Neovim 0.11) + `test-keymaps-vim.sh` (Vim) |
|
||||
|
||||
`.gitea/workflows/release.yaml` triggers on `v*` tags: cross-compiles the four
|
||||
Linux targets (gnu/musl × x86_64/aarch64) via `cross`, packages `.tar.gz`, and
|
||||
creates a Gitea release using `RELEASE_TOKEN`. macOS and Windows binaries are
|
||||
built manually and attached to the same release. crates.io publishing is not
|
||||
wired up.
|
||||
|
||||
A change is semver-breaking if it alters `nuwiki-core` AST nodes, the
|
||||
`SyntaxPlugin`/`Renderer` traits, the user-config schema, or removes an LSP
|
||||
capability.
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# development/_common.sh — shared helpers for the start-* dev launchers.
|
||||
#
|
||||
# Sourced by start-nvim.sh and start-vim.sh (not run directly). Owns the
|
||||
# pieces both launchers share so they stay in lockstep — most importantly
|
||||
# the sample-wiki fixture, which previously had to be edited in two
|
||||
# places.
|
||||
#
|
||||
# Exports:
|
||||
# REPO_ROOT / DEV_DIR / WIKI_DIR — common paths
|
||||
# log — prefixed status line
|
||||
# ensure_binary — build + symlink nuwiki-ls
|
||||
# write_sample_wiki DIR — lay down the feature-showcase wiki
|
||||
# seed_wiki — seed WIKI_DIR (respects NUWIKI_DEV_NO_SEED)
|
||||
|
||||
# Resolve paths relative to this file so it works regardless of which
|
||||
# launcher sources it.
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEV_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/nuwiki-dev"
|
||||
WIKI_DIR="${NUWIKI_DEV_WIKI:-$DEV_DIR/wiki}"
|
||||
|
||||
log() { printf '\033[1;34m[nuwiki-dev]\033[0m %s\n' "$*"; }
|
||||
|
||||
ensure_binary() {
|
||||
local bin="$REPO_ROOT/bin/nuwiki-ls"
|
||||
local built="$REPO_ROOT/target/release/nuwiki-ls"
|
||||
if [[ "${NUWIKI_DEV_SKIP_BUILD:-0}" == "1" ]]; then
|
||||
if [[ ! -x "$bin" ]]; then
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 but $bin is missing — building anyway"
|
||||
else
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 — using existing $bin"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
log "building nuwiki-ls (release)…"
|
||||
cargo build --release -p nuwiki-ls --manifest-path "$REPO_ROOT/Cargo.toml"
|
||||
mkdir -p "$REPO_ROOT/bin"
|
||||
ln -sf "$built" "$bin"
|
||||
log "binary ready: $bin (built $(date -r "$built" '+%Y-%m-%d %H:%M:%S'))"
|
||||
}
|
||||
|
||||
# write_sample_wiki DIR — lay down a fixture that exercises every vimwiki
|
||||
# feature nuwiki supports: one page per feature group plus a diary.
|
||||
# Heredocs use a quoted delimiter so `$math`, backticks, and backslashes
|
||||
# land verbatim.
|
||||
write_sample_wiki() {
|
||||
local root="$1"
|
||||
mkdir -p "$root" "$root/diary"
|
||||
|
||||
cat > "$root/index.wiki" <<'EOF'
|
||||
= nuwiki sample wiki =
|
||||
|
||||
%% Generated by the start-* dev launchers. A single fixture that exercises
|
||||
%% every vimwiki feature nuwiki supports — open the linked pages and run
|
||||
%% the plugin commands against them.
|
||||
|
||||
One page per feature group:
|
||||
|
||||
- [[Syntax]] — inline formatting, blocks, comments
|
||||
- [[Lists]] — bullets, ordering, checkbox states, nesting
|
||||
- [[Tables]] — alignment and spanning cells
|
||||
- [[Links]] — every wikilink and URL form
|
||||
- [[Notes]] — anchor targets for cross-page links
|
||||
- [[diary/diary]] — the diary index
|
||||
|
||||
=== Heading level 3 ===
|
||||
==== Heading level 4 ====
|
||||
===== Heading level 5 =====
|
||||
====== Heading level 6 ======
|
||||
|
||||
== Commands to try ==
|
||||
|
||||
- :NuwikiTOC
|
||||
- :NuwikiGenerateLinks
|
||||
- :NuwikiCheckLinks
|
||||
- :NuwikiMakeDiaryNote
|
||||
|
||||
:sample:smoke-test:sandbox:
|
||||
EOF
|
||||
|
||||
cat > "$root/Syntax.wiki" <<'EOF'
|
||||
= Syntax showcase =
|
||||
|
||||
Back to [[index]].
|
||||
|
||||
== Inline formatting ==
|
||||
|
||||
*bold*, _italic_, *_bold italic_*, ~~strikethrough~~, `inline code`,
|
||||
super^script^ and sub,,script,,, and inline math $e^{i\pi} + 1 = 0$.
|
||||
|
||||
== Keywords ==
|
||||
|
||||
TODO STARTED FIXME XXX DONE FIXED STOPPED
|
||||
|
||||
== Horizontal rule ==
|
||||
|
||||
----
|
||||
|
||||
== Blockquote ==
|
||||
|
||||
> A quoted line.
|
||||
> A second quoted line.
|
||||
|
||||
== Definition list ==
|
||||
|
||||
Term:: Definition of the term.
|
||||
Another:: Its definition.
|
||||
|
||||
== Code block ==
|
||||
|
||||
{{{python
|
||||
def greet(name):
|
||||
return f"hello {name}"
|
||||
}}}
|
||||
|
||||
== Math block ==
|
||||
|
||||
{{$
|
||||
\sum_{i=1}^{n} i = \frac{n(n+1)}{2}
|
||||
}}$
|
||||
|
||||
== Comments ==
|
||||
|
||||
%% a single-line comment
|
||||
%%+ a multi-line
|
||||
comment block +%%
|
||||
|
||||
== Transclusion ==
|
||||
|
||||
{{file:image.png}}
|
||||
{{image.png|alt text|width=120}}
|
||||
EOF
|
||||
|
||||
cat > "$root/Lists.wiki" <<'EOF'
|
||||
= Lists =
|
||||
|
||||
Back to [[index]].
|
||||
|
||||
== Unordered ==
|
||||
|
||||
- item with dash
|
||||
- second
|
||||
- nested under dash
|
||||
* item with star
|
||||
# item with hash
|
||||
|
||||
== Ordered ==
|
||||
|
||||
1. first
|
||||
2. second
|
||||
1. nested ordered
|
||||
1) paren style
|
||||
a) alpha
|
||||
A) Alpha
|
||||
i) roman
|
||||
I) Roman
|
||||
|
||||
== Checkbox states ==
|
||||
|
||||
- [ ] empty (0%)
|
||||
- [.] started (1-33%)
|
||||
- [o] in progress (34-66%)
|
||||
- [O] nearly done (67-99%)
|
||||
- [X] done (100%)
|
||||
- [-] rejected
|
||||
|
||||
== Nested with checkboxes ==
|
||||
|
||||
- [ ] parent task
|
||||
- [X] done subtask
|
||||
- [ ] pending subtask
|
||||
- [ ] deep subtask
|
||||
EOF
|
||||
|
||||
cat > "$root/Tables.wiki" <<'EOF'
|
||||
= Tables =
|
||||
|
||||
Back to [[index]].
|
||||
|
||||
== Plain ==
|
||||
|
||||
| Name | Role |
|
||||
| Alice | Author |
|
||||
| Bob | Editor |
|
||||
|
||||
== Aligned ==
|
||||
|
||||
| Left | Center | Right |
|
||||
|:-----|:------:|------:|
|
||||
| a | b | c |
|
||||
| d | e | f |
|
||||
|
||||
== Spanning cells ==
|
||||
|
||||
| Header | Span |
|
||||
| cell | > |
|
||||
| rowspan | value |
|
||||
| \/ | value |
|
||||
EOF
|
||||
|
||||
cat > "$root/Links.wiki" <<'EOF'
|
||||
= Links =
|
||||
|
||||
Back to [[index]].
|
||||
|
||||
== Wikilinks ==
|
||||
|
||||
- [[Notes]]
|
||||
- [[Notes|described]]
|
||||
- [[Notes#Section one]]
|
||||
- [[Notes#Section one|anchored + described]]
|
||||
- [[/index]]
|
||||
- [[diary/]]
|
||||
- [[#Wikilinks]]
|
||||
|
||||
== Diary and interwiki ==
|
||||
|
||||
- [[diary:2026-05-30]]
|
||||
- [[wiki1:index]]
|
||||
- [[wn.MyWiki:index]]
|
||||
|
||||
== External and raw URLs ==
|
||||
|
||||
- [[https://example.com]]
|
||||
- [[https://example.com|Example]]
|
||||
- https://example.com
|
||||
- mailto:gffranco@gmail.com
|
||||
- file:///etc/hosts
|
||||
EOF
|
||||
|
||||
cat > "$root/Notes.wiki" <<'EOF'
|
||||
= Notes =
|
||||
|
||||
A page that other pages point at. Back to [[index]].
|
||||
|
||||
== Section one ==
|
||||
|
||||
Anchor target for cross-page links.
|
||||
|
||||
== Section two ==
|
||||
|
||||
Another anchor target.
|
||||
EOF
|
||||
|
||||
cat > "$root/diary/diary.wiki" <<'EOF'
|
||||
= Diary =
|
||||
|
||||
Back to [[/index]].
|
||||
|
||||
- [[2026-05-30]]
|
||||
EOF
|
||||
|
||||
cat > "$root/diary/2026-05-30.wiki" <<'EOF'
|
||||
= 2026-05-30 =
|
||||
|
||||
A diary entry. Back to [[diary]].
|
||||
|
||||
- [X] set up sample wiki
|
||||
- [ ] review every feature page
|
||||
EOF
|
||||
}
|
||||
|
||||
seed_wiki() {
|
||||
if [[ "${NUWIKI_DEV_NO_SEED:-0}" == "1" ]]; then
|
||||
log "NUWIKI_DEV_NO_SEED=1 — skipping wiki seed (using $WIKI_DIR as-is)"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$WIKI_DIR" "$WIKI_DIR/diary"
|
||||
# Seed the whole showcase as a unit; skip when an index already exists
|
||||
# so a re-run never clobbers edits made while testing.
|
||||
if [[ -f "$WIKI_DIR/index.wiki" ]]; then
|
||||
return
|
||||
fi
|
||||
write_sample_wiki "$WIKI_DIR"
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>nuwiki Architecture Diagram</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: #020617;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #22d3ee;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #1e293b;
|
||||
padding: 1.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
min-width: 900px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #1e293b;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.card-dot.cyan { background: #22d3ee; }
|
||||
.card-dot.emerald { background: #34d399; }
|
||||
.card-dot.violet { background: #a78bfa; }
|
||||
.card-dot.amber { background: #fbbf24; }
|
||||
.card-dot.rose { background: #fb7185; }
|
||||
|
||||
.card h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card ul {
|
||||
list-style: none;
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.card li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: #475569;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-row">
|
||||
<div class="pulse-dot"></div>
|
||||
<h1>nuwiki Architecture</h1>
|
||||
</div>
|
||||
<p class="subtitle">Layered architecture showing the Rust LSP backend and editor integration layers</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Diagram -->
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 720">
|
||||
<!-- Definitions -->
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
|
||||
</marker>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- Background Grid -->
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
|
||||
<!-- Layer Model from SPEC.md -->
|
||||
<!-- Consumer Layer -->
|
||||
<rect x="50" y="60" width="400" height="80" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="250" y="95" fill="white" font-size="12" font-weight="600" text-anchor="middle">Consumer Layer</text>
|
||||
<text x="250" y="115" fill="#94a3b8" font-size="9" text-anchor="middle">Editor highlight, HTML export, TOC, etc.</text>
|
||||
|
||||
<!-- AST Layer -->
|
||||
<rect x="50" y="180" width="400" height="80" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="250" y="215" fill="white" font-size="12" font-weight="600" text-anchor="middle">AST Layer (nuwiki-core)</text>
|
||||
<text x="250" y="235" fill="#94a3b8" font-size="9" text-anchor="middle">Document > Block nodes > Inline nodes</text>
|
||||
|
||||
<!-- Parser Layer -->
|
||||
<rect x="50" y="300" width="400" height="80" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="250" y="335" fill="white" font-size="12" font-weight="600" text-anchor="middle">Parser Layer</text>
|
||||
<text x="250" y="355" fill="#94a3b8" font-size="9" text-anchor="middle">Syntax-specific (nuwiki-core)</text>
|
||||
|
||||
<!-- Lexer Layer -->
|
||||
<rect x="50" y="420" width="400" height="80" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="250" y="455" fill="white" font-size="12" font-weight="600" text-anchor="middle">Lexer Layer</text>
|
||||
<text x="250" y="475" fill="#94a3b8" font-size="9" text-anchor="middle">Syntax-specific (nuwiki-core)</text>
|
||||
|
||||
<!-- Raw Text Input -->
|
||||
<rect x="50" y="540" width="400" height="60" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="250" y="575" fill="white" font-size="12" font-weight="600" text-anchor="middle">Raw Text Input</text>
|
||||
|
||||
<!-- Arrows showing data flow downward -->
|
||||
<line x1="250" y1="140" x2="250" y2="180" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
<line x1="250" y1="260" x2="250" y2="300" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
<line x1="250" y1="380" x2="250" y2="420" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
<line x1="250" y1="500" x2="250" y2="540" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Crate Dependency Rules (right side) -->
|
||||
<!-- nuwiki-ls -->
|
||||
<rect x="550" y="100" width="180" height="60" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text x="640" y="130" fill="white" font-size="11" font-weight="600" text-anchor="middle">nuwiki-ls</text>
|
||||
<text x="640" y="145" fill="#94a3b8" font-size="8" text-anchor="middle">Binary crate — starts stdio LSP server</text>
|
||||
|
||||
<!-- nuwiki-lsp -->
|
||||
<rect x="550" y="200" width="180" height="60" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text x="640" y="230" fill="white" font-size="11" font-weight="600" text-anchor="middle">nuwiki-lsp</text>
|
||||
<text x="640" y="245" fill="#94a3b8" font-size="8" text-anchor="middle">LSP protocol bridge (tower-lsp)</text>
|
||||
|
||||
<!-- nuwiki-core -->
|
||||
<rect x="550" y="300" width="180" height="60" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text x="640" y="330" fill="white" font-size="11" font-weight="600" text-anchor="middle">nuwiki-core</text>
|
||||
<text x="640" y="345" fill="#94a3b8" font-size="8" text-anchor="middle">Parser, AST, Renderer (editor-agnostic)</text>
|
||||
|
||||
<!-- Dependencies arrows -->
|
||||
<line x1="730" y1="130" x2="730" y2="170" stroke="#34d399" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
<line x1="730" y1="230" x2="730" y2="270" stroke="#34d399" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Dependency labels -->
|
||||
<text x="750" y="150" fill="#94a3b8" font-size="8">depends on</text>
|
||||
<text x="750" y="250" fill="#94a3b8" font-size="8">depends on</text>
|
||||
|
||||
<!-- Editor Integration Layers -->
|
||||
<!-- VimL Layer -->
|
||||
<rect x="50" y="620" width="250" height="60" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text x="175" y="648" fill="white" font-size="10" font-weight="600" text-anchor="middle">VimL Layer</text>
|
||||
<text x="175" y="663" fill="#94a3b8" font-size="8" text-anchor="middle">plugin/nuwiki.vim, autoload/</text>
|
||||
|
||||
<!-- Lua Layer (Neovim) -->
|
||||
<rect x="350" y="620" width="250" height="60" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text x="475" y="648" fill="white" font-size="10" font-weight="600" text-anchor="middle">Lua Layer</text>
|
||||
<text x="475" y="663" fill="#94a3b8" font-size="8" text-anchor="middle">lua/nuwiki/</text>
|
||||
|
||||
<!-- Editor arrows from layers to LSP server -->
|
||||
<line x1="175" y1="680" x2="250" y2="600" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
<line x1="475" y1="680" x2="250" y2="600" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Labels for editor connections -->
|
||||
<text x="100" y="695" fill="#94a3b8" font-size="8">Vim/Neovim</text>
|
||||
<text x="400" y="695" fill="#94a3b8" font-size="8">Neovim only</text>
|
||||
|
||||
<!-- LSP Features Section -->
|
||||
<rect x="750" y="100" width="200" height="200" rx="8" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<text x="850" y="125" fill="white" font-size="11" font-weight="600" text-anchor="middle">LSP Features</text>
|
||||
<text x="760" y="145" fill="#94a3b8" font-size="8">• textDocument/semanticTokens</text>
|
||||
<text x="760" y="158" fill="#94a3b8" font-size="8">• textDocument/publishDiagnostics</text>
|
||||
<text x="760" y="171" fill="#94a3b8" font-size="8">• textDocument/definition</text>
|
||||
<text x="760" y="184" fill="#94a3b8" font-size="8">• textDocument/references</text>
|
||||
<text x="760" y="197" fill="#94a3b8" font-size="8">• textDocument/documentSymbol</text>
|
||||
<text x="760" y="210" fill="#94a3b8" font-size="8">• textDocument/hover</text>
|
||||
<text x="760" y="223" fill="#94a3b8" font-size="8">• textDocument/completion</text>
|
||||
<text x="760" y="236" fill="#94a3b8" font-size="8">• workspace/rename</text>
|
||||
<text x="760" y="249" fill="#94a3b8" font-size="8">• workspace/symbol</text>
|
||||
|
||||
<!-- Arrow from LSP layer to LSP Features -->
|
||||
<line x1="730" y1="260" x2="750" y2="200" stroke="#fbbf24" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Security Boundary/Crate Dependencies -->
|
||||
<rect x="540" y="80" width="200" height="300" rx="12" fill="rgba(251, 191, 36, 0.05)" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4"/>
|
||||
<text x="552" y="98" fill="#fbbf24" font-size="9" font-weight="600">Crate Dependencies</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<text x="750" y="350" fill="white" font-size="10" font-weight="600">Legend</text>
|
||||
|
||||
<rect x="750" y="362" width="16" height="10" rx="2" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1"/>
|
||||
<text x="772" y="370" fill="#94a3b8" font-size="8">Editor Layers</text>
|
||||
|
||||
<rect x="750" y="382" width="16" height="10" rx="2" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1"/>
|
||||
<text x="772" y="390" fill="#94a3b8" font-size="8">Core Crates</text>
|
||||
|
||||
<rect x="750" y="402" width="16" height="10" rx="2" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1"/>
|
||||
<text x="772" y="410" fill="#94a3b8" font-size="8">Layer Boundaries</text>
|
||||
|
||||
<line x1="750" y1="425" x2="766" y2="425" stroke="#fbbf24" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<text x="772" y="433" fill="#94a3b8" font-size="8">Data Flow</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Info Cards -->
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-dot cyan"></div>
|
||||
<h3>Editor Integration</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<li>• VimL layer for universal Vim support</li>
|
||||
<li>• Lua layer for Neovim integration</li>
|
||||
<li>• Zero-logic editor layers (pure wiring only)</li>
|
||||
<li>• Supports vim-lsp and coc.nvim for Vim</li>
|
||||
<li>• Built-in LSP client for Neovim 0.11+</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-dot emerald"></div>
|
||||
<h3>Core Architecture</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<li>• Strict layer separation (no circular deps)</li>
|
||||
<li>• nuwiki-core is editor-agnostic</li>
|
||||
<li>• LSP bridge uses tower-lsp</li>
|
||||
<li>• Binary crate is intentionally thin</li>
|
||||
<li>• Strict dependency rules enforced</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-dot violet"></div>
|
||||
<h3>LSP Features</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<li>• Syntax highlighting (semantic tokens)</li>
|
||||
<li>• Diagnostics (broken links, parse errors)</li>
|
||||
<li>• Go-to-definition and references</li>
|
||||
<li>• Document symbols (TOC/outline)</li>
|
||||
<li>• Hover, completion, workspace symbols</li>
|
||||
<li>• Rename and workspace-wide operations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<p class="footer">
|
||||
nuwiki • Rust-based vimwiki replacement with LSP backend
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,11 +10,12 @@
|
||||
# $XDG_CACHE_HOME/nuwiki-dev/wiki
|
||||
# * calls `require('nuwiki').setup({...})`
|
||||
#
|
||||
# Usage: ./start-nvim.sh [extra args...]
|
||||
# ./start-nvim.sh # open the scratch wiki's index
|
||||
# ./start-nvim.sh path/to/note.wiki # open a specific file
|
||||
# NUWIKI_DEV_WIKI=/tmp/foo ./start-nvim.sh
|
||||
# NUWIKI_DEV_SKIP_BUILD=1 ./start-nvim.sh # reuse the cached binary
|
||||
# Usage: ./development/start-nvim.sh [extra args...]
|
||||
# ./development/start-nvim.sh # open the scratch wiki's index
|
||||
# ./development/start-nvim.sh path/to/note.wiki # open a specific file
|
||||
# NUWIKI_DEV_WIKI=/tmp/foo ./development/start-nvim.sh
|
||||
# NUWIKI_DEV_NO_SEED=1 ./development/start-nvim.sh # use WIKI_DIR as-is (no scratch seeding)
|
||||
# NUWIKI_DEV_SKIP_BUILD=1 ./development/start-nvim.sh # reuse the cached binary
|
||||
#
|
||||
# The release binary is rebuilt on every launch so source changes
|
||||
# always reach the running LSP. `cargo build --release` is incremental,
|
||||
@@ -27,82 +28,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEV_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/nuwiki-dev"
|
||||
WIKI_DIR="${NUWIKI_DEV_WIKI:-$DEV_DIR/wiki}"
|
||||
# Shared paths + helpers (log, ensure_binary, write_sample_wiki, seed_wiki).
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
|
||||
|
||||
INIT_FILE="$DEV_DIR/init.lua"
|
||||
STATE_DIR="$DEV_DIR/nvim-state"
|
||||
DATA_DIR="$DEV_DIR/nvim-data"
|
||||
CONFIG_DIR="$DEV_DIR/nvim-config"
|
||||
|
||||
log() { printf '\033[1;34m[nuwiki-dev]\033[0m %s\n' "$*"; }
|
||||
|
||||
ensure_binary() {
|
||||
local bin="$REPO_ROOT/bin/nuwiki-ls"
|
||||
local built="$REPO_ROOT/target/release/nuwiki-ls"
|
||||
if [[ "${NUWIKI_DEV_SKIP_BUILD:-0}" == "1" ]]; then
|
||||
if [[ ! -x "$bin" ]]; then
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 but $bin is missing — building anyway"
|
||||
else
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 — using existing $bin"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
log "building nuwiki-ls (release)…"
|
||||
cargo build --release -p nuwiki-ls --manifest-path "$REPO_ROOT/Cargo.toml"
|
||||
mkdir -p "$REPO_ROOT/bin"
|
||||
ln -sf "$built" "$bin"
|
||||
log "binary ready: $bin (built $(date -r "$built" '+%Y-%m-%d %H:%M:%S'))"
|
||||
}
|
||||
|
||||
seed_wiki() {
|
||||
mkdir -p "$WIKI_DIR" "$WIKI_DIR/diary"
|
||||
if [[ ! -f "$WIKI_DIR/index.wiki" ]]; then
|
||||
cat > "$WIKI_DIR/index.wiki" <<'EOF'
|
||||
= Welcome to nuwiki =
|
||||
|
||||
This is a scratch wiki created by start-nvim.sh. Edit, run commands,
|
||||
or jump to the linked pages to test the plugin.
|
||||
|
||||
== Smoke test ==
|
||||
|
||||
- [[Notes]]
|
||||
- [[diary/]]
|
||||
- [ ] Toggle me with <C-Space>
|
||||
- [ ] Or with :VimwikiToggleListItem
|
||||
|
||||
== Commands to try ==
|
||||
|
||||
- :VimwikiTOC
|
||||
- :VimwikiGenerateLinks
|
||||
- :VimwikiCheckLinks
|
||||
- :VimwikiMakeDiaryNote
|
||||
- :Vimwiki2HTMLBrowse
|
||||
|
||||
== Tags ==
|
||||
|
||||
:smoke-test:sandbox:
|
||||
|
||||
== Links ==
|
||||
|
||||
* [[Notes]]
|
||||
* [[Notes#section-one]]
|
||||
* https://example.com
|
||||
EOF
|
||||
fi
|
||||
if [[ ! -f "$WIKI_DIR/Notes.wiki" ]]; then
|
||||
cat > "$WIKI_DIR/Notes.wiki" <<'EOF'
|
||||
= Notes =
|
||||
|
||||
A second page so [[index]] has somewhere to point.
|
||||
|
||||
== Section one ==
|
||||
|
||||
Anchor target for :checkhealth-style smoke tests.
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
write_init() {
|
||||
mkdir -p "$DEV_DIR"
|
||||
cat > "$INIT_FILE" <<EOF
|
||||
@@ -7,13 +7,13 @@
|
||||
# the dev cache on first run so the LSP integration is exercisable
|
||||
# without polluting your real Vim install.
|
||||
#
|
||||
# Usage: ./start-vim.sh [extra args...]
|
||||
# ./start-vim.sh # open the scratch wiki's index
|
||||
# ./start-vim.sh path/to/note.wiki # open a specific file
|
||||
# NUWIKI_DEV_WIKI=/tmp/foo ./start-vim.sh
|
||||
# NUWIKI_DEV_NO_LSP=1 ./start-vim.sh # skip vim-lsp setup
|
||||
# NUWIKI_DEV_NO_SEED=1 ./start-vim.sh # use WIKI_DIR as-is (no scratch seeding)
|
||||
# NUWIKI_DEV_SKIP_BUILD=1 ./start-vim.sh # reuse the cached binary
|
||||
# Usage: ./development/start-vim.sh [extra args...]
|
||||
# ./development/start-vim.sh # open the scratch wiki's index
|
||||
# ./development/start-vim.sh path/to/note.wiki # open a specific file
|
||||
# NUWIKI_DEV_WIKI=/tmp/foo ./development/start-vim.sh
|
||||
# NUWIKI_DEV_NO_LSP=1 ./development/start-vim.sh # skip vim-lsp setup
|
||||
# NUWIKI_DEV_NO_SEED=1 ./development/start-vim.sh # use WIKI_DIR as-is (no scratch seeding)
|
||||
# NUWIKI_DEV_SKIP_BUILD=1 ./development/start-vim.sh # reuse the cached binary
|
||||
#
|
||||
# The release binary is rebuilt on every launch so source changes
|
||||
# always reach the running LSP. `cargo build --release` is incremental,
|
||||
@@ -25,34 +25,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEV_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/nuwiki-dev"
|
||||
WIKI_DIR="${NUWIKI_DEV_WIKI:-$DEV_DIR/wiki}"
|
||||
# Shared paths + helpers (log, ensure_binary, write_sample_wiki, seed_wiki).
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
|
||||
|
||||
VIMRC="$DEV_DIR/vimrc"
|
||||
VIM_STATE="$DEV_DIR/vim-state"
|
||||
VIM_LSP_DIR="$DEV_DIR/vim-lsp"
|
||||
ASYNC_DIR="$DEV_DIR/async.vim"
|
||||
|
||||
log() { printf '\033[1;34m[nuwiki-dev]\033[0m %s\n' "$*"; }
|
||||
|
||||
ensure_binary() {
|
||||
local bin="$REPO_ROOT/bin/nuwiki-ls"
|
||||
local built="$REPO_ROOT/target/release/nuwiki-ls"
|
||||
if [[ "${NUWIKI_DEV_SKIP_BUILD:-0}" == "1" ]]; then
|
||||
if [[ ! -x "$bin" ]]; then
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 but $bin is missing — building anyway"
|
||||
else
|
||||
log "NUWIKI_DEV_SKIP_BUILD=1 — using existing $bin"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
log "building nuwiki-ls (release)…"
|
||||
cargo build --release -p nuwiki-ls --manifest-path "$REPO_ROOT/Cargo.toml"
|
||||
mkdir -p "$REPO_ROOT/bin"
|
||||
ln -sf "$built" "$bin"
|
||||
log "binary ready: $bin (built $(date -r "$built" '+%Y-%m-%d %H:%M:%S'))"
|
||||
}
|
||||
|
||||
clone_if_missing() {
|
||||
local repo_url="$1"
|
||||
local dest="$2"
|
||||
@@ -77,51 +57,6 @@ ensure_vim_lsp() {
|
||||
clone_if_missing https://github.com/prabirshrestha/async.vim.git "$ASYNC_DIR" || true
|
||||
}
|
||||
|
||||
seed_wiki() {
|
||||
if [[ "${NUWIKI_DEV_NO_SEED:-0}" == "1" ]]; then
|
||||
log "NUWIKI_DEV_NO_SEED=1 — skipping wiki seed (using $WIKI_DIR as-is)"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$WIKI_DIR" "$WIKI_DIR/diary"
|
||||
if [[ ! -f "$WIKI_DIR/index.wiki" ]]; then
|
||||
cat > "$WIKI_DIR/index.wiki" <<'EOF'
|
||||
= Welcome to nuwiki =
|
||||
|
||||
This is a scratch wiki created by start-vim.sh. Edit, run commands,
|
||||
or jump to the linked pages to test the plugin.
|
||||
|
||||
== Smoke test ==
|
||||
|
||||
- [[Notes]]
|
||||
- [[diary/]]
|
||||
- [ ] Toggle me with :VimwikiToggleListItem
|
||||
- [ ] Inspect the LSP via :LspStatus
|
||||
|
||||
== Commands to try ==
|
||||
|
||||
- :VimwikiTOC
|
||||
- :VimwikiGenerateLinks
|
||||
- :VimwikiCheckLinks
|
||||
- :VimwikiMakeDiaryNote
|
||||
|
||||
== Tags ==
|
||||
|
||||
:smoke-test:sandbox:
|
||||
EOF
|
||||
fi
|
||||
if [[ ! -f "$WIKI_DIR/Notes.wiki" ]]; then
|
||||
cat > "$WIKI_DIR/Notes.wiki" <<'EOF'
|
||||
= Notes =
|
||||
|
||||
A second page so [[index]] has somewhere to point.
|
||||
|
||||
== Section one ==
|
||||
|
||||
Anchor target for cross-page link follows.
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
write_vimrc() {
|
||||
mkdir -p "$DEV_DIR"
|
||||
cat > "$VIMRC" <<EOF
|
||||
@@ -1,8 +1,8 @@
|
||||
" scripts/syntax-diag.vim — dump highlighting state to /tmp/nuwiki-syndiag.log
|
||||
" development/syntax-diag.vim — dump highlighting state to /tmp/nuwiki-syndiag.log
|
||||
"
|
||||
" Usage: open a .wiki file with VARIED markup (heading, *bold*, _italic_,
|
||||
" `code`), then:
|
||||
" :source scripts/syntax-diag.vim
|
||||
" :source development/syntax-diag.vim
|
||||
" Then paste /tmp/nuwiki-syndiag.log back.
|
||||
|
||||
redir! > /tmp/nuwiki-syndiag.log
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# scripts/test-keymaps-vim.sh — Vim counterpart of test-keymaps.sh.
|
||||
# development/tests/test-keymaps-vim.sh — Vim counterpart of test-keymaps.sh.
|
||||
#
|
||||
# Pure-VimL portion only. The LSP-roundtrip keymaps (`<C-Space>`,
|
||||
# `=`, `-`, …) talk to the server via vim-lsp's timer-driven async
|
||||
# layer, which doesn't fire reliably inside `vim -e -s`. Those have
|
||||
# Neovim coverage in scripts/test-keymaps.lua, exercising the same
|
||||
# Neovim coverage in development/tests/test-keymaps.lua, exercising the same
|
||||
# Lua command layer + server binary.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TMP="$(mktemp -d -t nuwiki-keymap-vim-test-XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
@@ -33,6 +33,9 @@ VIMRC="$TMP/vimrc"
|
||||
cat > "$VIMRC" <<EOF
|
||||
set nocompatible
|
||||
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||
" Enable the opt-in mouse mappings before the ftplugin loads so the
|
||||
" harness can assert the full documented mapping surface (doc §6 mouse).
|
||||
let g:nuwiki_mouse_mappings = 1
|
||||
filetype plugin indent on
|
||||
syntax enable
|
||||
" ftdetect/nuwiki.vim already maps *.wiki → vimwiki via \`set filetype\`.
|
||||
@@ -44,7 +47,7 @@ log "running harness…"
|
||||
# `</dev/null` so Vim's ex-mode doesn't hang waiting on stdin;
|
||||
# `timeout` so a misbehaving script can't stall CI for 30 minutes.
|
||||
NUWIKI_KEYMAP_RESULTS="$RESULTS" \
|
||||
timeout 30 vim -e -s -u "$VIMRC" -c "edit $SEED" -c "source $REPO_ROOT/scripts/test-keymaps-vim.vim" \
|
||||
timeout 30 vim -e -s -u "$VIMRC" -c "edit $SEED" -c "source $REPO_ROOT/development/tests/test-keymaps-vim.vim" \
|
||||
</dev/null >"$TMP/vim.log" 2>&1 || true
|
||||
|
||||
if [[ ! -f "$RESULTS" ]]; then
|
||||
@@ -1,4 +1,4 @@
|
||||
" scripts/test-keymaps-vim.vim — driven by scripts/test-keymaps-vim.sh.
|
||||
" development/tests/test-keymaps-vim.vim — driven by development/tests/test-keymaps-vim.sh.
|
||||
"
|
||||
" Pure-VimL keymap regression suite. Plain Vim's LSP path
|
||||
" (vim-lsp + async.vim) talks to the server via timers, which won't
|
||||
@@ -6,7 +6,7 @@
|
||||
" are restricted to the bindings whose RHS is implemented entirely in
|
||||
" VimL (header/link nav, `o`/`O` bullet continuation, the wrap step
|
||||
" of `<CR>`). LSP-roundtrip bindings (`<C-Space>`, `=`, `-`, …) get
|
||||
" their coverage from the Neovim harness in scripts/test-keymaps.lua
|
||||
" their coverage from the Neovim harness in development/tests/test-keymaps.lua
|
||||
" — same Lua codepath, same server.
|
||||
|
||||
let s:results = []
|
||||
@@ -95,6 +95,107 @@ else
|
||||
call s:record(0, 'cmd.NuwikiIndex_defined', ':NuwikiIndex not registered')
|
||||
endif
|
||||
|
||||
" ===== Documented command surface =====
|
||||
" Every command must be reachable in BOTH the canonical :Nuwiki* form and
|
||||
" the vimwiki-compatible :Vimwiki* form. The suffixes below are the command
|
||||
" list minus the prefix. :NuwikiInstall is the one exception — it's a nuwiki-only
|
||||
" maintenance command with no :Vimwiki* alias, so it's checked separately.
|
||||
|
||||
let s:both_forms = [
|
||||
\ 'Index', 'TabIndex', 'UISelect', 'Goto', 'FollowLink', 'Backlinks',
|
||||
\ 'NextLink', 'PrevLink', 'BaddLink', 'RenameFile', 'DeleteFile',
|
||||
\ 'MakeDiaryNote', 'MakeYesterdayDiaryNote', 'MakeTomorrowDiaryNote',
|
||||
\ 'DiaryIndex', 'DiaryGenerateLinks', 'DiaryNextDay', 'DiaryPrevDay',
|
||||
\ 'TOC', 'GenerateLinks', 'CheckLinks', 'SearchTags', 'GenerateTagLinks',
|
||||
\ 'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||
\ ]
|
||||
for s:suffix in s:both_forms
|
||||
for s:prefix in ['Nuwiki', 'Vimwiki']
|
||||
let s:cmd = s:prefix . s:suffix
|
||||
let s:ok = exists(':' . s:cmd) == 2
|
||||
call s:record(s:ok ? 1 : 0, 'surface.' . s:cmd,
|
||||
\ s:ok ? '' : ':' . s:cmd . ' not registered')
|
||||
endfor
|
||||
endfor
|
||||
let s:ok = exists(':NuwikiInstall') == 2
|
||||
call s:record(s:ok ? 1 : 0, 'surface.NuwikiInstall',
|
||||
\ s:ok ? '' : ':NuwikiInstall not registered')
|
||||
|
||||
" ===== Pure-VimL command invocation (cursor movement) =====
|
||||
" :NuwikiNextLink / :NuwikiPrevLink wrap search('\[\[', …) so they run
|
||||
" entirely in VimL and move the cursor without touching the LSP.
|
||||
|
||||
call s:set_buf(['intro [[A]] more [[B]] end'])
|
||||
call cursor(1, 1)
|
||||
silent NuwikiNextLink
|
||||
call s:record(
|
||||
\ col('.') == 7 ? 1 : 0,
|
||||
\ 'invoke.NuwikiNextLink_jumps_to_first_link',
|
||||
\ 'col=' . col('.'))
|
||||
silent NuwikiNextLink
|
||||
call s:record(
|
||||
\ col('.') == 18 ? 1 : 0,
|
||||
\ 'invoke.NuwikiNextLink_jumps_to_second_link',
|
||||
\ 'col=' . col('.'))
|
||||
silent NuwikiPrevLink
|
||||
call s:record(
|
||||
\ col('.') == 7 ? 1 : 0,
|
||||
\ 'invoke.NuwikiPrevLink_jumps_back',
|
||||
\ 'col=' . col('.'))
|
||||
|
||||
" The :Vimwiki* alias must drive the identical motion.
|
||||
call cursor(1, 1)
|
||||
silent VimwikiNextLink
|
||||
call s:record(
|
||||
\ col('.') == 7 ? 1 : 0,
|
||||
\ 'invoke.VimwikiNextLink_jumps_to_first_link',
|
||||
\ 'col=' . col('.'))
|
||||
|
||||
" ===== Documented mapping surface =====
|
||||
" Every documented mapping must resolve to a buffer-local mapping in the
|
||||
" mode(s) the doc lists it under. The mouse group is opt-in — the shell
|
||||
" wrapper sets g:nuwiki_mouse_mappings before the ftplugin loads.
|
||||
|
||||
function! s:has_map(lhs, mode) abort
|
||||
let l:d = maparg(a:lhs, a:mode, 0, 1)
|
||||
return !empty(l:d) && get(l:d, 'buffer', 0)
|
||||
endfunction
|
||||
|
||||
let s:mapping_surface = [
|
||||
\ ['<CR>', 'n'], ['<S-CR>', 'n'], ['<C-CR>', 'n'], ['<C-S-CR>', 'n'],
|
||||
\ ['<BS>', 'n'], ['<Tab>', 'n'], ['<S-Tab>', 'n'], ['+', 'nx'],
|
||||
\ ['<C-Space>', 'nx'], ['<C-@>', 'nx'], ['<Nul>', 'nx'],
|
||||
\ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'],
|
||||
\ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'],
|
||||
\ ['glr', 'n'], ['gLr', 'n'], ['gl<Space>', 'n'], ['gL<Space>', 'n'],
|
||||
\ ['o', 'n'], ['O', 'n'],
|
||||
\ ['=', 'n'], ['-', 'n'], [']]', 'n'], ['[[', 'n'],
|
||||
\ [']=', 'n'], ['[=', 'n'], [']u', 'n'], ['[u', 'n'],
|
||||
\ ['gqq', 'n'], ['gq1', 'n'], ['gww', 'n'], ['gw1', 'n'],
|
||||
\ ['<A-Left>', 'n'], ['<A-Right>', 'n'],
|
||||
\ ['<Leader>ww', 'n'], ['<Leader>ws', 'n'], ['<Leader>wi', 'n'],
|
||||
\ ['<Leader>w<Leader>w', 'n'], ['<Leader>w<Leader>y', 'n'],
|
||||
\ ['<Leader>w<Leader>t', 'n'], ['<Leader>w<Leader>i', 'n'],
|
||||
\ ['<Leader>wn', 'n'], ['<Leader>wd', 'n'], ['<Leader>wr', 'n'],
|
||||
\ ['<Leader>wh', 'n'], ['<Leader>whh', 'n'], ['<Leader>wha', 'n'],
|
||||
\ ['<Leader>wc', 'nx'], ['<C-Down>', 'n'], ['<C-Up>', 'n'],
|
||||
\ ['<2-LeftMouse>', 'n'], ['<S-2-LeftMouse>', 'n'], ['<C-2-LeftMouse>', 'n'],
|
||||
\ ['<MiddleMouse>', 'n'], ['<RightMouse>', 'n'],
|
||||
\ ['ah', 'ox'], ['ih', 'ox'], ['aH', 'ox'], ['iH', 'ox'],
|
||||
\ ['al', 'ox'], ['il', 'ox'], ['a\', 'ox'], ['i\', 'ox'],
|
||||
\ ['ac', 'ox'], ['ic', 'ox'],
|
||||
\ ['<CR>', 'i'], ['<Tab>', 'i'], ['<S-Tab>', 'i'],
|
||||
\ ['<C-D>', 'i'], ['<C-T>', 'i'],
|
||||
\ ['<C-L><C-J>', 'i'], ['<C-L><C-K>', 'i'], ['<C-L><C-M>', 'i'],
|
||||
\ ]
|
||||
for s:entry in s:mapping_surface
|
||||
for s:m in split(s:entry[1], '\zs')
|
||||
let s:ok = s:has_map(s:entry[0], s:m)
|
||||
call s:record(s:ok ? 1 : 0, 'map[' . s:m . '].' . s:entry[0],
|
||||
\ s:ok ? '' : s:entry[0] . ' (' . s:m . ') not mapped buffer-local')
|
||||
endfor
|
||||
endfor
|
||||
|
||||
" ===== Header nav (pure VimL) =====
|
||||
|
||||
call s:run('headers.]]_jumps_to_next', {
|
||||
@@ -121,6 +222,12 @@ call s:run('headers.]=_jumps_to_next_sibling', {
|
||||
\ 'keys': ']=',
|
||||
\ 'expect_cursor_line': 4,
|
||||
\ })
|
||||
call s:run('headers.[=_jumps_to_prev_sibling', {
|
||||
\ 'lines': ['= A =', '== child ==', 'body', '= B ='],
|
||||
\ 'cursor': [4, 1],
|
||||
\ 'keys': '[=',
|
||||
\ 'expect_cursor_line': 1,
|
||||
\ })
|
||||
|
||||
" ===== Link nav (pure VimL) =====
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- scripts/test-keymaps.lua — driven by scripts/test-keymaps.sh.
|
||||
-- development/tests/test-keymaps.lua — driven by development/tests/test-keymaps.sh.
|
||||
--
|
||||
-- Headless Neovim harness for the buffer-local keymaps registered by
|
||||
-- `ftplugin/vimwiki.vim` + `lua/nuwiki/keymaps.lua`. Each case sets up
|
||||
@@ -123,6 +123,105 @@ vim.defer_fn(function()
|
||||
end
|
||||
record(true, 'lsp.attached', 'client=' .. client.name)
|
||||
|
||||
-- ===== Documented command surface =====
|
||||
-- Every command must be reachable in BOTH the canonical :Nuwiki* form
|
||||
-- and the vimwiki-compatible :Vimwiki* form. The suffixes below are the
|
||||
-- command list minus the prefix. :NuwikiInstall is the one exception — a
|
||||
-- nuwiki-only maintenance command with no :Vimwiki* alias.
|
||||
local both_forms = {
|
||||
'Index', 'TabIndex', 'UISelect', 'Goto', 'FollowLink', 'Backlinks',
|
||||
'NextLink', 'PrevLink', 'BaddLink', 'RenameFile', 'DeleteFile',
|
||||
'MakeDiaryNote', 'MakeYesterdayDiaryNote', 'MakeTomorrowDiaryNote',
|
||||
'DiaryIndex', 'DiaryGenerateLinks', 'DiaryNextDay', 'DiaryPrevDay',
|
||||
'TOC', 'GenerateLinks', 'CheckLinks', 'SearchTags', 'GenerateTagLinks',
|
||||
'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||
}
|
||||
for _, suffix in ipairs(both_forms) do
|
||||
for _, prefix in ipairs({ 'Nuwiki', 'Vimwiki' }) do
|
||||
local cmd = prefix .. suffix
|
||||
local exists = vim.fn.exists(':' .. cmd) == 2
|
||||
record(exists, 'surface.' .. cmd, exists and '' or (':' .. cmd .. ' not registered'))
|
||||
end
|
||||
end
|
||||
do
|
||||
local exists = vim.fn.exists(':NuwikiInstall') == 2
|
||||
record(exists, 'surface.NuwikiInstall',
|
||||
exists and '' or ':NuwikiInstall not registered')
|
||||
end
|
||||
|
||||
-- ===== Pure-Lua command invocation (cursor movement) =====
|
||||
-- :NuwikiNextLink / :NuwikiPrevLink wrap the link-search motion and run
|
||||
-- without the LSP, so we can assert exact cursor movement here.
|
||||
do
|
||||
local ok, err = pcall(function()
|
||||
set_buf({ 'intro [[A]] more [[B]] end' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.cmd('NuwikiNextLink')
|
||||
local c1 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c1 ~= 6 then error('NextLink #1 col=' .. c1 .. ' want 6') end
|
||||
vim.cmd('NuwikiNextLink')
|
||||
local c2 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c2 ~= 17 then error('NextLink #2 col=' .. c2 .. ' want 17') end
|
||||
vim.cmd('VimwikiPrevLink')
|
||||
local c3 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c3 ~= 6 then error('PrevLink col=' .. c3 .. ' want 6') end
|
||||
end)
|
||||
record(ok, 'invoke.next_prev_link_cursor_movement', ok and '' or tostring(err))
|
||||
end
|
||||
|
||||
-- ===== Documented mapping surface =====
|
||||
-- Every documented mapping must resolve to a buffer-local mapping in
|
||||
-- the mode(s) the doc lists it under. `modes` is a string of mode
|
||||
-- letters: n/x/o/i. The mouse group is opt-in — the harness enables it
|
||||
-- via setup({ mappings = { mouse = true } }).
|
||||
local function has_map(lhs, mode)
|
||||
local d = vim.fn.maparg(lhs, mode, false, true)
|
||||
return next(d) ~= nil and d.buffer == 1
|
||||
end
|
||||
local mapping_surface = {
|
||||
-- Links
|
||||
{ '<CR>', 'n' }, { '<S-CR>', 'n' }, { '<C-CR>', 'n' }, { '<C-S-CR>', 'n' },
|
||||
{ '<BS>', 'n' }, { '<Tab>', 'n' }, { '<S-Tab>', 'n' }, { '+', 'nx' },
|
||||
-- Lists
|
||||
{ '<C-Space>', 'nx' }, { '<C-@>', 'nx' }, { '<Nul>', 'nx' },
|
||||
{ 'gnt', 'n' }, { 'gln', 'nx' }, { 'glp', 'nx' }, { 'glx', 'nx' },
|
||||
{ 'glh', 'n' }, { 'gll', 'n' }, { 'gLh', 'n' }, { 'gLl', 'n' },
|
||||
{ 'glr', 'n' }, { 'gLr', 'n' }, { 'gl<Space>', 'n' }, { 'gL<Space>', 'n' },
|
||||
{ 'o', 'n' }, { 'O', 'n' },
|
||||
-- Headers
|
||||
{ '=', 'n' }, { '-', 'n' }, { ']]', 'n' }, { '[[', 'n' },
|
||||
{ ']=', 'n' }, { '[=', 'n' }, { ']u', 'n' }, { '[u', 'n' },
|
||||
-- Tables
|
||||
{ 'gqq', 'n' }, { 'gq1', 'n' }, { 'gww', 'n' }, { 'gw1', 'n' },
|
||||
{ '<A-Left>', 'n' }, { '<A-Right>', 'n' },
|
||||
-- Wiki / diary / export
|
||||
{ '<Leader>ww', 'n' }, { '<Leader>ws', 'n' }, { '<Leader>wi', 'n' },
|
||||
{ '<Leader>w<Leader>w', 'n' }, { '<Leader>w<Leader>y', 'n' },
|
||||
{ '<Leader>w<Leader>t', 'n' }, { '<Leader>w<Leader>i', 'n' },
|
||||
{ '<Leader>wn', 'n' }, { '<Leader>wd', 'n' }, { '<Leader>wr', 'n' },
|
||||
{ '<Leader>wh', 'n' }, { '<Leader>whh', 'n' }, { '<Leader>wha', 'n' },
|
||||
{ '<Leader>wc', 'nx' }, { '<C-Down>', 'n' }, { '<C-Up>', 'n' },
|
||||
-- Mouse (opt-in)
|
||||
{ '<2-LeftMouse>', 'n' }, { '<S-2-LeftMouse>', 'n' }, { '<C-2-LeftMouse>', 'n' },
|
||||
{ '<MiddleMouse>', 'n' }, { '<RightMouse>', 'n' },
|
||||
-- Text objects (operator-pending + visual)
|
||||
{ 'ah', 'ox' }, { 'ih', 'ox' }, { 'aH', 'ox' }, { 'iH', 'ox' },
|
||||
{ 'al', 'ox' }, { 'il', 'ox' }, { 'a\\', 'ox' }, { 'i\\', 'ox' },
|
||||
{ 'ac', 'ox' }, { 'ic', 'ox' },
|
||||
-- Insert-mode
|
||||
{ '<CR>', 'i' }, { '<Tab>', 'i' }, { '<S-Tab>', 'i' },
|
||||
{ '<C-D>', 'i' }, { '<C-T>', 'i' },
|
||||
{ '<C-L><C-J>', 'i' }, { '<C-L><C-K>', 'i' }, { '<C-L><C-M>', 'i' },
|
||||
}
|
||||
for _, entry in ipairs(mapping_surface) do
|
||||
local lhs, modes = entry[1], entry[2]
|
||||
for m in modes:gmatch('.') do
|
||||
local ok = has_map(lhs, m)
|
||||
record(ok, 'map[' .. m .. '].' .. lhs,
|
||||
ok and '' or (lhs .. ' (' .. m .. ') not mapped buffer-local'))
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== Lists =====
|
||||
|
||||
run('list.toggle_via_C-Space', {
|
||||
@@ -167,6 +266,84 @@ vim.defer_fn(function()
|
||||
expect_line = '- [ ] done',
|
||||
expect_at = 1,
|
||||
})
|
||||
-- `glp` must cycle BACKWARD (regression for the glp==gln bug). From
|
||||
-- `[.]` the backward step lands on `[ ]`, whereas `gln` would advance
|
||||
-- to `[o]`.
|
||||
run('list.cycle_back_via_glp', {
|
||||
lines = { '- [.] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glp',
|
||||
expect_line = '- [ ] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.cycle_back_via_glp_wraps', {
|
||||
-- `[ ]` backward wraps to the top of the progression (`[X]`).
|
||||
lines = { '- [ ] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glp',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Change level (glh/gll) =====
|
||||
|
||||
run('list.indent_via_gll', {
|
||||
lines = { '- item' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gll',
|
||||
expect_line = ' - item',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.dedent_via_glh', {
|
||||
lines = { ' - item' },
|
||||
cursor = { 1, 2 },
|
||||
keys = 'glh',
|
||||
expect_line = '- item',
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Renumber (glr / gLr) =====
|
||||
|
||||
run('list.renumber_via_glr', {
|
||||
lines = { '5. one', '9. two', '2. three' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glr',
|
||||
expect_lines = { '1. one', '2. two', '3. three' },
|
||||
})
|
||||
|
||||
-- ===== Remove done (gl<Space> current list / gL<Space> whole doc) =====
|
||||
|
||||
-- `gl<Space>` removes done items from the cursor's list only; the
|
||||
-- second list's done item survives.
|
||||
run('list.remove_done_current_list_via_gl_space', {
|
||||
lines = {
|
||||
'- [X] done a', '- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gl<Space>',
|
||||
expect_lines = {
|
||||
'- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
})
|
||||
-- `gL<Space>` sweeps the whole buffer regardless of cursor position.
|
||||
run('list.remove_done_whole_buffer_via_gL_space', {
|
||||
lines = {
|
||||
'- [X] done a', '- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gL<Space>',
|
||||
expect_lines = {
|
||||
'- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b',
|
||||
},
|
||||
})
|
||||
|
||||
-- ===== Next task (gnt) =====
|
||||
|
||||
@@ -247,7 +424,7 @@ vim.defer_fn(function()
|
||||
wait_ms = 200,
|
||||
})
|
||||
|
||||
-- ===== Link helpers (§13.1 Cluster C) =====
|
||||
-- ===== Link helpers (Cluster C) =====
|
||||
|
||||
run('links.normalize_via_+', {
|
||||
lines = { 'MyPage rest' },
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# scripts/test-keymaps.sh — drive scripts/test-keymaps.lua under
|
||||
# development/tests/test-keymaps.sh — drive development/tests/test-keymaps.lua under
|
||||
# headless Neovim and report pass/fail per buffer-local keymap.
|
||||
#
|
||||
# The Lua harness needs a real LSP attachment (most keymaps dispatch
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TMP="$(mktemp -d -t nuwiki-keymap-test-XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
@@ -38,7 +38,9 @@ cat > "$INIT" <<EOF
|
||||
-- Keep the harness self-contained; no plugin manager required.
|
||||
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||
vim.g.nuwiki_binary_path = '$BIN'
|
||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki' })
|
||||
-- Enable the opt-in mouse mappings so the harness can assert the full
|
||||
-- documented mapping surface (doc §6 mouse group).
|
||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki', mappings = { mouse = true } })
|
||||
EOF
|
||||
|
||||
RESULTS="$TMP/results.txt"
|
||||
@@ -51,7 +53,7 @@ log "running harness…"
|
||||
# stalling CI runners indefinitely.
|
||||
NUWIKI_KEYMAP_RESULTS="$RESULTS" \
|
||||
timeout 60 nvim --clean -u "$INIT" --headless "$TMP/wiki/index.wiki" \
|
||||
-c "luafile $REPO_ROOT/scripts/test-keymaps.lua" \
|
||||
-c "luafile $REPO_ROOT/development/tests/test-keymaps.lua" \
|
||||
>"$TMP/nvim.log" 2>&1 || true
|
||||
|
||||
if [[ ! -f "$RESULTS" ]]; then
|
||||
+70
-65
@@ -228,63 +228,68 @@ Vim-specific globals ~
|
||||
Override the auto-discovered server binary path. If set and the
|
||||
file is readable, it overrides the `{plugin}/bin/nuwiki-ls` default.
|
||||
|
||||
==============================================================================
|
||||
==============================================================================
|
||||
5. COMMANDS *nuwiki-commands*
|
||||
|
||||
Every command listed below is registered buffer-local on `.wiki` files
|
||||
under both names: the `:Vimwiki*` form (for migration from vimwiki) and
|
||||
the canonical `:Nuwiki*` alias. Only the `:Vimwiki*` form is shown.
|
||||
All commands are available in both forms:
|
||||
- Canonical form: :Nuwiki*
|
||||
- Vimwiki-compatible form: :Vimwiki*
|
||||
|
||||
Every command listed below is registered buffer-logic on `.wiki` files.
|
||||
Only the `:Nuwiki*` form is shown in this document for brevity.
|
||||
|
||||
|
||||
Wiki / navigation ~
|
||||
|
||||
*:VimwikiIndex*
|
||||
:VimwikiIndex [{count}]
|
||||
*:NuwikiIndex*
|
||||
:NuwikiIndex [{count}]
|
||||
Open wiki N's index page (default: 1).
|
||||
|
||||
*:VimwikiTabIndex*
|
||||
:VimwikiTabIndex [{count}]
|
||||
*:NuwikiTabIndex*
|
||||
:NuwikiTabIndex [{count}]
|
||||
Same, in a new tab.
|
||||
|
||||
*:VimwikiUISelect*
|
||||
:VimwikiUISelect
|
||||
*:NuwikiUISelect*
|
||||
:NuwikiUISelect
|
||||
Pick a wiki from a list (multi-wiki only). Uses |vim.ui.select| on
|
||||
Neovim, |inputlist()| on Vim.
|
||||
|
||||
*:VimwikiGoto*
|
||||
:VimwikiGoto {page}
|
||||
*:NuwikiGoto*
|
||||
:NuwikiGoto {page}
|
||||
Open `{page}.wiki` by name.
|
||||
|
||||
*:VimwikiFollowLink*
|
||||
:VimwikiFollowLink
|
||||
*:NuwikiFollowLink*
|
||||
:NuwikiFollowLink
|
||||
Follow the link under the cursor.
|
||||
|
||||
*:VimwikiBacklinks*
|
||||
:VimwikiBacklinks
|
||||
*:NuwikiBacklinks*
|
||||
:NuwikiBacklinks
|
||||
Show all references to the current page.
|
||||
|
||||
*:VimwikiNextLink*
|
||||
:VimwikiNextLink
|
||||
*:VimwikiPrevLink*
|
||||
:VimwikiPrevLink
|
||||
*:NuwikiNextLink*
|
||||
:NuwikiNextLink
|
||||
*:NuwikiPrevLink*
|
||||
:NuwikiPrevLink
|
||||
Jump to the next / previous wikilink on the page.
|
||||
|
||||
*:VimwikiBaddLink*
|
||||
:VimwikiBaddLink
|
||||
*:NuwikiBaddLink*
|
||||
:NuwikiBaddLink
|
||||
Add the target of the link under the cursor to the buffer list.
|
||||
|
||||
*:VimwikiRenameFile*
|
||||
:VimwikiRenameFile
|
||||
*:NuwikiRenameFile*
|
||||
:NuwikiRenameFile
|
||||
Prompt for a new name; rename the current page and rewrite every
|
||||
inbound link.
|
||||
|
||||
*:VimwikiDeleteFile*
|
||||
:VimwikiDeleteFile
|
||||
*:NuwikiDeleteFile*
|
||||
:NuwikiDeleteFile
|
||||
Delete the current page and its on-disk file.
|
||||
|
||||
Diary ~
|
||||
|
||||
*:VimwikiMakeDiaryNote*
|
||||
:VimwikiMakeDiaryNote
|
||||
*:NuwikiMakeDiaryNote*
|
||||
:NuwikiMakeDiaryNote
|
||||
Open today's diary entry. With `diary_frequency = 'weekly'` (or
|
||||
`'monthly'` / `'yearly'`) this addresses this week's / this month's
|
||||
/ this year's entry instead. The file stems follow:
|
||||
@@ -294,74 +299,74 @@ Diary ~
|
||||
monthly YYYY-MM 2026-05.wiki
|
||||
yearly YYYY 2026.wiki
|
||||
|
||||
*:VimwikiMakeYesterdayDiaryNote*
|
||||
:VimwikiMakeYesterdayDiaryNote
|
||||
*:VimwikiMakeTomorrowDiaryNote*
|
||||
:VimwikiMakeTomorrowDiaryNote
|
||||
*:NuwikiMakeYesterdayDiaryNote*
|
||||
:NuwikiMakeYesterdayDiaryNote
|
||||
*:NuwikiMakeTomorrowDiaryNote*
|
||||
:NuwikiMakeTomorrowDiaryNote
|
||||
Step the diary back / forward by one period at the configured
|
||||
cadence.
|
||||
|
||||
*:VimwikiDiaryIndex*
|
||||
:VimwikiDiaryIndex
|
||||
*:NuwikiDiaryIndex*
|
||||
:NuwikiDiaryIndex
|
||||
Open the diary index page.
|
||||
|
||||
*:VimwikiDiaryGenerateLinks*
|
||||
:VimwikiDiaryGenerateLinks
|
||||
*:NuwikiDiaryGenerateLinks*
|
||||
:NuwikiDiaryGenerateLinks
|
||||
Rebuild the diary index page from the entries currently on disk.
|
||||
|
||||
*:VimwikiDiaryNextDay*
|
||||
:VimwikiDiaryNextDay
|
||||
*:VimwikiDiaryPrevDay*
|
||||
:VimwikiDiaryPrevDay
|
||||
*:NuwikiDiaryNextDay*
|
||||
:NuwikiDiaryNextDay
|
||||
*:NuwikiDiaryPrevDay*
|
||||
:NuwikiDiaryPrevDay
|
||||
Walk to the next / previous indexed diary entry at the same cadence
|
||||
as the current one.
|
||||
|
||||
Page generation ~
|
||||
|
||||
*:VimwikiTOC*
|
||||
:VimwikiTOC
|
||||
*:NuwikiTOC*
|
||||
:NuwikiTOC
|
||||
Generate or refresh the table of contents on the current page.
|
||||
|
||||
*:VimwikiGenerateLinks*
|
||||
:VimwikiGenerateLinks
|
||||
*:NuwikiGenerateLinks*
|
||||
:NuwikiGenerateLinks
|
||||
Insert a flat list of every page in the wiki under the cursor.
|
||||
|
||||
*:VimwikiCheckLinks*
|
||||
:VimwikiCheckLinks
|
||||
*:NuwikiCheckLinks*
|
||||
:NuwikiCheckLinks
|
||||
Send every broken link in the workspace to the |quickfix| list.
|
||||
|
||||
Tags ~
|
||||
|
||||
*:VimwikiSearchTags*
|
||||
:VimwikiSearchTags {tag}
|
||||
*:NuwikiSearchTags*
|
||||
:NuwikiSearchTags {tag}
|
||||
Quickfix listing every `:tag:` occurrence.
|
||||
|
||||
*:VimwikiGenerateTagLinks*
|
||||
:VimwikiGenerateTagLinks [tag]
|
||||
*:NuwikiGenerateTagLinks*
|
||||
:NuwikiGenerateTagLinks [tag]
|
||||
Insert a section linking every page that has `<tag>`. With no
|
||||
argument, generates a section per tag found in the workspace.
|
||||
|
||||
*:VimwikiRebuildTags*
|
||||
:VimwikiRebuildTags
|
||||
*:NuwikiRebuildTags*
|
||||
:NuwikiRebuildTags
|
||||
Force a full workspace re-index.
|
||||
|
||||
HTML export ~
|
||||
|
||||
*:Vimwiki2HTML*
|
||||
:Vimwiki2HTML
|
||||
*:Nuwiki2HTML*
|
||||
:Nuwiki2HTML
|
||||
Render the current page to HTML.
|
||||
|
||||
*:Vimwiki2HTMLBrowse*
|
||||
:Vimwiki2HTMLBrowse
|
||||
*:Nuwiki2HTMLBrowse*
|
||||
:Nuwiki2HTMLBrowse
|
||||
Render the current page and open it in the default browser.
|
||||
|
||||
*:VimwikiAll2HTML*
|
||||
:VimwikiAll2HTML[!]
|
||||
*:NuwikiAll2HTML*
|
||||
:NuwikiAll2HTML[!]
|
||||
Export every page in the wiki. `!` forces a full rebuild; otherwise
|
||||
only out-of-date pages are re-rendered.
|
||||
|
||||
*:VimwikiRss*
|
||||
:VimwikiRss
|
||||
*:NuwikiRss*
|
||||
:NuwikiRss
|
||||
Write `rss.xml` summarising recent diary entries.
|
||||
|
||||
Other ~
|
||||
@@ -522,7 +527,7 @@ The `diary_frequency` per-wiki option controls the cadence:
|
||||
monthly YYYY-MM e.g. `2026-05.wiki`
|
||||
yearly YYYY e.g. `2026.wiki`
|
||||
|
||||
`:VimwikiMakeDiaryNote` opens the entry for the current period at the
|
||||
`:NuwikiMakeDiaryNote` opens the entry for the current period at the
|
||||
configured cadence; the yesterday / tomorrow commands step back and
|
||||
forward by one period. `<C-Down>` / `<C-Up>` walk between indexed
|
||||
entries of the same flavour as the one under the cursor — so on a
|
||||
@@ -530,7 +535,7 @@ weekly page, navigation stays weekly.
|
||||
|
||||
Index ~
|
||||
|
||||
`:VimwikiDiaryGenerateLinks` rebuilds the diary index page (`diary.wiki`
|
||||
`:NuwikiDiaryGenerateLinks` rebuilds the diary index page (`diary.wiki`
|
||||
by default) with a chronological list of every entry on disk, grouped
|
||||
by year / month. The page name is set by `diary_index`; the heading is
|
||||
set by `diary_header`.
|
||||
@@ -564,8 +569,8 @@ column moves, cell navigation, new-row insertion).
|
||||
==============================================================================
|
||||
11. HTML EXPORT *nuwiki-html*
|
||||
|
||||
`:Vimwiki2HTML` renders the current page; `:Vimwiki2HTMLBrowse` opens
|
||||
the result in the default browser; `:VimwikiAll2HTML` exports every
|
||||
`:Nuwiki2HTML` renders the current page; `:Nuwiki2HTMLBrowse` opens
|
||||
the result in the default browser; `:NuwikiAll2HTML` exports every
|
||||
page (incremental by default; `!` forces a full rebuild).
|
||||
|
||||
Per-wiki knobs:
|
||||
@@ -579,7 +584,7 @@ Per-wiki knobs:
|
||||
`exclude_files` Glob patterns excluded from export.
|
||||
|
||||
Templates may include `{title}`, `{rootpath}`, `{content}`, `{css}`,
|
||||
`{date}` placeholders. RSS for the diary is written by `:VimwikiRss`.
|
||||
`{date}` placeholders. RSS for the diary is written by `:NuwikiRss`.
|
||||
|
||||
==============================================================================
|
||||
12. FOLDING *nuwiki-folding*
|
||||
@@ -631,7 +636,7 @@ match vimwiki. To migrate:
|
||||
|
||||
Existing pages, diary entries, tags, and templates work without
|
||||
modification. The first workspace scan after launch may show an empty
|
||||
`:VimwikiCheckLinks` result until the background index completes;
|
||||
`:NuwikiCheckLinks` result until the background index completes;
|
||||
subsequent runs are instant.
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
|
||||
+38
-9
@@ -1,9 +1,9 @@
|
||||
" ftplugin/nuwiki.vim — per-buffer settings + Phase 19 command/keymap glue.
|
||||
" ftplugin/nuwiki.vim — per-buffer settings + command/keymap glue.
|
||||
"
|
||||
" Wires up:
|
||||
" - basic edit-time options (commentstring, suffixesadd, …)
|
||||
" - every `:Vimwiki*` / `:Nuwiki*` command from SPEC §12.10 that has a
|
||||
" server-side counterpart (deferred §13.1 commands stub out with a
|
||||
" - every `:Vimwiki*` / `:Nuwiki*` command that has a
|
||||
" server-side counterpart (deferred commands stub out with a
|
||||
" "not yet implemented" notification)
|
||||
" - on Neovim: keymaps + text objects + folding via
|
||||
" `nuwiki.ftplugin.attach()`
|
||||
@@ -78,7 +78,7 @@ if !has('nvim')
|
||||
command! -buffer -nargs=? VimwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
|
||||
command! -buffer -nargs=? VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
||||
|
||||
" §13.1 deferred stubs.
|
||||
" Deferred stubs.
|
||||
command! -buffer -nargs=1 VimwikiTable call nuwiki#commands#table_insert()
|
||||
command! -buffer VimwikiTableMoveColumnLeft call nuwiki#commands#table_move_left()
|
||||
command! -buffer VimwikiTableMoveColumnRight call nuwiki#commands#table_move_right()
|
||||
@@ -115,6 +115,21 @@ if !has('nvim')
|
||||
command! -buffer -nargs=? NuwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
||||
command! -buffer -nargs=1 NuwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
|
||||
|
||||
" Canonical :Nuwiki* names that mirror the documented surface.
|
||||
" The older :Nuwiki* spellings above stay defined for back-compat.
|
||||
command! -buffer NuwikiNextLink call nuwiki#commands#link_next()
|
||||
command! -buffer NuwikiPrevLink call nuwiki#commands#link_prev()
|
||||
command! -buffer NuwikiBaddLink call nuwiki#commands#badd_link()
|
||||
command! -buffer NuwikiMakeDiaryNote call nuwiki#commands#diary_today()
|
||||
command! -buffer NuwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday()
|
||||
command! -buffer NuwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow()
|
||||
command! -buffer NuwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
|
||||
command! -buffer NuwikiDiaryNextDay call nuwiki#commands#diary_next()
|
||||
command! -buffer NuwikiDiaryPrevDay call nuwiki#commands#diary_prev()
|
||||
command! -buffer Nuwiki2HTML call nuwiki#commands#export_current()
|
||||
command! -buffer Nuwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
||||
command! -buffer -bang NuwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
||||
|
||||
" ===== Default key mappings (parity with upstream vimwiki) =====
|
||||
"
|
||||
" Users can opt out by setting `g:nuwiki_no_default_mappings = 1`
|
||||
@@ -162,8 +177,8 @@ if !has('nvim')
|
||||
nnoremap <silent><buffer> gnt :call nuwiki#commands#next_task()<CR>
|
||||
nnoremap <silent><buffer> gln :call nuwiki#commands#cycle_list_item()<CR>
|
||||
xnoremap <silent><buffer> gln :<C-u>call nuwiki#commands#cycle_list_item()<CR>
|
||||
nnoremap <silent><buffer> glp :call nuwiki#commands#cycle_list_item()<CR>
|
||||
xnoremap <silent><buffer> glp :<C-u>call nuwiki#commands#cycle_list_item()<CR>
|
||||
nnoremap <silent><buffer> glp :call nuwiki#commands#cycle_list_item_back()<CR>
|
||||
xnoremap <silent><buffer> glp :<C-u>call nuwiki#commands#cycle_list_item_back()<CR>
|
||||
nnoremap <silent><buffer> glx :call nuwiki#commands#reject_list_item()<CR>
|
||||
xnoremap <silent><buffer> glx :<C-u>call nuwiki#commands#reject_list_item()<CR>
|
||||
nnoremap <silent><buffer> glh :call nuwiki#commands#list_change_level(-1, 0)<CR>
|
||||
@@ -173,7 +188,7 @@ if !has('nvim')
|
||||
nnoremap <silent><buffer> glr :call nuwiki#commands#list_renumber()<CR>
|
||||
nnoremap <silent><buffer> gLr :call nuwiki#commands#list_renumber_all()<CR>
|
||||
nnoremap <silent><buffer> gl<Space> :call nuwiki#commands#list_remove_done()<CR>
|
||||
nnoremap <silent><buffer> gL<Space> :call nuwiki#commands#list_remove_done()<CR>
|
||||
nnoremap <silent><buffer> gL<Space> :call nuwiki#commands#list_remove_done_all()<CR>
|
||||
nnoremap <silent><buffer> o :call nuwiki#commands#open_below_with_bullet()<CR>
|
||||
nnoremap <silent><buffer> O :call nuwiki#commands#open_above_with_bullet()<CR>
|
||||
|
||||
@@ -316,7 +331,7 @@ command! -buffer VimwikiRebuildTags lua require('nuwiki.commands').
|
||||
command! -buffer -nargs=? VimwikiSearchTags lua require('nuwiki.commands').tags_search(<q-args>)
|
||||
command! -buffer -nargs=? VimwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||
|
||||
" §13.1 deferred — stubs notify the user instead of erroring.
|
||||
" Deferred — stubs notify the user instead of erroring.
|
||||
command! -buffer -nargs=1 VimwikiTable lua require('nuwiki.commands').table_insert()
|
||||
command! -buffer VimwikiTableMoveColumnLeft lua require('nuwiki.commands').table_move_column_left()
|
||||
command! -buffer VimwikiTableMoveColumnRight lua require('nuwiki.commands').table_move_column_right()
|
||||
@@ -355,5 +370,19 @@ command! -buffer -nargs=? NuwikiSearchTags lua require('nuwiki.commands'
|
||||
command! -buffer -nargs=? NuwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||
command! -buffer -nargs=1 NuwikiGoto lua require('nuwiki.commands').wiki_goto_page(<q-args>)
|
||||
|
||||
" Phase 19 buffer attach: keymaps + text objects + folding.
|
||||
" Canonical :Nuwiki* names that mirror the documented surface.
|
||||
" The older :Nuwiki* spellings above stay defined for back-compat.
|
||||
command! -buffer NuwikiNextLink lua require('nuwiki.commands').link_next()
|
||||
command! -buffer NuwikiPrevLink lua require('nuwiki.commands').link_prev()
|
||||
command! -buffer NuwikiBaddLink lua require('nuwiki.commands').badd_link()
|
||||
command! -buffer NuwikiMakeDiaryNote lua require('nuwiki.commands').diary_today()
|
||||
command! -buffer NuwikiMakeYesterdayDiaryNote lua require('nuwiki.commands').diary_yesterday()
|
||||
command! -buffer NuwikiMakeTomorrowDiaryNote lua require('nuwiki.commands').diary_tomorrow()
|
||||
command! -buffer NuwikiDiaryNextDay lua require('nuwiki.commands').diary_next()
|
||||
command! -buffer NuwikiDiaryPrevDay lua require('nuwiki.commands').diary_prev()
|
||||
command! -buffer Nuwiki2HTML lua require('nuwiki.commands').export_current()
|
||||
command! -buffer Nuwiki2HTMLBrowse lua require('nuwiki.commands').export_browse()
|
||||
command! -buffer -bang NuwikiAll2HTML execute (<bang>0 ? "lua require('nuwiki.commands').export_all_force()" : "lua require('nuwiki.commands').export_all()")
|
||||
|
||||
" Buffer attach: keymaps + text objects + folding.
|
||||
lua require('nuwiki.ftplugin').attach(0)
|
||||
|
||||
+19
-6
@@ -1,5 +1,5 @@
|
||||
-- lua/nuwiki/commands.lua — Lua wrappers around every `nuwiki.*`
|
||||
-- `workspace/executeCommand` the server advertises. Phase 19 plumbing.
|
||||
-- `workspace/executeCommand` the server advertises.
|
||||
--
|
||||
-- Each public function is named after its `:Vimwiki*` / `:Nuwiki*`
|
||||
-- counterpart so `ftplugin/nuwiki.vim` can wire them up declaratively.
|
||||
@@ -323,6 +323,12 @@ end
|
||||
|
||||
M.toggle_list_item = _exec_pos('nuwiki.list.toggleCheckbox')
|
||||
M.cycle_list_item = _exec_pos('nuwiki.list.cycleCheckbox')
|
||||
-- `glp` cycles backward — same command, `reverse = true` in the payload.
|
||||
function M.cycle_list_item_back()
|
||||
local a = pos_args()
|
||||
a[1].reverse = true
|
||||
exec('nuwiki.list.cycleCheckbox', a)
|
||||
end
|
||||
M.reject_list_item = _exec_pos('nuwiki.list.rejectCheckbox')
|
||||
M.heading_add_level = _exec_pos('nuwiki.heading.addLevel')
|
||||
M.heading_remove_level = _exec_pos('nuwiki.heading.removeLevel')
|
||||
@@ -462,7 +468,7 @@ function M.rename_file()
|
||||
vim.lsp.buf.rename()
|
||||
end
|
||||
|
||||
-- ===== Deferred placeholders (§13.1) =====
|
||||
-- ===== Deferred placeholders =====
|
||||
--
|
||||
-- These commands aren't implemented on the server yet. Stub them so the
|
||||
-- `:Vimwiki*` compat surface exists and users get a clear message
|
||||
@@ -471,15 +477,22 @@ end
|
||||
local function _not_yet(name)
|
||||
return function()
|
||||
vim.notify(
|
||||
'nuwiki: ' .. name .. ' is not yet implemented — see SPEC §13.1',
|
||||
'nuwiki: ' .. name .. ' is not yet implemented',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- §13.1 Cluster A — list rewriters.
|
||||
-- Cluster A — list rewriters.
|
||||
|
||||
-- `gl<Space>` — remove done items from the current list only. Passing the
|
||||
-- cursor position scopes the server to the contiguous list block under it.
|
||||
function M.list_remove_done()
|
||||
exec('nuwiki.list.removeDone', pos_args())
|
||||
end
|
||||
|
||||
-- `gL<Space>` — remove done items across the whole buffer (no position).
|
||||
function M.list_remove_done_all()
|
||||
exec('nuwiki.list.removeDone', uri_args())
|
||||
end
|
||||
|
||||
@@ -878,7 +891,7 @@ function M.smart_shift_tab()
|
||||
)
|
||||
end
|
||||
|
||||
-- §13.1 Cluster B — table rewriters.
|
||||
-- Cluster B — table rewriters.
|
||||
|
||||
function M.table_insert(cols, rows)
|
||||
cols = tonumber(cols) or 3
|
||||
@@ -961,7 +974,7 @@ function M.colorize(color)
|
||||
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
||||
end
|
||||
|
||||
-- §13.1 Cluster C — link helpers.
|
||||
-- Cluster C — link helpers.
|
||||
|
||||
function M.paste_link()
|
||||
exec('nuwiki.link.pasteWikilink', pos_args())
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
-- lua/nuwiki/config.lua — defaults and user-config merge.
|
||||
--
|
||||
-- Schema mirrors SPEC §7.5 + §12.11. Keep this aligned with
|
||||
-- `doc/nuwiki.txt`.
|
||||
-- Keep this aligned with `doc/nuwiki.txt`.
|
||||
|
||||
local M = {}
|
||||
|
||||
M.defaults = {
|
||||
-- v1.0 single-wiki shorthand. The server desugars these into a
|
||||
-- Single-wiki shorthand. The server desugars these into a
|
||||
-- one-entry `wikis = {…}` list. Use either this or the multi-wiki
|
||||
-- form below; if both are set, `wikis` wins.
|
||||
wiki_root = '~/vimwiki',
|
||||
@@ -14,12 +13,12 @@ M.defaults = {
|
||||
syntax = 'vimwiki',
|
||||
log_level = 'warn',
|
||||
|
||||
-- v1.1 multi-wiki shape. Each entry honours every per-wiki key the
|
||||
-- Multi-wiki shape. Each entry honours every per-wiki key the
|
||||
-- server understands. Leave nil to fall through to the shorthand
|
||||
-- above.
|
||||
--
|
||||
-- Per-wiki keys (all optional, server defaults documented in SPEC
|
||||
-- §12.11):
|
||||
-- Per-wiki keys (all optional, server defaults documented in
|
||||
-- `doc/nuwiki.txt`):
|
||||
-- name, root, file_extension, syntax, index
|
||||
-- diary_rel_path, diary_index, diary_frequency,
|
||||
-- diary_start_week_day, diary_caption_level, diary_sort,
|
||||
@@ -31,7 +30,7 @@ M.defaults = {
|
||||
-- links_space_char, nested_syntaxes
|
||||
wikis = nil,
|
||||
|
||||
-- Phase 19: per-buffer glue. Each subgroup mirrors vimwiki's
|
||||
-- Per-buffer glue. Each subgroup mirrors vimwiki's
|
||||
-- `g:vimwiki_key_mappings` shape and can be flipped off
|
||||
-- independently to suppress that group of keymaps.
|
||||
mappings = {
|
||||
@@ -47,7 +46,7 @@ M.defaults = {
|
||||
mouse = false, -- <2-LeftMouse>, <RightMouse>, … (opt-in)
|
||||
},
|
||||
|
||||
-- Phase 19: folding. `'lsp'` uses the server's `foldingRange`
|
||||
-- Folding. `'lsp'` uses the server's `foldingRange`
|
||||
-- provider (Neovim 0.11+ wires it automatically). `'expr'` falls
|
||||
-- back to a regex-based `foldexpr`. `'off'` skips folding setup.
|
||||
folding = 'lsp',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- lua/nuwiki/ftplugin.lua — per-buffer hookup invoked by
|
||||
-- `ftplugin/nuwiki.vim`. Centralises the Lua side of Phase 19 so the
|
||||
-- VimL ftplugin stays a one-liner regardless of which subgroups are
|
||||
-- `ftplugin/nuwiki.vim`. Centralises the Lua side of the per-buffer
|
||||
-- glue so the VimL ftplugin stays a one-liner regardless of which subgroups are
|
||||
-- enabled.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- lua/nuwiki/health.lua — `:checkhealth nuwiki` target.
|
||||
--
|
||||
-- v1.0 checks per SPEC §7.6 (binary, version, filetype, LSP attach).
|
||||
-- v1.1 §12.10 additions: registered LSP commands, indexed wikis, HTML
|
||||
-- Core checks: binary, version, filetype, LSP attach.
|
||||
-- Additional checks: registered LSP commands, indexed wikis, HTML
|
||||
-- output path writability, default keymaps installed.
|
||||
|
||||
local M = {}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
-- require('nuwiki').install() — install / build the language server
|
||||
-- require('nuwiki').health() — :checkhealth nuwiki target
|
||||
--
|
||||
-- Per SPEC §6.2/§6.3 this layer is wiring only. Everything substantive
|
||||
-- This layer is wiring only. Everything substantive
|
||||
-- happens in the Rust language server.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- lua/nuwiki/install.lua — install / build the `nuwiki-ls` binary.
|
||||
--
|
||||
-- Strategy (matches SPEC §7.2):
|
||||
-- Strategy:
|
||||
-- 1. Look for `bin/nuwiki-ls[.exe]` next to the plugin.
|
||||
-- 2. If missing or `g:nuwiki_build_from_source` is set, build with cargo.
|
||||
-- 3. Otherwise try to download a release asset for the current target.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
-- wiki_prefix = true, -- <Leader>ww, <Leader>ws, <Leader>wi, …
|
||||
-- }
|
||||
--
|
||||
-- Mappings that point at §13.1-deferred commands stub out to a
|
||||
-- Mappings that point at deferred commands stub out to a
|
||||
-- "not yet implemented" notification — better than a silent no-op
|
||||
-- so users get parity *signalling*, not just parity in lhs.
|
||||
|
||||
@@ -133,12 +133,12 @@ local function open_above_with_bullet()
|
||||
vim.api.nvim_feedkeys('O' .. prefix, 'n', false)
|
||||
end
|
||||
|
||||
-- ===== Stub for deferred §13.1 commands =====
|
||||
-- ===== Stub for deferred commands =====
|
||||
|
||||
local function deferred(name)
|
||||
return function()
|
||||
vim.notify(
|
||||
'nuwiki: ' .. name .. ' is not yet implemented — see SPEC §13.1',
|
||||
'nuwiki: ' .. name .. ' is not yet implemented',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
@@ -218,8 +218,8 @@ function M.attach(bufnr, mappings)
|
||||
map('n', 'gnt', cmd.next_task, { desc = 'nuwiki: next unfinished task' }, bufnr)
|
||||
map('n', 'gln', cmd.cycle_list_item, { desc = 'nuwiki: increment checkbox' }, bufnr)
|
||||
map('x', 'gln', cmd.cycle_list_item, { desc = 'nuwiki: increment checkbox' }, bufnr)
|
||||
map('n', 'glp', cmd.cycle_list_item, { desc = 'nuwiki: decrement checkbox' }, bufnr)
|
||||
map('x', 'glp', cmd.cycle_list_item, { desc = 'nuwiki: decrement checkbox' }, bufnr)
|
||||
map('n', 'glp', cmd.cycle_list_item_back, { desc = 'nuwiki: decrement checkbox' }, bufnr)
|
||||
map('x', 'glp', cmd.cycle_list_item_back, { desc = 'nuwiki: decrement checkbox' }, bufnr)
|
||||
map('n', 'glx', cmd.reject_list_item, { desc = 'nuwiki: reject checkbox' }, bufnr)
|
||||
map('x', 'glx', cmd.reject_list_item, { desc = 'nuwiki: reject checkbox' }, bufnr)
|
||||
map('n', 'glh', function() cmd.list_change_level(-1, false) end,
|
||||
@@ -235,8 +235,8 @@ function M.attach(bufnr, mappings)
|
||||
map('n', 'gLr', cmd.list_renumber_all,
|
||||
{ desc = 'nuwiki: renumber all lists' }, bufnr)
|
||||
map('n', 'gl<Space>', cmd.list_remove_done,
|
||||
{ desc = 'nuwiki: remove done items' }, bufnr)
|
||||
map('n', 'gL<Space>', cmd.list_remove_done,
|
||||
{ desc = 'nuwiki: remove done items (current list)' }, bufnr)
|
||||
map('n', 'gL<Space>', cmd.list_remove_done_all,
|
||||
{ desc = 'nuwiki: remove done items (whole doc)' }, bufnr)
|
||||
map('n', 'o', open_below_with_bullet, { desc = 'nuwiki: open below + bullet' }, bufnr)
|
||||
map('n', 'O', open_above_with_bullet, { desc = 'nuwiki: open above + bullet' }, bufnr)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
" their config (lazy.nvim does this automatically through `opts`). On Vim
|
||||
" we start the LSP client when a vimwiki buffer is loaded.
|
||||
"
|
||||
" Per SPEC §6.2 / §6.3 this file contains wiring only — all logic lives in
|
||||
" This file contains wiring only — all logic lives in
|
||||
" the Rust language server.
|
||||
|
||||
if exists('g:loaded_nuwiki')
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
" syntax/vimwiki.vim — default highlight groups for nuwiki.
|
||||
"
|
||||
" Two responsibilities:
|
||||
" 1. Default `@vimwiki*` highlight groups for the LSP semantic tokens
|
||||
" (SPEC §11 P7). Loaded eagerly so the very first hover/highlight
|
||||
" 1. Default `@vimwiki*` highlight groups for the LSP semantic tokens.
|
||||
" Loaded eagerly so the very first hover/highlight
|
||||
" cycle after server startup already has colours.
|
||||
" 2. A small regex-based fallback so static highlighting works on
|
||||
" buffers where the LSP isn't running yet.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-personal-wiki.sh — open the user's real personal wiki via start-vim.sh.
|
||||
#
|
||||
# Reuses start-vim.sh's plumbing (binary build, vim-lsp clone, viminfo
|
||||
# under the dev cache) but points the wiki root at ~/.vimwiki/personal_wiki
|
||||
# and disables seed_wiki so we don't drop a scratch `Notes.wiki` into your
|
||||
# real notes.
|
||||
#
|
||||
# Useful when reproducing a user-reported bug against actual content rather
|
||||
# than the seeded smoke-test wiki. State (viminfo/sessions) still lives
|
||||
# under $XDG_CACHE_HOME/nuwiki-dev/ so your real Vim install stays untouched.
|
||||
#
|
||||
# Usage:
|
||||
# ./test-personal-wiki.sh # open index.wiki
|
||||
# ./test-personal-wiki.sh path/to/note.wiki # open a specific file
|
||||
# NUWIKI_PERSONAL_WIKI=/other/path ./test-personal-wiki.sh
|
||||
# NUWIKI_DEV_SKIP_BUILD=1 ./test-personal-wiki.sh # reuse the cached binary
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WIKI_DIR="${NUWIKI_PERSONAL_WIKI:-$HOME/.vimwiki/personal_wiki}"
|
||||
|
||||
if [[ ! -d "$WIKI_DIR" ]]; then
|
||||
echo "test-personal-wiki: $WIKI_DIR does not exist" >&2
|
||||
echo "set NUWIKI_PERSONAL_WIKI to point at your wiki, or create the default path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export NUWIKI_DEV_WIKI="$WIKI_DIR"
|
||||
export NUWIKI_DEV_NO_SEED=1
|
||||
|
||||
exec "$REPO_ROOT/start-vim.sh" "$@"
|
||||
Reference in New Issue
Block a user