Major clean up and improvements to vimwiki compatibility and documentation.
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m25s

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-30 18:35:40 +00:00
parent 95645a2b91
commit 8ab6015405
71 changed files with 2496 additions and 1914 deletions
+93 -29
View File
@@ -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 {
+17 -19
View File
@@ -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 1319
//! 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>,
+3 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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.
+10 -10
View File
@@ -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())
+26 -27
View File
@@ -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(&params.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 -1
View File
@@ -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:
+2 -2
View File
@@ -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.
+2 -3
View File
@@ -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:
//!
+4 -4
View File
@@ -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 1217 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};