feat(lsp): add list.removeCheckbox and link.catUrl commands
removeCheckbox strips the `[ ]` marker (plus its trailing space) from the current item or every item in the list. catUrl returns the `file://` URL of the current page's HTML export, mirroring vimwiki's :VimwikiCatUrl. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -68,9 +68,11 @@ pub const COMMANDS: &[&str] = &[
|
||||
"nuwiki.link.pasteWikilink",
|
||||
"nuwiki.link.pasteUrl",
|
||||
"nuwiki.list.removeDone",
|
||||
"nuwiki.list.removeCheckbox",
|
||||
"nuwiki.list.renumber",
|
||||
"nuwiki.list.changeSymbol",
|
||||
"nuwiki.list.changeLevel",
|
||||
"nuwiki.link.catUrl",
|
||||
"nuwiki.table.insert",
|
||||
"nuwiki.table.align",
|
||||
"nuwiki.table.moveColumn",
|
||||
@@ -183,6 +185,10 @@ pub(crate) async fn execute(
|
||||
"nuwiki.list.removeDone" => {
|
||||
list_remove_done(backend, args).map(|o| o.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.list.removeCheckbox" => {
|
||||
list_remove_checkbox(backend, args).map(|o| o.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.link.catUrl" => link_cat_url(backend, args).map(|o| o.map(CommandOutcome::Value)),
|
||||
"nuwiki.list.renumber" => list_renumber(backend, args).map(|o| o.map(CommandOutcome::Edit)),
|
||||
"nuwiki.list.changeSymbol" => {
|
||||
list_change_symbol(backend, args).map(|o| o.map(CommandOutcome::Edit))
|
||||
@@ -854,6 +860,55 @@ fn list_remove_done(backend: &Backend, args: Vec<Value>) -> Result<Option<Worksp
|
||||
))
|
||||
}
|
||||
|
||||
fn list_remove_checkbox(
|
||||
backend: &Backend,
|
||||
args: Vec<Value>,
|
||||
) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Args {
|
||||
uri: Url,
|
||||
position: LspPosition,
|
||||
#[serde(default)]
|
||||
whole_list: bool,
|
||||
}
|
||||
let raw = args
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| "missing { uri, position } argument".to_string())?;
|
||||
let parsed: Args = serde_json::from_value(raw).map_err(|e| format!("invalid args: {e}"))?;
|
||||
let doc = match backend.documents.get(&parsed.uri) {
|
||||
Some(d) => d,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||
let (line, _) = nav::lsp_to_byte_pos(parsed.position, &doc.text, utf8);
|
||||
Ok(ops::remove_checkbox_edit(
|
||||
&doc.text,
|
||||
&doc.ast,
|
||||
&parsed.uri,
|
||||
line,
|
||||
parsed.whole_list,
|
||||
utf8,
|
||||
))
|
||||
}
|
||||
|
||||
/// Compute the `file://` URL of the current page's HTML export, returned
|
||||
/// to the client. Mirrors vimwiki's `VimwikiCatUrl`, which echoes
|
||||
/// `file://<html-export-path>` for the current file.
|
||||
fn link_cat_url(backend: &Backend, args: Vec<Value>) -> Result<Option<Value>, String> {
|
||||
let p = parse_uri_arg(args)?;
|
||||
let Some(wiki) = backend.wiki_for_uri(&p.uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let cfg = &wiki.config;
|
||||
let name = crate::index::page_name_from_uri(&p.uri, Some(&cfg.root));
|
||||
let out_path = crate::export::output_path_for(&cfg.html, &name);
|
||||
let url = Url::from_file_path(&out_path)
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or_else(|_| format!("file://{}", out_path.display()));
|
||||
Ok(Some(Value::from(url)))
|
||||
}
|
||||
|
||||
fn list_renumber(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Args {
|
||||
@@ -2161,6 +2216,70 @@ pub mod ops {
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
/// Strip the `[ ]` checkbox (plus a single trailing space) from the
|
||||
/// item at `line`, or from every item in the list when `whole_list`
|
||||
/// is set. Leaves the marker and content intact — only the checkbox
|
||||
/// disappears.
|
||||
pub fn remove_checkbox_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
uri: &Url,
|
||||
line: u32,
|
||||
whole_list: bool,
|
||||
utf8: bool,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
if whole_list {
|
||||
let list = find_list_at_line(ast, line)?;
|
||||
collect_checkbox_spans(list, text, &mut spans);
|
||||
} else {
|
||||
let item = find_list_item_at(ast, line, 0)?;
|
||||
if let Some(s) = remove_checkbox_span(text, item.span) {
|
||||
spans.push(s);
|
||||
}
|
||||
}
|
||||
if spans.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
for span in spans {
|
||||
b.edit(uri.clone(), text_edit_delete(span, text, utf8));
|
||||
}
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
fn collect_checkbox_spans(list: &ListNode, text: &str, spans: &mut Vec<Span>) {
|
||||
for item in &list.items {
|
||||
if let Some(s) = remove_checkbox_span(text, item.span) {
|
||||
spans.push(s);
|
||||
}
|
||||
if let Some(sub) = &item.sublist {
|
||||
collect_checkbox_spans(sub, text, spans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Span of the `[ ]` checkbox plus one trailing space (so removal
|
||||
/// leaves `* foo`, not `* foo`). `None` when the item has no checkbox.
|
||||
fn remove_checkbox_span(text: &str, item_span: Span) -> Option<Span> {
|
||||
let cb = find_checkbox_span(text, item_span)?;
|
||||
let bytes = text.as_bytes();
|
||||
let mut end_off = cb.end.offset;
|
||||
let mut end_col = cb.end.column;
|
||||
if end_off < bytes.len() && bytes[end_off] == b' ' {
|
||||
end_off += 1;
|
||||
end_col += 1;
|
||||
}
|
||||
Some(Span::new(
|
||||
cb.start,
|
||||
AstPosition {
|
||||
line: cb.end.line,
|
||||
column: end_col,
|
||||
offset: end_off,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user