feat(toc): insert a fresh TOC at the cursor line
CI / cargo fmt --check (push) Successful in 35s
CI / cargo clippy (push) Successful in 39s
CI / cargo test (push) Successful in 50s
CI / editor keymaps (push) Successful in 1m25s

:NuwikiTOC inserted a new table of contents at the top of the file. It now
inserts at the cursor line instead, so you can place the TOC where you want
it (a blank line separates it from preceding text). An existing TOC is
still refreshed in place, and auto_toc-on-save is unaffected.

The Vim/Lua clients send the 0-based cursor line with nuwiki.toc.generate;
the server threads it into toc_edit (clamped to the document) and falls
back to the top of the file when absent. toc_rebuild_edit passes None.

Tests: toc_edit_inserts_at_cursor_line +
toc_edit_cursor_line_clamped_and_existing_replaced_in_place. 575 passed,
clippy clean, keymap harnesses green. Doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 00:51:40 +00:00
parent 9d6d28a1b6
commit 94cb58064d
6 changed files with 94 additions and 17 deletions
+44 -10
View File
@@ -323,7 +323,17 @@ fn parse_optional_uri_arg(args: Vec<Value>) -> Result<OptionalUriArg, String> {
}
fn toc_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
let p = parse_uri_arg(args)?;
#[derive(Deserialize)]
struct TocArg {
uri: Url,
/// 0-based cursor line; a fresh TOC is inserted here.
#[serde(default)]
line: Option<u32>,
}
let p: TocArg = match args.into_iter().next() {
Some(v) => serde_json::from_value(v).map_err(|e| format!("invalid args: {e}"))?,
None => return Err("missing { uri } argument".to_string()),
};
let doc = match backend.documents.get(&p.uri) {
Some(d) => d,
None => return Ok(None),
@@ -348,6 +358,7 @@ fn toc_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceE
level,
margin,
link_format,
p.line,
))
}
@@ -2060,6 +2071,11 @@ pub mod ops {
/// Produce a `WorkspaceEdit` that replaces or inserts the TOC for the
/// given document. Returns `None` if the document has no headings at
/// all (other than possibly a pre-existing TOC heading).
///
/// When there's no existing TOC section, a fresh one is inserted at
/// `insert_line` (0-based) — the cursor line passed by the client — or at
/// the top of the file when `None`. An existing TOC is always replaced in
/// place, regardless of the cursor.
#[allow(clippy::too_many_arguments)]
pub fn toc_edit(
text: &str,
@@ -2070,6 +2086,7 @@ pub mod ops {
level: u8,
margin: usize,
link_format: u8,
insert_line: Option<u32>,
) -> Option<WorkspaceEdit> {
let items = collect_toc_items(ast, heading);
if items.is_empty() {
@@ -2087,16 +2104,21 @@ pub mod ops {
b.edit(uri.clone(), text_edit_replace(span, new_text, text, utf8));
}
None => {
let with_sep = format!("{new_text}\n");
// Insert at the cursor line (clamped to the document), or the
// top of the file when the client didn't send one.
let line = insert_line
.unwrap_or(0)
.min(text.matches('\n').count() as u32);
// A blank line keeps the TOC heading from abutting preceding
// text — not needed at the very top of the file.
let block = if line == 0 {
format!("{new_text}\n")
} else {
format!("\n{new_text}\n")
};
b.edit(
uri.clone(),
text_edit_insert(
LspPosition {
line: 0,
character: 0,
},
with_sep,
),
text_edit_insert(LspPosition { line, character: 0 }, block),
);
}
}
@@ -2119,7 +2141,19 @@ pub mod ops {
link_format: u8,
) -> Option<WorkspaceEdit> {
find_section_range(ast, heading)?;
toc_edit(text, ast, uri, utf8, heading, level, margin, link_format)
// Rebuild-on-save only ever replaces an existing TOC in place, so the
// insert line is irrelevant.
toc_edit(
text,
ast,
uri,
utf8,
heading,
level,
margin,
link_format,
None,
)
}
/// Produce a `WorkspaceEdit` that replaces or inserts the