feat: autofix unknown variable: a and file not found (searched at a.typ) by code action (#1743)

* feat: check context

* feat: implement it

* fix: warnings

* test: update snapshot
This commit is contained in:
Myriad-Dreamin 2025-05-09 23:44:12 +08:00 committed by GitHub
parent d6d3766b6f
commit 38974a3b5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 834 additions and 84 deletions

View file

@ -1,8 +1,13 @@
//! Provides code actions for the document.
use ecow::eco_format;
use lsp_types::{ChangeAnnotation, CreateFile, CreateFileOptions};
use regex::Regex;
use tinymist_analysis::syntax::{adjust_expr, node_ancestors, SyntaxClass};
use tinymist_analysis::syntax::{
adjust_expr, node_ancestors, previous_items, PreviousItem, SyntaxClass,
};
use tinymist_std::path::{diff, unix_slash};
use typst::syntax::Side;
use super::get_link_exprs_in;
use crate::analysis::LinkTarget;
@ -16,7 +21,7 @@ pub struct CodeActionWorker<'a> {
/// The source document to analyze.
source: Source,
/// The code actions to provide.
pub actions: Vec<CodeActionOrCommand>,
pub actions: Vec<CodeAction>,
/// The lazily calculated local URL to [`Self::source`].
local_url: OnceLock<Option<Url>>,
}
@ -39,20 +44,200 @@ impl<'a> CodeActionWorker<'a> {
}
#[must_use]
fn local_edits(&self, edits: Vec<TextEdit>) -> Option<WorkspaceEdit> {
Some(WorkspaceEdit {
fn local_edits(&self, edits: Vec<EcoSnippetTextEdit>) -> Option<EcoWorkspaceEdit> {
Some(EcoWorkspaceEdit {
changes: Some(HashMap::from_iter([(self.local_url()?.clone(), edits)])),
..Default::default()
})
}
#[must_use]
fn local_edit(&self, edit: TextEdit) -> Option<WorkspaceEdit> {
fn local_edit(&self, edit: EcoSnippetTextEdit) -> Option<EcoWorkspaceEdit> {
self.local_edits(vec![edit])
}
pub(crate) fn autofix(
&mut self,
root: &LinkedNode<'_>,
range: &Range<usize>,
context: &lsp_types::CodeActionContext,
) -> Option<()> {
if let Some(only) = &context.only {
if !only.is_empty()
&& !only
.iter()
.any(|kind| *kind == CodeActionKind::EMPTY || *kind == CodeActionKind::QUICKFIX)
{
return None;
}
}
for diag in &context.diagnostics {
if diag.source.as_ref().is_none_or(|t| t != "typst") {
continue;
}
match match_autofix_kind(diag.message.as_str()) {
Some(AutofixKind::UnknownVariable) => {
self.autofix_unknown_variable(root, range);
}
Some(AutofixKind::FileNotFound) => {
self.autofix_file_not_found(root, range);
}
_ => {}
}
}
Some(())
}
/// Automatically fixes unknown variable errors.
pub fn autofix_unknown_variable(
&mut self,
root: &LinkedNode,
range: &Range<usize>,
) -> Option<()> {
let cursor = (range.start + 1).min(self.source.text().len());
let node = root.leaf_at_compat(cursor)?;
let ident = 'determine_ident: {
if let Some(ident) = node.cast::<ast::Ident>() {
break 'determine_ident ident.get().clone();
}
if let Some(ident) = node.cast::<ast::MathIdent>() {
break 'determine_ident ident.get().clone();
}
return None;
};
enum CreatePosition {
Before(usize),
After(usize),
Bad,
}
let previous_decl = previous_items(node, |item| {
match item {
PreviousItem::Parent(parent, ..) => match parent.kind() {
SyntaxKind::LetBinding => {
let mut create_before = parent.clone();
while let Some(before) = create_before.prev_sibling() {
if matches!(before.kind(), SyntaxKind::Hash) {
create_before = before;
continue;
}
break;
}
return Some(CreatePosition::Before(create_before.range().start));
}
SyntaxKind::CodeBlock | SyntaxKind::ContentBlock => {
let child = parent.children().find(|child| {
matches!(
child.kind(),
SyntaxKind::LeftBrace | SyntaxKind::LeftBracket
)
})?;
return Some(CreatePosition::After(child.range().end));
}
SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude => {
return Some(CreatePosition::Bad);
}
_ => {}
},
PreviousItem::Sibling(node) => {
if matches!(
node.kind(),
SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude
) {
// todo: hash
return Some(CreatePosition::After(node.range().end));
}
}
}
None
});
let (create_pos, side) = match previous_decl {
Some(CreatePosition::Before(pos)) => (pos, Side::Before),
Some(CreatePosition::After(pos)) => (pos, Side::After),
None => (0, Side::After),
Some(CreatePosition::Bad) => return None,
};
let pos_node = root.leaf_at(create_pos, side.clone());
let mode = match interpret_mode_at(pos_node.as_ref()) {
InterpretMode::Markup => "#",
_ => "",
};
let extend_assign = if self.ctx.analysis.extended_code_action {
" = ${1:none}$0"
} else {
""
};
let new_text = if matches!(side, Side::Before) {
eco_format!("{mode}let {ident}{extend_assign}\n\n")
} else {
eco_format!("\n\n{mode}let {ident}{extend_assign}")
};
let range = self.ctx.to_lsp_range(create_pos..create_pos, &self.source);
let edit = self.local_edit(EcoSnippetTextEdit::new(range, new_text))?;
let action = CodeAction {
title: "Create missing variable".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,
root: &LinkedNode,
range: &Range<usize>,
) -> Option<()> {
let cursor = (range.start + 1).min(self.source.text().len());
let node = root.leaf_at_compat(cursor)?;
let importing = node.cast::<ast::Str>()?.get();
if importing.starts_with('@') {
// todo: create local package?
// if importing.starts_with("@local") { return None; }
// This is a package import, not a file import.
return None;
}
let file_id = node.span().id()?;
let root_path = self.ctx.path_for_id(file_id.join("/")).ok()?;
let path_in_workspace = file_id.vpath().join(importing.as_str());
let new_path = path_in_workspace.resolve(root_path.as_path())?;
let new_file_url = path_to_url(&new_path).ok()?;
let edit = self.create_file(new_file_url, false);
let file_to_create = unix_slash(path_in_workspace.as_rooted_path());
let action = CodeAction {
title: format!("Create missing file at `{file_to_create}`"),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(edit),
..CodeAction::default()
};
self.actions.push(action);
Some(())
}
/// Starts to work.
pub fn work(&mut self, root: LinkedNode, range: Range<usize>) -> Option<()> {
pub fn scoped(&mut self, root: &LinkedNode, range: &Range<usize>) -> Option<()> {
let cursor = (range.start + 1).min(self.source.text().len());
let node = root.leaf_at_compat(cursor)?;
let mut node = &node;
@ -133,12 +318,12 @@ impl<'a> CodeActionWorker<'a> {
let cur_path = id.vpath().as_rooted_path().parent().unwrap();
let new_path = diff(path, cur_path)?;
let edit = self.edit_str(node, unix_slash(&new_path))?;
let action = CodeActionOrCommand::CodeAction(CodeAction {
let action = CodeAction {
title: "Convert to relative path".to_string(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(edit),
..CodeAction::default()
});
};
self.actions.push(action);
} else {
// Convert relative path to absolute path
@ -155,32 +340,32 @@ impl<'a> CodeActionWorker<'a> {
}
}
let edit = self.edit_str(node, unix_slash(&new_path))?;
let action = CodeActionOrCommand::CodeAction(CodeAction {
let action = CodeAction {
title: "Convert to absolute path".to_string(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(edit),
..CodeAction::default()
});
};
self.actions.push(action);
}
Some(())
}
fn edit_str(&mut self, node: &LinkedNode, new_content: String) -> Option<WorkspaceEdit> {
fn edit_str(&mut self, node: &LinkedNode, new_content: String) -> Option<EcoWorkspaceEdit> {
if !matches!(node.kind(), SyntaxKind::Str) {
log::warn!("edit_str only works on string AST nodes: {:?}", node.kind());
return None;
}
self.local_edit(TextEdit {
range: self.ctx.to_lsp_range(node.range(), &self.source),
// todo: this is merely ocasionally correct, abusing string escape (`fmt::Debug`)
new_text: format!("{new_content:?}"),
})
self.local_edit(EcoSnippetTextEdit::new_plain(
self.ctx.to_lsp_range(node.range(), &self.source),
// todo: this is merely occasionally correct, abusing string escape (`fmt::Debug`)
eco_format!("{new_content:?}"),
))
}
fn wrap_actions(&mut self, node: &LinkedNode, range: Range<usize>) -> Option<()> {
fn wrap_actions(&mut self, node: &LinkedNode, range: &Range<usize>) -> Option<()> {
if range.is_empty() {
return None;
}
@ -191,24 +376,23 @@ impl<'a> CodeActionWorker<'a> {
}
let edit = self.local_edits(vec![
TextEdit {
range: self
.ctx
EcoSnippetTextEdit::new_plain(
self.ctx
.to_lsp_range(range.start..range.start, &self.source),
new_text: "#[".into(),
},
TextEdit {
range: self.ctx.to_lsp_range(range.end..range.end, &self.source),
new_text: "]".into(),
},
EcoString::inline("#["),
),
EcoSnippetTextEdit::new_plain(
self.ctx.to_lsp_range(range.end..range.end, &self.source),
EcoString::inline("]"),
),
])?;
let action = CodeActionOrCommand::CodeAction(CodeAction {
let action = CodeAction {
title: "Wrap with content block".to_string(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(edit),
..CodeAction::default()
});
};
self.actions.push(action);
Some(())
@ -226,28 +410,28 @@ impl<'a> CodeActionWorker<'a> {
if depth > 1 {
// Decrease depth of heading
let action = CodeActionOrCommand::CodeAction(CodeAction {
let action = CodeAction {
title: "Decrease depth of heading".to_string(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(self.local_edit(TextEdit {
range: self.ctx.to_lsp_range(marker_range.clone(), &self.source),
new_text: "=".repeat(depth - 1),
})?),
edit: Some(self.local_edit(EcoSnippetTextEdit::new_plain(
self.ctx.to_lsp_range(marker_range.clone(), &self.source),
EcoString::inline("=").repeat(depth - 1),
))?),
..CodeAction::default()
});
};
self.actions.push(action);
}
// Increase depth of heading
let action = CodeActionOrCommand::CodeAction(CodeAction {
let action = CodeAction {
title: "Increase depth of heading".to_string(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(self.local_edit(TextEdit {
range: self.ctx.to_lsp_range(marker_range, &self.source),
new_text: "=".repeat(depth + 1),
})?),
edit: Some(self.local_edit(EcoSnippetTextEdit::new_plain(
self.ctx.to_lsp_range(marker_range, &self.source),
EcoString::inline("=").repeat(depth + 1),
))?),
..CodeAction::default()
});
};
self.actions.push(action);
Some(())
@ -302,10 +486,7 @@ impl<'a> CodeActionWorker<'a> {
let ch_range = self
.ctx
.to_lsp_range(node_end..node_end + nx.len_utf8(), &self.source);
let remove_edit = TextEdit {
range: ch_range,
new_text: "".to_owned(),
};
let remove_edit = EcoSnippetTextEdit::new_plain(ch_range, EcoString::new());
Some((nx, remove_edit))
} else {
None
@ -313,22 +494,19 @@ impl<'a> CodeActionWorker<'a> {
let rewrite_action = |title: &str, new_text: &str| {
let mut edits = vec![
TextEdit {
range: front_range,
new_text: new_text.to_owned(),
},
TextEdit {
range: back_range,
new_text: if !new_text.is_empty() {
EcoSnippetTextEdit::new_plain(front_range, new_text.into()),
EcoSnippetTextEdit::new_plain(
back_range,
if !new_text.is_empty() {
if let Some((ch, _)) = &punc_modify {
ch.to_string() + new_text
EcoString::from(*ch) + new_text
} else {
new_text.to_owned()
new_text.into()
}
} else {
"".to_owned()
EcoString::new()
},
},
),
];
if !new_text.is_empty() {
@ -337,12 +515,12 @@ impl<'a> CodeActionWorker<'a> {
}
}
Some(CodeActionOrCommand::CodeAction(CodeAction {
Some(CodeAction {
title: title.to_owned(),
kind: Some(CodeActionKind::REFACTOR_REWRITE),
edit: Some(self.local_edits(edits)?),
..CodeAction::default()
}))
})
};
// Prepare actions
@ -360,4 +538,54 @@ impl<'a> CodeActionWorker<'a> {
Some(())
}
fn create_file(&self, uri: Url, needs_confirmation: bool) -> EcoWorkspaceEdit {
let change_id = "Typst Create Missing Files".to_string();
let create_op = EcoDocumentChangeOperation::Op(lsp_types::ResourceOp::Create(CreateFile {
uri,
options: Some(CreateFileOptions {
overwrite: Some(false),
ignore_if_exists: None,
}),
annotation_id: Some(change_id.clone()),
}));
let mut change_annotations = HashMap::new();
change_annotations.insert(
change_id.clone(),
ChangeAnnotation {
label: change_id,
needs_confirmation: Some(needs_confirmation),
description: Some("The file is missing but required by code".to_string()),
},
);
EcoWorkspaceEdit {
changes: None,
document_changes: Some(EcoDocumentChanges::Operations(vec![create_op])),
change_annotations: Some(change_annotations),
}
}
}
#[derive(Debug, Clone, Copy)]
enum AutofixKind {
UnknownVariable,
FileNotFound,
}
fn match_autofix_kind(msg: &str) -> Option<AutofixKind> {
static PATTERNS: &[(&str, AutofixKind)] = &[
("unknown variable", AutofixKind::UnknownVariable),
("file not found", AutofixKind::FileNotFound),
];
for (pattern, kind) in PATTERNS {
if msg.starts_with(pattern) {
return Some(*kind);
}
}
None
}

View file

@ -377,11 +377,11 @@ type Cursor<'a> = CompletionCursor<'a>;
/// A node selected by [`CompletionCursor`].
enum SelectedNode<'a> {
/// Selects an identifier, e.g. `foo|` or `fo|o`.
/// Selects an identifier, e.g. `foobar|` or `foo|bar`.
Ident(LinkedNode<'a>),
/// Selects a label, e.g. `<foo|>` or `<fo|o>`.
/// Selects a label, e.g. `<foobar|>` or `<foo|bar>`.
Label(LinkedNode<'a>),
/// Selects a reference, e.g. `@foo|` or `@fo|o`.
/// Selects a reference, e.g. `@foobar|` or `@foo|bar`.
Ref(LinkedNode<'a>),
}

View file

@ -65,6 +65,14 @@ pub struct Analysis {
pub allow_multiline_token: bool,
/// Whether to remove html from markup content in responses.
pub remove_html: bool,
/// Whether to utilize the extended `tinymist.resolveCodeAction` at client
/// side.
///
/// The extended feature by `tinymist.resolveCodeAction`:
/// - supports Snippet edit.
///
/// The example implementation can be found in the VS Code extension.
pub extended_code_action: bool,
/// Tinymist's completion features.
pub completion_feat: CompletionFeat,
/// The editor's color theme.

View file

@ -60,8 +60,10 @@ impl LinkTarget {
LinkTarget::Url(url) => Some(url.as_ref().clone()),
LinkTarget::Path(id, path) => {
// Avoid creating new ids here.
let root = ctx.path_for_id(id.join("")).ok()?;
crate::path_res_to_url(root.join(path).ok()?).ok()
let root = ctx.path_for_id(id.join("/")).ok()?;
let path_in_workspace = id.vpath().join(Path::new(path.as_str()));
let path = root.resolve_to(&path_in_workspace)?;
crate::path_res_to_url(path).ok()
}
}
}

View file

@ -1,5 +1,9 @@
use lsp_types::CodeActionContext;
use crate::{analysis::CodeActionWorker, prelude::*, SemanticRequest};
pub(crate) mod proto;
/// The [`textDocument/codeAction`] request is sent from the client to the
/// server to compute commands for a given text document and range. These
/// commands are typically code fixes to either fix problems or to
@ -64,18 +68,23 @@ pub struct CodeActionRequest {
pub path: PathBuf,
/// The range of the document to get code actions for.
pub range: LspRange,
/// The context of the code action request.
pub context: CodeActionContext,
}
impl SemanticRequest for CodeActionRequest {
type Response = Vec<CodeActionOrCommand>;
type Response = Vec<CodeAction>;
fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
log::info!("requested code action: {self:?}");
let source = ctx.source_by_path(&self.path).ok()?;
let range = ctx.to_typst_range(self.range, &source)?;
let root = LinkedNode::new(source.root());
let mut worker = CodeActionWorker::new(ctx, source.clone());
worker.work(root, range);
worker.autofix(&root, &range, &self.context);
worker.scoped(&root, &range);
(!worker.actions.is_empty()).then_some(worker.actions)
}
@ -94,12 +103,13 @@ mod tests {
let request = CodeActionRequest {
path: path.clone(),
range: find_test_range(&source),
context: CodeActionContext::default(),
};
let result = request.request(ctx);
with_settings!({
description => format!("Code Action on {}", make_range_annoation(&source)),
description => format!("Code Action on {}", make_range_annotation(&source)),
}, {
assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
})

View file

@ -0,0 +1,309 @@
use std::collections::HashMap;
use ecow::EcoString;
use lsp_types::{
ChangeAnnotation, ChangeAnnotationIdentifier, CodeActionDisabled, CodeActionKind, Command,
Diagnostic, InsertTextFormat, OneOf, OptionalVersionedTextDocumentIdentifier, ResourceOp, Url,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::LspRange;
use crate::completion::EcoTextEdit;
/// A textual edit applicable to a text document.
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EcoSnippetTextEdit {
/// The text edit
#[serde(flatten)]
edit: EcoTextEdit,
/// The format of the insert text. The format applies to both the
insert_text_format: Option<InsertTextFormat>,
}
impl EcoSnippetTextEdit {
pub fn new_plain(range: LspRange, new_text: EcoString) -> EcoSnippetTextEdit {
EcoSnippetTextEdit {
edit: EcoTextEdit::new(range, new_text),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
}
}
pub fn new(range: LspRange, new_text: EcoString) -> EcoSnippetTextEdit {
EcoSnippetTextEdit {
edit: EcoTextEdit::new(range, new_text),
insert_text_format: Some(InsertTextFormat::SNIPPET),
}
}
}
/// A special text edit with an additional change annotation.
///
/// @since 3.16.0
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EcoAnnotatedTextEdit {
#[serde(flatten)]
pub text_edit: EcoSnippetTextEdit,
/// The actual annotation
pub annotation_id: ChangeAnnotationIdentifier,
}
/// Describes textual changes on a single text document. The text document is
/// referred to as a `OptionalVersionedTextDocumentIdentifier` to allow clients
/// to check the text document version before an edit is applied. A
/// `TextDocumentEdit` describes all changes on a version Si and after they are
/// applied move the document to version Si+1. So the creator of a
/// `TextDocumentEdit` doesn't need to sort the array or do any kind of
/// ordering. However the edits must be non overlapping.
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EcoTextDocumentEdit {
/// The text document to change.
pub text_document: OptionalVersionedTextDocumentIdentifier,
/// The edits to be applied.
///
/// @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the
/// client capability `workspace.workspaceEdit.changeAnnotationSupport`
pub edits: Vec<OneOf<EcoSnippetTextEdit, EcoAnnotatedTextEdit>>,
}
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
/// A short, human-readable, title for this code action.
pub title: String,
/// The kind of the code action.
/// Used to filter code actions.
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<CodeActionKind>,
/// The diagnostics that this code action resolves.
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<Diagnostic>>,
/// The workspace edit this code action performs.
#[serde(skip_serializing_if = "Option::is_none")]
pub edit: Option<EcoWorkspaceEdit>,
/// A command this code action executes. If a code action
/// provides an edit and a command, first the edit is
/// executed and then the command.
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<Command>,
/// Marks this as a preferred action. Preferred actions are used by the
/// `auto fix` command and can be targeted by keybindings.
/// A quick fix should be marked preferred if it properly addresses the
/// underlying error. A refactoring should be marked preferred if it is
/// the most reasonable choice of actions to take.
///
/// @since 3.15.0
#[serde(skip_serializing_if = "Option::is_none")]
pub is_preferred: Option<bool>,
/// Marks that the code action cannot currently be applied.
///
/// Clients should follow the following guidelines regarding disabled code
/// actions:
///
/// - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action)
/// code action menu.
///
/// - Disabled actions are shown as faded out in the code action menu when
/// the user request a more specific type of code action, such as
/// refactorings.
///
/// - If the user has a keybinding that auto applies a code action and only
/// a disabled code actions are returned, the client should show the user
/// an error message with `reason` in the editor.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<CodeActionDisabled>,
/// A data entry field that is preserved on a code action between
/// a `textDocument/codeAction` and a `codeAction/resolve` request.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
/// A workspace edit represents changes to many resources managed in the
/// workspace. The edit should either provide `changes` or `documentChanges`.
/// If the client can handle versioned document edits and if `documentChanges`
/// are present, the latter are preferred over `changes`.
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EcoWorkspaceEdit {
/// Holds changes to existing resources.
#[serde(with = "url_map")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub changes: Option<HashMap<Url, Vec<EcoSnippetTextEdit>>>,
/// Depending on the client capability
/// `workspace.workspaceEdit.resourceOperations` document changes
/// are either an array of `TextDocumentEdit`s to express changes to n
/// different text documents where each text document edit addresses a
/// specific version of a text document. Or it can contain
/// above `TextDocumentEdit`s mixed with create, rename and delete file /
/// folder operations.
///
/// Whether a client supports versioned document edits is expressed via
/// `workspace.workspaceEdit.documentChanges` client capability.
///
/// If a client neither supports `documentChanges` nor
/// `workspace.workspaceEdit.resourceOperations` then only plain
/// `TextEdit`s using the `changes` property are supported.
#[serde(skip_serializing_if = "Option::is_none")]
pub document_changes: Option<EcoDocumentChanges>,
/// A map of change annotations that can be referenced in
/// `AnnotatedTextEdit`s or create, rename and delete file / folder
/// operations.
///
/// Whether clients honor this property depends on the client capability
/// `workspace.changeAnnotationSupport`.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub change_annotations: Option<HashMap<ChangeAnnotationIdentifier, ChangeAnnotation>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum EcoDocumentChanges {
Edits(Vec<EcoTextDocumentEdit>),
Operations(Vec<EcoDocumentChangeOperation>),
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged, rename_all = "lowercase")]
pub enum EcoDocumentChangeOperation {
Op(ResourceOp),
Edit(EcoTextDocumentEdit),
}
mod url_map {
use std::marker::PhantomData;
use std::{collections::HashMap, fmt};
use lsp_types::Url;
use serde::de;
pub fn deserialize<'de, D, V>(deserializer: D) -> Result<Option<HashMap<Url, V>>, D::Error>
where
D: serde::Deserializer<'de>,
V: de::DeserializeOwned,
{
struct UrlMapVisitor<V> {
_marker: PhantomData<V>,
}
impl<V: de::DeserializeOwned> Default for UrlMapVisitor<V> {
fn default() -> Self {
UrlMapVisitor {
_marker: PhantomData,
}
}
}
impl<'de, V: de::DeserializeOwned> de::Visitor<'de> for UrlMapVisitor<V> {
type Value = HashMap<Url, V>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("map")
}
fn visit_map<M>(self, mut visitor: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut values = HashMap::with_capacity(visitor.size_hint().unwrap_or(0));
// While there are entries remaining in the input, add them
// into our map.
while let Some((key, value)) = visitor.next_entry::<Url, _>()? {
values.insert(key, value);
}
Ok(values)
}
}
struct OptionUrlMapVisitor<V> {
_marker: PhantomData<V>,
}
impl<V: de::DeserializeOwned> Default for OptionUrlMapVisitor<V> {
fn default() -> Self {
OptionUrlMapVisitor {
_marker: PhantomData,
}
}
}
impl<'de, V: de::DeserializeOwned> de::Visitor<'de> for OptionUrlMapVisitor<V> {
type Value = Option<HashMap<Url, V>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("option")
}
#[inline]
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
#[inline]
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
#[inline]
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer
.deserialize_map(UrlMapVisitor::<V>::default())
.map(Some)
}
}
// Instantiate our Visitor and ask the Deserializer to drive
// it over the input data, resulting in an instance of MyMap.
deserializer.deserialize_option(OptionUrlMapVisitor::default())
}
pub fn serialize<S, V>(
changes: &Option<HashMap<Url, V>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
V: serde::Serialize,
{
use serde::ser::SerializeMap;
match *changes {
Some(ref changes) => {
let mut map = serializer.serialize_map(Some(changes.len()))?;
for (k, v) in changes {
map.serialize_entry(k.as_str(), v)?;
}
map.end()
}
None => serializer.serialize_none(),
}
}
}

View file

@ -1,6 +1,6 @@
---
source: crates/tinymist-query/src/code_action.rs
description: "Code Action on t \"/base.t||yp\"/* rang)"
description: "Code Action on t \"/base.t||yp\"/* rang"
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/code_action/absolute_path_import.typ
---
@ -10,6 +10,7 @@ input_file: crates/tinymist-query/src/fixtures/code_action/absolute_path_import.
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": "\"base.typ\"",
"range": "0:8:0:19"
}

View file

@ -1,6 +1,6 @@
---
source: crates/tinymist-query/src/code_action.rs
description: "Code Action on rt \"base.t||yp\"/* rang)"
description: "Code Action on rt \"base.t||yp\"/* rang"
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/code_action/path_and_equation.typ
---
@ -10,6 +10,7 @@ input_file: crates/tinymist-query/src/fixtures/code_action/path_and_equation.typ
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": "\"/base.typ\"",
"range": "0:9:0:19"
}
@ -24,10 +25,12 @@ input_file: crates/tinymist-query/src/fixtures/code_action/path_and_equation.typ
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": " ",
"range": "0:1:0:1"
},
{
"insertTextFormat": 1,
"newText": " ",
"range": "0:38:0:38"
}
@ -42,10 +45,12 @@ input_file: crates/tinymist-query/src/fixtures/code_action/path_and_equation.typ
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": "\n",
"range": "0:1:0:1"
},
{
"insertTextFormat": 1,
"newText": "\n",
"range": "0:38:0:38"
}

View file

@ -1,6 +1,6 @@
---
source: crates/tinymist-query/src/code_action.rs
description: "Code Action on rt \"base.t||yp\"/* rang)"
description: "Code Action on rt \"base.t||yp\"/* rang"
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/code_action/path_import.typ
---
@ -10,6 +10,7 @@ input_file: crates/tinymist-query/src/fixtures/code_action/path_import.typ
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": "\"/base.typ\"",
"range": "0:8:0:18"
}

View file

@ -1,6 +1,6 @@
---
source: crates/tinymist-query/src/code_action.rs
description: "Code Action on n(\"base.js||on\" /* ran)"
description: "Code Action on n(\"base.js||on\" /* ran"
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/code_action/path_json.typ
---
@ -10,6 +10,7 @@ input_file: crates/tinymist-query/src/fixtures/code_action/path_json.typ
"changes": {
"s1.typ": [
{
"insertTextFormat": 1,
"newText": "\"/base.json\"",
"range": "0:6:0:17"
}

View file

@ -250,7 +250,7 @@ mod tests {
.join("\n");
with_settings!({
description => format!("Jump cursor on {})", make_range_annoation(&source)),
description => format!("Jump cursor on {})", make_range_annotation(&source)),
}, {
assert_snapshot!(results);
})

View file

@ -297,7 +297,7 @@ mod polymorphic {
DocumentLink(Option<Vec<DocumentLink>>),
DocumentHighlight(Option<Vec<DocumentHighlight>>),
ColorPresentation(Option<Vec<ColorPresentation>>),
CodeAction(Option<Vec<CodeActionOrCommand>>),
CodeAction(Option<Vec<CodeAction>>),
CodeLens(Option<Vec<CodeLens>>),
Completion(Option<CompletionList>),
SignatureHelp(Option<SignatureHelp>),

View file

@ -7,14 +7,14 @@ pub use std::sync::{Arc, LazyLock, OnceLock};
pub use ecow::{eco_vec, EcoVec};
pub use itertools::Itertools;
pub use lsp_types::{
request::GotoDeclarationResponse, CodeAction, CodeActionKind, CodeActionOrCommand, CodeLens,
ColorInformation, ColorPresentation, Diagnostic, DiagnosticRelatedInformation,
DiagnosticSeverity, DocumentHighlight, DocumentLink, DocumentSymbol, DocumentSymbolResponse,
Documentation, FoldingRange, GotoDefinitionResponse, Hover, HoverContents, InlayHint,
Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind,
ParameterInformation, Position as LspPosition, PrepareRenameResponse, SelectionRange,
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureHelp, SignatureInformation, SymbolInformation, TextEdit, Url, WorkspaceEdit,
request::GotoDeclarationResponse, CodeActionKind, CodeLens, ColorInformation,
ColorPresentation, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity,
DocumentHighlight, DocumentLink, DocumentSymbol, DocumentSymbolResponse, Documentation,
FoldingRange, GotoDefinitionResponse, Hover, HoverContents, InlayHint, Location as LspLocation,
LocationLink, MarkedString, MarkupContent, MarkupKind, ParameterInformation,
Position as LspPosition, PrepareRenameResponse, SelectionRange, SemanticTokens,
SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp,
SignatureInformation, SymbolInformation, TextEdit, Url, WorkspaceEdit,
};
pub use serde_json::Value as JsonValue;
pub use tinymist_project::LspComputeGraph;
@ -29,6 +29,7 @@ pub use typst::World;
pub use typst_shim::syntax::LinkedNodeExt;
pub use crate::analysis::{Definition, LocalContext};
pub use crate::code_action::proto::*;
pub use crate::docs::DefDocs;
pub use crate::lsp_typst_boundary::{
path_to_url, to_lsp_position, to_lsp_range, to_typst_position, to_typst_range, LspRange,

View file

@ -141,7 +141,7 @@ mod tests {
fn test() {
snapshot_testing("signature_help", &|ctx, path| {
let source = ctx.source_by_path(&path).unwrap();
let (position, anno) = make_pos_annoation(&source);
let (position, anno) = make_pos_annotation(&source);
let request = SignatureHelpRequest { path, position };

View file

@ -275,7 +275,7 @@ fn match_by_pos(mut n: LinkedNode, prev: bool, ident: bool) -> usize {
n.offset()
}
pub fn make_pos_annoation(source: &Source) -> (LspPosition, String) {
pub fn make_pos_annotation(source: &Source) -> (LspPosition, String) {
let pos = find_test_typst_pos(source);
let range_before = pos.saturating_sub(10)..pos;
let range_after = pos..pos.saturating_add(10).min(source.text().len());
@ -287,7 +287,7 @@ pub fn make_pos_annoation(source: &Source) -> (LspPosition, String) {
(pos, format!("{window_before}|{window_after}"))
}
pub fn make_range_annoation(source: &Source) -> String {
pub fn make_range_annotation(source: &Source) -> String {
let range = find_test_range_(source);
let range_before = range.start.saturating_sub(10)..range.start;
let range_window = range.clone();