diff --git a/crates/tinymist-query/src/analysis/code_action.rs b/crates/tinymist-query/src/analysis/code_action.rs index e59b6cf3..951b3854 100644 --- a/crates/tinymist-query/src/analysis/code_action.rs +++ b/crates/tinymist-query/src/analysis/code_action.rs @@ -99,7 +99,16 @@ impl<'a> CodeActionWorker<'a> { ) -> Option<()> { let cursor = (range.start + 1).min(self.source.text().len()); let node = root.leaf_at_compat(cursor)?; + self.create_missing_variable(root, &node); + self.add_spaces_to_math_unknown_variable(&node); + Some(()) + } + fn create_missing_variable( + &mut self, + root: &LinkedNode<'_>, + node: &LinkedNode<'_>, + ) -> Option<()> { let ident = 'determine_ident: { if let Some(ident) = node.cast::() { break 'determine_ident ident.get().clone(); @@ -117,7 +126,7 @@ impl<'a> CodeActionWorker<'a> { Bad, } - let previous_decl = previous_items(node, |item| { + let previous_decl = previous_items(node.clone(), |item| { match item { PreviousItem::Parent(parent, ..) => match parent.kind() { SyntaxKind::LetBinding => { @@ -198,6 +207,34 @@ impl<'a> CodeActionWorker<'a> { Some(()) } + /// Add spaces between letters in an unknown math identifier: `$xyz$` -> `$x y z$`. + fn add_spaces_to_math_unknown_variable(&mut self, node: &LinkedNode<'_>) -> Option<()> { + let ident = node.cast::()?.get(); + + // Rewrite `a_ij` as `a_(i j)`, not `a_i j`. + // Likewise rewrite `ab/c` as `(a b)/c`, not `a b/c`. + let needs_parens = matches!( + node.parent_kind(), + Some(SyntaxKind::MathAttach | SyntaxKind::MathFrac) + ); + let new_text = if needs_parens { + eco_format!("({})", ident.chars().join(" ")) + } else { + ident.chars().join(" ").into() + }; + + let range = self.ctx.to_lsp_range(node.range(), &self.source); + let edit = self.local_edit(EcoSnippetTextEdit::new(range, new_text))?; + let action = CodeAction { + title: "Add spaces between letters".to_string(), + kind: Some(CodeActionKind::QUICKFIX), + edit: Some(edit), + ..CodeAction::default() + }; + self.actions.push(action); + Some(()) + } + /// Automatically fixes file not found errors. pub fn autofix_file_not_found( &mut self, diff --git a/crates/tinymist-query/src/check.rs b/crates/tinymist-query/src/check.rs index c8d90180..58155f4e 100644 --- a/crates/tinymist-query/src/check.rs +++ b/crates/tinymist-query/src/check.rs @@ -14,6 +14,6 @@ impl SemanticRequest for CheckRequest { fn request(self, ctx: &mut LocalContext) -> Option { let worker = DiagWorker::new(ctx); - Some(worker.check().convert_all(self.snap.diagnostics())) + Some(worker.full_check().convert_all(self.snap.diagnostics())) } } diff --git a/crates/tinymist-query/src/code_action.rs b/crates/tinymist-query/src/code_action.rs index c5a93cbd..568175bd 100644 --- a/crates/tinymist-query/src/code_action.rs +++ b/crates/tinymist-query/src/code_action.rs @@ -93,18 +93,22 @@ impl SemanticRequest for CodeActionRequest { #[cfg(test)] mod tests { + use typst::{diag::Warned, layout::PagedDocument}; + use super::*; - use crate::tests::*; + use crate::{DiagWorker, tests::*}; #[test] fn test() { snapshot_testing("code_action", &|ctx, path| { let source = ctx.source_by_path(&path).unwrap(); + let request_range = find_test_range(&source); + let code_action_ctx = compute_code_action_context(ctx, &source, &request_range); let request = CodeActionRequest { path: path.clone(), - range: find_test_range(&source), - context: CodeActionContext::default(), + range: request_range, + context: code_action_ctx, }; let result = request.request(ctx); @@ -116,4 +120,37 @@ mod tests { }) }); } + + fn compute_code_action_context( + ctx: &mut LocalContext, + source: &Source, + request_range: &LspRange, + ) -> CodeActionContext { + let Warned { output, warnings } = typst::compile::(&ctx.world); + let errors = output.err().unwrap_or_default(); + let compiler_diagnostics = warnings.iter().chain(errors.iter()); + + // Run the linter for additional diagnostics as well. + let diagnostics = DiagWorker::new(ctx) + .check(source) + .convert_all(compiler_diagnostics) + .into_values() + .flatten(); + + CodeActionContext { + // The filtering here matches the LSP specification and VS Code behavior; + // see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext: + // `diagnostics`: An array of diagnostics known on the client side overlapping the range + // provided to the textDocument/codeAction request [...] + diagnostics: diagnostics + .filter(|diag| ranges_overlap(&diag.range, request_range)) + .collect(), + only: None, + trigger_kind: None, + } + } + + fn ranges_overlap(r1: &LspRange, r2: &LspRange) -> bool { + !(r1.end <= r2.start || r2.end <= r1.start) + } } diff --git a/crates/tinymist-query/src/diagnostics.rs b/crates/tinymist-query/src/diagnostics.rs index 4304405b..9824bce0 100644 --- a/crates/tinymist-query/src/diagnostics.rs +++ b/crates/tinymist-query/src/diagnostics.rs @@ -46,8 +46,16 @@ impl<'w> DiagWorker<'w> { } } - /// Runs code check on the document. - pub fn check(mut self) -> Self { + /// Runs code check on the given document. + pub fn check(mut self, source: &Source) -> Self { + for diag in self.ctx.lint(&source) { + self.handle(&diag); + } + self + } + + /// Runs code check on the main document and all its dependencies. + pub fn full_check(mut self) -> Self { for dep in self.ctx.world.depended_files() { if WorkspaceResolver::is_package_file(dep) { continue; diff --git a/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident.typ.snap b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident.typ.snap new file mode 100644 index 00000000..85b38363 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident.typ.snap @@ -0,0 +1,78 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: "Code Action on $xy||z/* range " +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_action/unknown_math_ident.typ +--- +[ + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "\n\nlet xyz", + "range": "0:0:0:0" + } + ] + } + }, + "kind": "quickfix", + "title": "Create missing variable" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "x y z", + "range": "0:1:0:4" + } + ] + } + }, + "kind": "quickfix", + "title": "Add spaces between letters" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:4:0:22" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to block equation" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:4:0:22" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to multiple-line block equation" + } +] diff --git a/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident_frac.typ.snap b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident_frac.typ.snap new file mode 100644 index 00000000..51a4d54c --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_ident_frac.typ.snap @@ -0,0 +1,78 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: Code Action on $a||b/c/* rang +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_action/unknown_math_ident_frac.typ +--- +[ + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "\n\nlet ab", + "range": "0:0:0:0" + } + ] + } + }, + "kind": "quickfix", + "title": "Create missing variable" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "(a b)", + "range": "0:1:0:3" + } + ] + } + }, + "kind": "quickfix", + "title": "Add spaces between letters" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:5:0:23" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to block equation" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:5:0:23" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to multiple-line block equation" + } +] diff --git a/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript.typ.snap b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript.typ.snap new file mode 100644 index 00000000..e21e795a --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript.typ.snap @@ -0,0 +1,78 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: "Code Action on $a_i||j/* range " +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript.typ +--- +[ + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "\n\nlet ij", + "range": "0:0:0:0" + } + ] + } + }, + "kind": "quickfix", + "title": "Create missing variable" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "(i j)", + "range": "0:3:0:5" + } + ] + } + }, + "kind": "quickfix", + "title": "Add spaces between letters" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:5:0:23" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to block equation" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:5:0:23" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to multiple-line block equation" + } +] diff --git a/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript_paren.typ.snap b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript_paren.typ.snap new file mode 100644 index 00000000..956cfc2f --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/snaps/test@unknown_math_subscript_paren.typ.snap @@ -0,0 +1,78 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: Code Action on $a_(i||j)/* range +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript_paren.typ +--- +[ + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "\n\nlet ij", + "range": "0:0:0:0" + } + ] + } + }, + "kind": "quickfix", + "title": "Create missing variable" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 2, + "newText": "i j", + "range": "0:4:0:6" + } + ] + } + }, + "kind": "quickfix", + "title": "Add spaces between letters" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": " ", + "range": "0:7:0:25" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to block equation" + }, + { + "edit": { + "changes": { + "s0.typ": [ + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:1:0:1" + }, + { + "insertTextFormat": 1, + "newText": "\n", + "range": "0:7:0:25" + } + ] + } + }, + "kind": "refactor.rewrite", + "title": "Convert to multiple-line block equation" + } +] diff --git a/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident.typ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident.typ new file mode 100644 index 00000000..b4d94652 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident.typ @@ -0,0 +1 @@ +$xyz/* range -1..-1 */$ diff --git a/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident_frac.typ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident_frac.typ new file mode 100644 index 00000000..5c1ab385 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_ident_frac.typ @@ -0,0 +1 @@ +$ab/c/* range -3..-3 */$ diff --git a/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript.typ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript.typ new file mode 100644 index 00000000..0f32a345 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript.typ @@ -0,0 +1 @@ +$a_ij/* range -1..-1 */$ diff --git a/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript_paren.typ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript_paren.typ new file mode 100644 index 00000000..76330c4b --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_action/unknown_math_subscript_paren.typ @@ -0,0 +1 @@ +$a_(ij)/* range -2..-2 */$