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

@ -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(),
}
}
}