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 {