mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-22 12:34:39 +00:00
feat: offer quickfix to add spaces separating letters in unknown math var (#2062)
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run
Examples: | Input | Suggested fix | | -- | -- | | `$xyz$` | `$x y z$` | | `$a_ij$` | `$a_(i j)$` | | `$a_(ij)$` | `$a_(i j)$` | | `$xy/z$` | `$(x y)/z$` | If the unknown identifier appears as a subscript or as the numerator/denominator of a fraction, we parenthesize the suggested fix. For example, `a_ij` turns into `a_(i j)`, not `a_i j`, because the latter is unlikely to be what the user intended.
This commit is contained in:
parent
25d5bfa222
commit
4324f8fd70
12 changed files with 405 additions and 7 deletions
|
|
@ -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::<ast::Ident>() {
|
||||
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::<ast::MathIdent>()?.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,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@ impl SemanticRequest for CheckRequest {
|
|||
|
||||
fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
|
||||
let worker = DiagWorker::new(ctx);
|
||||
Some(worker.check().convert_all(self.snap.diagnostics()))
|
||||
Some(worker.full_check().convert_all(self.snap.diagnostics()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<PagedDocument>(&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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
$xyz/* range -1..-1 */$
|
||||
|
|
@ -0,0 +1 @@
|
|||
$ab/c/* range -3..-3 */$
|
||||
|
|
@ -0,0 +1 @@
|
|||
$a_ij/* range -1..-1 */$
|
||||
|
|
@ -0,0 +1 @@
|
|||
$a_(ij)/* range -2..-2 */$
|
||||
Loading…
Add table
Add a link
Reference in a new issue