diff --git a/crates/tinymist-query/src/code_action.rs b/crates/tinymist-query/src/code_action.rs index 3c622c5d2..72966cc8b 100644 --- a/crates/tinymist-query/src/code_action.rs +++ b/crates/tinymist-query/src/code_action.rs @@ -3,7 +3,11 @@ use once_cell::sync::{Lazy, OnceCell}; use regex::Regex; use typst_shim::syntax::LinkedNodeExt; -use crate::{prelude::*, SemanticRequest}; +use crate::{ + prelude::*, + syntax::{interpret_mode_at, InterpretMode}, + SemanticRequest, +}; /// The [`textDocument/codeAction`] request is sent from the client to the /// server to compute commands for a given text document and range. These @@ -77,12 +81,10 @@ impl SemanticRequest for CodeActionRequest { fn request(self, ctx: &mut LocalContext) -> Option { let source = ctx.source_by_path(&self.path).ok()?; let range = ctx.to_typst_range(self.range, &source)?; - let cursor = (range.start + 1).min(source.text().len()); - // todo: don't ignore the range end let root = LinkedNode::new(source.root()); let mut worker = CodeActionWorker::new(ctx, source.clone()); - worker.work(root, cursor); + worker.work(root, range); let res = worker.actions; (!res.is_empty()).then_some(res) @@ -112,6 +114,7 @@ impl<'a> CodeActionWorker<'a> { .as_ref() } + #[must_use] fn local_edits(&self, edits: Vec) -> Option { Some(WorkspaceEdit { changes: Some(HashMap::from_iter([(self.local_url()?.clone(), edits)])), @@ -119,10 +122,45 @@ impl<'a> CodeActionWorker<'a> { }) } + #[must_use] fn local_edit(&self, edit: TextEdit) -> Option { self.local_edits(vec![edit]) } + fn wrap_actions(&mut self, node: &LinkedNode, range: Range) -> Option<()> { + if range.is_empty() { + return None; + } + + let start_mode = interpret_mode_at(Some(node)); + if !matches!(start_mode, InterpretMode::Markup | InterpretMode::Math) { + return None; + } + + let edit = self.local_edits(vec![ + TextEdit { + range: self + .ctx + .to_lsp_range(range.start..range.start, &self.current), + new_text: "#[".into(), + }, + TextEdit { + range: self.ctx.to_lsp_range(range.end..range.end, &self.current), + new_text: "]".into(), + }, + ])?; + + let action = CodeActionOrCommand::CodeAction(CodeAction { + title: "Wrap with content block".to_string(), + kind: Some(CodeActionKind::REFACTOR_REWRITE), + edit: Some(edit), + ..CodeAction::default() + }); + self.actions.push(action); + + Some(()) + } + fn heading_actions(&mut self, node: &LinkedNode) -> Option<()> { let h = node.cast::()?; let depth = h.depth().get(); @@ -269,13 +307,16 @@ impl<'a> CodeActionWorker<'a> { Some(()) } - fn work(&mut self, root: LinkedNode, cursor: usize) -> Option<()> { + fn work(&mut self, root: LinkedNode, range: Range) -> Option<()> { + let cursor = (range.start + 1).min(self.current.text().len()); let node = root.leaf_at_compat(cursor)?; let mut node = &node; let mut heading_resolved = false; let mut equation_resolved = false; + self.wrap_actions(node, range); + loop { match node.kind() { // Only the deepest heading is considered