diff --git a/CHANGELOG.md b/CHANGELOG.md index af839752..f9fbc613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v1.1.15 - UNRELEASED +### Added + +- **aiken-lsp**: an additional code action to use constructors or identifiers from qualified imports is now offered on missing constructor or identifier. @KtorZ + ### Changed - **aiken-lang**: fixed `UnknownTypeConstructor` wrongly reported as `UnknownVariable` (then messing up with LSP quickfix suggestions). @KtorZ diff --git a/crates/aiken-lsp/src/edits.rs b/crates/aiken-lsp/src/edits.rs index 0b035309..90d2cb74 100644 --- a/crates/aiken-lsp/src/edits.rs +++ b/crates/aiken-lsp/src/edits.rs @@ -14,7 +14,10 @@ pub struct ParsedDocument { source_code: String, } -pub type AnnotatedEdit = (String, lsp_types::TextEdit); +pub enum AnnotatedEdit { + SimpleEdit(String, lsp_types::TextEdit), + CombinedEdits(String, Vec), +} /// Parse the target document as an 'UntypedModule' alongside its line numbers. This is useful in /// case we need to manipulate the AST for a quickfix. @@ -166,20 +169,36 @@ impl ParsedDocument { let new_text = String::new(); - ( + AnnotatedEdit::SimpleEdit( "Remove redundant import".to_string(), lsp_types::TextEdit { range, new_text }, ) } + pub fn use_qualified( + module: &str, + unqualified: &str, + range: &lsp_types::Range, + ) -> Option { + let title = format!("Use qualified from {}", module); + let suffix = module.split("/").last()?; + Some(AnnotatedEdit::SimpleEdit( + title, + lsp_types::TextEdit { + range: *range, + new_text: format!("{suffix}.{unqualified}"), + }, + )) + } + fn insert_qualified_before( &self, import: &CheckedModule, unqualified: &str, location: Span, ) -> AnnotatedEdit { - let title = format!("Use '{}' from {}", unqualified, import.name); - ( + let title = format!("Import '{}' from {}", unqualified, import.name); + AnnotatedEdit::SimpleEdit( title, insert_text( location.start, @@ -195,8 +214,8 @@ impl ParsedDocument { unqualified: &str, location: Span, ) -> AnnotatedEdit { - let title = format!("Use '{}' from {}", unqualified, import.name); - ( + let title = format!("Import '{}' from {}", unqualified, import.name); + AnnotatedEdit::SimpleEdit( title, insert_text( location.end, @@ -212,8 +231,8 @@ impl ParsedDocument { unqualified: &str, location: Span, ) -> AnnotatedEdit { - let title = format!("Use '{}' from {}", unqualified, import.name); - ( + let title = format!("Import '{}' from {}", unqualified, import.name); + AnnotatedEdit::SimpleEdit( title, insert_text( location.end, @@ -238,9 +257,9 @@ impl ParsedDocument { } ); - let title = format!("Add new import line: {import_line}"); + let title = format!("Add line: '{import_line}'"); - ( + AnnotatedEdit::SimpleEdit( title, match location { None => insert_text(0, &self.line_numbers, format!("{import_line}\n")), diff --git a/crates/aiken-lsp/src/quickfix.rs b/crates/aiken-lsp/src/quickfix.rs index ab457608..1347e0bf 100644 --- a/crates/aiken-lsp/src/quickfix.rs +++ b/crates/aiken-lsp/src/quickfix.rs @@ -2,6 +2,7 @@ use crate::{ edits::{self, AnnotatedEdit, ParsedDocument}, server::lsp_project::LspProject, }; +use aiken_project::module::CheckedModule; use std::{collections::HashMap, str::FromStr}; const UNKNOWN_VARIABLE: &str = "aiken::check::unknown::variable"; @@ -94,7 +95,12 @@ pub fn quickfix( &mut actions, text_document, diagnostic, - unknown_identifier(compiler, parsed_document, diagnostic.data.as_ref()), + unknown_identifier( + compiler, + parsed_document, + &diagnostic.range, + diagnostic.data.as_ref(), + ), ); } Quickfix::UnknownModule(diagnostic) => each_as_distinct_action( @@ -107,7 +113,12 @@ pub fn quickfix( &mut actions, text_document, diagnostic, - unknown_constructor(compiler, parsed_document, diagnostic.data.as_ref()), + unknown_constructor( + compiler, + parsed_document, + &diagnostic.range, + diagnostic.data.as_ref(), + ), ), Quickfix::UnusedImports(diagnostics) => as_single_action( &mut actions, @@ -152,10 +163,19 @@ fn each_as_distinct_action( diagnostic: &lsp_types::Diagnostic, edits: Vec, ) { - for (title, edit) in edits.into_iter() { + for edit in edits.into_iter() { let mut changes = HashMap::new(); - changes.insert(text_document.uri.clone(), vec![edit]); + let title = match edit { + AnnotatedEdit::SimpleEdit(title, one) => { + changes.insert(text_document.uri.clone(), vec![one]); + title + } + AnnotatedEdit::CombinedEdits(title, many) => { + changes.insert(text_document.uri.clone(), many); + title + } + }; actions.push(lsp_types::CodeAction { title, @@ -185,7 +205,13 @@ fn as_single_action( changes.insert( text_document.uri.clone(), - edits.into_iter().map(|(_, b)| b).collect(), + edits + .into_iter() + .flat_map(|edit| match edit { + AnnotatedEdit::SimpleEdit(_, one) => vec![one], + AnnotatedEdit::CombinedEdits(_, many) => many, + }) + .collect(), ); actions.push(lsp_types::CodeAction { @@ -207,6 +233,7 @@ fn as_single_action( fn unknown_identifier( compiler: &LspProject, parsed_document: &ParsedDocument, + range: &lsp_types::Range, data: Option<&serde_json::Value>, ) -> Vec { let mut edits = Vec::new(); @@ -217,6 +244,10 @@ fn unknown_identifier( if let Some(edit) = parsed_document.import(&module, Some(var_name)) { edits.push(edit) } + + if let Some(edit) = suggest_qualified(parsed_document, &module, var_name, range) { + edits.push(edit) + } } } } @@ -227,6 +258,7 @@ fn unknown_identifier( fn unknown_constructor( compiler: &LspProject, parsed_document: &ParsedDocument, + range: &lsp_types::Range, data: Option<&serde_json::Value>, ) -> Vec { let mut edits = Vec::new(); @@ -237,6 +269,12 @@ fn unknown_constructor( if let Some(edit) = parsed_document.import(&module, Some(constructor_name)) { edits.push(edit) } + + if let Some(edit) = + suggest_qualified(parsed_document, &module, constructor_name, range) + { + edits.push(edit) + } } } } @@ -244,6 +282,33 @@ fn unknown_constructor( edits } +fn suggest_qualified( + parsed_document: &ParsedDocument, + module: &CheckedModule, + identifier: &str, + range: &lsp_types::Range, +) -> Option { + if let Some(AnnotatedEdit::SimpleEdit(use_qualified_title, use_qualified)) = + ParsedDocument::use_qualified(&module.name, identifier, range) + { + if let Some(AnnotatedEdit::SimpleEdit(_, add_new_line)) = + parsed_document.import(module, None) + { + return Some(AnnotatedEdit::CombinedEdits( + use_qualified_title, + vec![add_new_line, use_qualified], + )); + } else { + return Some(AnnotatedEdit::SimpleEdit( + use_qualified_title, + use_qualified, + )); + } + } + + None +} + fn unknown_module( compiler: &LspProject, parsed_document: &ParsedDocument, @@ -298,7 +363,7 @@ fn utf8_byte_array_is_hex_string(diagnostic: &lsp_types::Diagnostic) -> Vec Vec Vec { - vec![( + vec![AnnotatedEdit::SimpleEdit( "Use 'let' instead of 'expect'".to_string(), lsp_types::TextEdit { range: diagnostic.range, @@ -324,7 +389,7 @@ fn unused_record_fields(diagnostic: &lsp_types::Diagnostic) -> Vec