diff --git a/kclvm/Cargo.lock b/kclvm/Cargo.lock index 40c9cde13..a77ec1f1e 100644 --- a/kclvm/Cargo.lock +++ b/kclvm/Cargo.lock @@ -1680,6 +1680,7 @@ dependencies = [ "indexmap 1.9.3", "kclvm-runtime", "kclvm-span", + "serde_json", "termize", "tracing", ] diff --git a/kclvm/error/Cargo.toml b/kclvm/error/Cargo.toml index e4e12eb76..80a5dba30 100644 --- a/kclvm/error/Cargo.toml +++ b/kclvm/error/Cargo.toml @@ -19,3 +19,4 @@ atty = "0.2" annotate-snippets = { version = "0.9.2", default-features = false, features = ["color"] } termize = "0.1.1" indexmap = "1.0" +serde_json = "1.0.86" diff --git a/kclvm/error/src/diagnostic.rs b/kclvm/error/src/diagnostic.rs index bbcb7f7e8..d2a203d6b 100644 --- a/kclvm/error/src/diagnostic.rs +++ b/kclvm/error/src/diagnostic.rs @@ -106,7 +106,7 @@ impl Diagnostic { note: Option<&str>, range: Range, code: Option, - suggested_replacement: Option, + suggestions: Option>, ) -> Self { Diagnostic { level, @@ -114,8 +114,8 @@ impl Diagnostic { range, style: Style::LineAndColumn, message: message.to_string(), - note: note.map(|s| s.to_string()), - suggested_replacement, + note: note.map(String::from), + suggested_replacement: suggestions.and_then(|v| v.into_iter().next()), }], code, } diff --git a/kclvm/error/src/lib.rs b/kclvm/error/src/lib.rs index 5af39db81..c61935635 100644 --- a/kclvm/error/src/lib.rs +++ b/kclvm/error/src/lib.rs @@ -126,13 +126,22 @@ impl Handler { /// Construct a type error and put it into the handler diagnostic buffer pub fn add_compile_error(&mut self, msg: &str, range: Range) -> &mut Self { + self.add_compile_error_with_suggestions(msg, range, None) + } + + pub fn add_compile_error_with_suggestions( + &mut self, + msg: &str, + range: Range, + suggestions: Option>, + ) -> &mut Self { let diag = Diagnostic::new_with_code( Level::Error, msg, None, range, Some(DiagnosticId::Error(E2L23.kind)), - None, + suggestions, ); self.add_diagnostic(diag); diff --git a/kclvm/sema/src/resolver/scope.rs b/kclvm/sema/src/resolver/scope.rs index dcb7a3623..da8ef92ff 100644 --- a/kclvm/sema/src/resolver/scope.rs +++ b/kclvm/sema/src/resolver/scope.rs @@ -432,13 +432,14 @@ impl<'ctx> Resolver<'ctx> { if suggs.len() > 0 { suggestion = format!(", did you mean '{:?}'?", suggs); } - self.handler.add_compile_error( + self.handler.add_compile_error_with_suggestions( &format!( "name '{}' is not defined{}", name.replace('@', ""), suggestion ), range, + Some(suggs.clone()), ); self.any_ty() } diff --git a/kclvm/tools/src/LSP/src/quick_fix.rs b/kclvm/tools/src/LSP/src/quick_fix.rs index 95f54dabd..0c026783c 100644 --- a/kclvm/tools/src/LSP/src/quick_fix.rs +++ b/kclvm/tools/src/LSP/src/quick_fix.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; -use kclvm_error::{DiagnosticId, WarningKind}; +use kclvm_error::{DiagnosticId, ErrorKind, WarningKind}; use lsp_types::{ CodeAction, CodeActionKind, CodeActionOrCommand, Diagnostic, NumberOrString, TextEdit, Url, }; +use serde_json::Value; pub(crate) fn quick_fix(uri: &Url, diags: &Vec) -> Vec { let mut code_actions: Vec = vec![]; @@ -11,7 +12,38 @@ pub(crate) fn quick_fix(uri: &Url, diags: &Vec) -> Vec continue, + DiagnosticId::Error(error) => match error { + ErrorKind::CompileError => { + if let Some(replacement_text) = + extract_suggested_replacement(&diag.data) + { + let mut changes = HashMap::new(); + changes.insert( + uri.clone(), + vec![TextEdit { + range: diag.range, + new_text: replacement_text.clone(), + }], + ); + code_actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: format!( + "a local variable with a similar name exists: `{}`", + replacement_text + ), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diag.clone()]), + edit: Some(lsp_types::WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + ..Default::default() + })) + } else { + continue; + } + } + _ => continue, + }, DiagnosticId::Warning(warn) => match warn { WarningKind::UnusedImportWarning => { let mut changes = HashMap::new(); @@ -63,6 +95,21 @@ pub(crate) fn quick_fix(uri: &Url, diags: &Vec) -> Vec) -> Option { + data.as_ref().and_then(|data| match data { + Value::Object(obj) => obj.get("suggested_replacement").and_then(|val| match val { + Value::String(s) => Some(s.clone()), + Value::Array(arr) if !arr.is_empty() => arr + .iter() + .filter_map(|v| v.as_str()) + .next() + .map(String::from), + _ => None, + }), + _ => None, + }) +} + pub(crate) fn conver_code_to_kcl_diag_id(code: &NumberOrString) -> Option { match code { NumberOrString::Number(_) => None, @@ -70,6 +117,7 @@ pub(crate) fn conver_code_to_kcl_diag_id(code: &NumberOrString) -> Option Some(DiagnosticId::Warning(WarningKind::CompilerWarning)), "UnusedImportWarning" => Some(DiagnosticId::Warning(WarningKind::UnusedImportWarning)), "ReimportWarning" => Some(DiagnosticId::Warning(WarningKind::ReimportWarning)), + "CompileError" => Some(DiagnosticId::Error(ErrorKind::CompileError)), "ImportPositionWarning" => { Some(DiagnosticId::Warning(WarningKind::ImportPositionWarning)) } diff --git a/kclvm/tools/src/LSP/src/test_data/diagnostics.k b/kclvm/tools/src/LSP/src/test_data/diagnostics.k index eeb396a83..f24dac378 100644 --- a/kclvm/tools/src/LSP/src/test_data/diagnostics.k +++ b/kclvm/tools/src/LSP/src/test_data/diagnostics.k @@ -7,3 +7,5 @@ c: Person = Person { d = 1 d = 2 +number = 2 +count = nu \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index 04e1ce749..b265a98b3 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -140,6 +140,7 @@ fn build_lsp_diag( severity: Option, related_info: Vec<(String, (u32, u32, u32, u32), String)>, code: Option, + data: Option, ) -> Diagnostic { let related_information = if related_info.is_empty() { None @@ -184,7 +185,7 @@ fn build_lsp_diag( message, related_information, tags: None, - data: None, + data, } } @@ -201,6 +202,7 @@ fn build_expect_diags() -> Vec { Some(DiagnosticSeverity::ERROR), vec![], Some(NumberOrString::String("InvalidSyntax".to_string())), + None, ), build_lsp_diag( (0, 0, 0, 10), @@ -208,6 +210,7 @@ fn build_expect_diags() -> Vec { Some(DiagnosticSeverity::ERROR), vec![], Some(NumberOrString::String("CannotFindModule".to_string())), + None, ), build_lsp_diag( (0, 0, 0, 10), @@ -218,6 +221,7 @@ fn build_expect_diags() -> Vec { Some(DiagnosticSeverity::ERROR), vec![], Some(NumberOrString::String("CannotFindModule".to_string())), + None, ), build_lsp_diag( (8, 0, 8, 1), @@ -229,6 +233,7 @@ fn build_expect_diags() -> Vec { "The variable 'd' is declared here".to_string(), )], Some(NumberOrString::String("ImmutableError".to_string())), + None, ), build_lsp_diag( (7, 0, 7, 1), @@ -240,6 +245,7 @@ fn build_expect_diags() -> Vec { "Can not change the value of 'd', because it was declared immutable".to_string(), )], Some(NumberOrString::String("ImmutableError".to_string())), + None, ), build_lsp_diag( (2, 0, 2, 1), @@ -247,6 +253,15 @@ fn build_expect_diags() -> Vec { Some(DiagnosticSeverity::ERROR), vec![], Some(NumberOrString::String("TypeError".to_string())), + None, + ), + build_lsp_diag( + (10, 8, 10, 10), + "name 'nu' is not defined, did you mean '[\"number\", \"n\", \"num\"]'?".to_string(), + Some(DiagnosticSeverity::ERROR), + vec![], + Some(NumberOrString::String("CompileError".to_string())), + Some(serde_json::json!({ "suggested_replacement": ["number"] })), ), build_lsp_diag( (0, 0, 0, 10), @@ -254,6 +269,7 @@ fn build_expect_diags() -> Vec { Some(DiagnosticSeverity::WARNING), vec![], Some(NumberOrString::String("UnusedImportWarning".to_string())), + None, ), ]; expected_diags diff --git a/kclvm/tools/src/LSP/src/to_lsp.rs b/kclvm/tools/src/LSP/src/to_lsp.rs index 87e8336d4..29da9fc28 100644 --- a/kclvm/tools/src/LSP/src/to_lsp.rs +++ b/kclvm/tools/src/LSP/src/to_lsp.rs @@ -5,6 +5,7 @@ use kclvm_error::Message; use kclvm_error::Position as KCLPos; use lsp_types::*; use ra_ap_vfs::FileId; +use serde_json::json; use crate::state::LanguageServerSnapshot; use std::{ @@ -43,6 +44,12 @@ fn kcl_msg_to_lsp_diags( let start_position = lsp_pos(&range.0); let end_position = lsp_pos(&range.1); + let data = msg + .suggested_replacement + .as_ref() + .filter(|s| !s.is_empty()) + .map(|s| json!({ "suggested_replacement": [s] })); + let related_information = if related_msg.is_empty() { None } else { @@ -72,7 +79,7 @@ fn kcl_msg_to_lsp_diags( message: msg.message.clone(), related_information, tags: None, - data: None, + data, } } @@ -97,12 +104,15 @@ pub fn kcl_diag_to_lsp_diags(diag: &KCLDiagnostic, file_name: &str) -> Vec