Merge pull request #1107 from aiken-lang/lsp-fixes

This commit is contained in:
Matthias Benkort 2025-02-23 01:14:16 +01:00 committed by GitHub
commit 586f848929
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 173 additions and 50 deletions

View File

@ -1,5 +1,15 @@
# Changelog # Changelog
## 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
## v1.1.14 - 2025-02-21 ## v1.1.14 - 2025-02-21
### Changed ### Changed

View File

@ -1357,6 +1357,10 @@ impl TypeConstructor {
public: true, public: true,
} }
} }
pub fn might_be(name: &str) -> bool {
name.chars().next().unwrap().is_uppercase()
}
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

View File

@ -898,7 +898,7 @@ Perhaps, try the following:
#[diagnostic(code("unknown::type_constructor"))] #[diagnostic(code("unknown::type_constructor"))]
#[diagnostic(help( #[diagnostic(help(
"{}", "{}",
suggest_neighbor(name, constructors.iter(), "Did you forget to import it?") suggest_neighbor(name, constructors.iter(), &suggest_import_constructor())
))] ))]
UnknownTypeConstructor { UnknownTypeConstructor {
#[label("unknown constructor")] #[label("unknown constructor")]
@ -914,11 +914,7 @@ Perhaps, try the following:
suggest_neighbor( suggest_neighbor(
name, name,
variables.iter(), variables.iter(),
&if name.chars().next().unwrap().is_uppercase() { "Did you forget to import it?",
suggest_import_constructor()
} else {
"Did you forget to import it?".to_string()
}
) )
))] ))]
UnknownVariable { UnknownVariable {
@ -1527,29 +1523,23 @@ fn suggest_import_constructor() -> String {
Data-type constructors are not automatically imported, even if their type is imported. So, if a module 'aiken/pet' defines the following type: Data-type constructors are not automatically imported, even if their type is imported. So, if a module 'aiken/pet' defines the following type:
aiken/pet.ak aiken/pet.ak ==> foo.ak
{keyword_pub} {keyword_type} {type_Pet} {{ {keyword_pub} {keyword_type} {type_Pet} {{ {keyword_use} aiken/pet.{{{type_Pet}, {variant_Dog}}}
{variant_Cat} {variant_Cat}
{variant_Dog} {variant_Dog} {keyword_fn} foo(pet : {type_Pet}) {{
}} }} {keyword_when} pet {keyword_is} {{
pet.{variant_Cat} -> // ...
{variant_Dog} -> // ...
}}
}}
You must import its constructors explicitly to use them, or prefix them with the module's name. You must import its constructors explicitly to use them, or prefix them with the module's name.
foo.ak
{keyword_use} aiken/pet.{{{type_Pet}, {variant_Dog}}}
{keyword_fn} foo(pet : {type_Pet}) {{
{keyword_when} pet {keyword_is} {{
pet.{variant_Cat} -> // ...
{variant_Dog} -> // ...
}}
}}
"# "#
, keyword_fn = "fn".if_supports_color(Stdout, |s| s.yellow()) , keyword_fn = "fn".if_supports_color(Stdout, |s| s.yellow())
, keyword_is = "is".if_supports_color(Stdout, |s| s.yellow()) , keyword_is = "is".if_supports_color(Stdout, |s| s.yellow())
, keyword_pub = "pub".if_supports_color(Stdout, |s| s.bright_blue()) , keyword_pub = "pub".if_supports_color(Stdout, |s| s.bright_purple())
, keyword_type = "type".if_supports_color(Stdout, |s| s.bright_blue()) , keyword_type = "type".if_supports_color(Stdout, |s| s.purple())
, keyword_use = "use".if_supports_color(Stdout, |s| s.bright_blue()) , keyword_use = "use".if_supports_color(Stdout, |s| s.bright_purple())
, keyword_when = "when".if_supports_color(Stdout, |s| s.yellow()) , keyword_when = "when".if_supports_color(Stdout, |s| s.yellow())
, type_Pet = "Pet" , type_Pet = "Pet"
.if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bright_blue())

View File

@ -21,7 +21,9 @@ use crate::{
expr::{FnStyle, TypedExpr, UntypedExpr}, expr::{FnStyle, TypedExpr, UntypedExpr},
format, format,
parser::token::Base, parser::token::Base,
tipo::{fields::FieldMap, DefaultFunction, ModuleKind, PatternConstructor, TypeVar}, tipo::{
fields::FieldMap, DefaultFunction, ModuleKind, PatternConstructor, TypeConstructor, TypeVar,
},
IdGenerator, IdGenerator,
}; };
use std::{ use std::{
@ -2481,10 +2483,30 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
self.environment self.environment
.get_variable(name) .get_variable(name)
.cloned() .cloned()
.ok_or_else(|| Error::UnknownVariable { .ok_or_else(|| {
location: *location, if TypeConstructor::might_be(name) {
name: name.to_string(), Error::UnknownTypeConstructor {
variables: self.environment.local_value_names(), location: *location,
name: name.to_string(),
constructors: self
.environment
.local_value_names()
.into_iter()
.filter(|s| TypeConstructor::might_be(s))
.collect::<Vec<_>>(),
}
} else {
Error::UnknownVariable {
location: *location,
name: name.to_string(),
variables: self
.environment
.local_value_names()
.into_iter()
.filter(|s| !TypeConstructor::might_be(s))
.collect::<Vec<_>>(),
}
}
})?; })?;
if let ValueConstructorVariant::ModuleFn { name: fn_name, .. } = if let ValueConstructorVariant::ModuleFn { name: fn_name, .. } =

View File

@ -14,7 +14,10 @@ pub struct ParsedDocument {
source_code: String, source_code: String,
} }
pub type AnnotatedEdit = (String, lsp_types::TextEdit); pub enum AnnotatedEdit {
SimpleEdit(String, lsp_types::TextEdit),
CombinedEdits(String, Vec<lsp_types::TextEdit>),
}
/// Parse the target document as an 'UntypedModule' alongside its line numbers. This is useful in /// 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. /// case we need to manipulate the AST for a quickfix.
@ -166,20 +169,36 @@ impl ParsedDocument {
let new_text = String::new(); let new_text = String::new();
( AnnotatedEdit::SimpleEdit(
"Remove redundant import".to_string(), "Remove redundant import".to_string(),
lsp_types::TextEdit { range, new_text }, lsp_types::TextEdit { range, new_text },
) )
} }
pub fn use_qualified(
module: &str,
unqualified: &str,
range: &lsp_types::Range,
) -> Option<AnnotatedEdit> {
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( fn insert_qualified_before(
&self, &self,
import: &CheckedModule, import: &CheckedModule,
unqualified: &str, unqualified: &str,
location: Span, location: Span,
) -> AnnotatedEdit { ) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name); let title = format!("Import '{}' from {}", unqualified, import.name);
( AnnotatedEdit::SimpleEdit(
title, title,
insert_text( insert_text(
location.start, location.start,
@ -195,8 +214,8 @@ impl ParsedDocument {
unqualified: &str, unqualified: &str,
location: Span, location: Span,
) -> AnnotatedEdit { ) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name); let title = format!("Import '{}' from {}", unqualified, import.name);
( AnnotatedEdit::SimpleEdit(
title, title,
insert_text( insert_text(
location.end, location.end,
@ -212,8 +231,8 @@ impl ParsedDocument {
unqualified: &str, unqualified: &str,
location: Span, location: Span,
) -> AnnotatedEdit { ) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name); let title = format!("Import '{}' from {}", unqualified, import.name);
( AnnotatedEdit::SimpleEdit(
title, title,
insert_text( insert_text(
location.end, 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, title,
match location { match location {
None => insert_text(0, &self.line_numbers, format!("{import_line}\n")), None => insert_text(0, &self.line_numbers, format!("{import_line}\n")),

View File

@ -9,7 +9,7 @@ mod edits;
pub mod error; pub mod error;
mod quickfix; mod quickfix;
pub mod server; pub mod server;
mod utils; pub mod utils;
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub fn start() -> Result<(), Error> { pub fn start() -> Result<(), Error> {

View File

@ -2,6 +2,7 @@ use crate::{
edits::{self, AnnotatedEdit, ParsedDocument}, edits::{self, AnnotatedEdit, ParsedDocument},
server::lsp_project::LspProject, server::lsp_project::LspProject,
}; };
use aiken_project::module::CheckedModule;
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
const UNKNOWN_VARIABLE: &str = "aiken::check::unknown::variable"; const UNKNOWN_VARIABLE: &str = "aiken::check::unknown::variable";
@ -94,7 +95,12 @@ pub fn quickfix(
&mut actions, &mut actions,
text_document, text_document,
diagnostic, 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( Quickfix::UnknownModule(diagnostic) => each_as_distinct_action(
@ -107,7 +113,12 @@ pub fn quickfix(
&mut actions, &mut actions,
text_document, text_document,
diagnostic, 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( Quickfix::UnusedImports(diagnostics) => as_single_action(
&mut actions, &mut actions,
@ -152,10 +163,19 @@ fn each_as_distinct_action(
diagnostic: &lsp_types::Diagnostic, diagnostic: &lsp_types::Diagnostic,
edits: Vec<AnnotatedEdit>, edits: Vec<AnnotatedEdit>,
) { ) {
for (title, edit) in edits.into_iter() { for edit in edits.into_iter() {
let mut changes = HashMap::new(); 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 { actions.push(lsp_types::CodeAction {
title, title,
@ -185,7 +205,13 @@ fn as_single_action(
changes.insert( changes.insert(
text_document.uri.clone(), 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 { actions.push(lsp_types::CodeAction {
@ -207,6 +233,7 @@ fn as_single_action(
fn unknown_identifier( fn unknown_identifier(
compiler: &LspProject, compiler: &LspProject,
parsed_document: &ParsedDocument, parsed_document: &ParsedDocument,
range: &lsp_types::Range,
data: Option<&serde_json::Value>, data: Option<&serde_json::Value>,
) -> Vec<AnnotatedEdit> { ) -> Vec<AnnotatedEdit> {
let mut edits = Vec::new(); let mut edits = Vec::new();
@ -217,6 +244,10 @@ fn unknown_identifier(
if let Some(edit) = parsed_document.import(&module, Some(var_name)) { if let Some(edit) = parsed_document.import(&module, Some(var_name)) {
edits.push(edit) 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( fn unknown_constructor(
compiler: &LspProject, compiler: &LspProject,
parsed_document: &ParsedDocument, parsed_document: &ParsedDocument,
range: &lsp_types::Range,
data: Option<&serde_json::Value>, data: Option<&serde_json::Value>,
) -> Vec<AnnotatedEdit> { ) -> Vec<AnnotatedEdit> {
let mut edits = Vec::new(); let mut edits = Vec::new();
@ -237,6 +269,12 @@ fn unknown_constructor(
if let Some(edit) = parsed_document.import(&module, Some(constructor_name)) { if let Some(edit) = parsed_document.import(&module, Some(constructor_name)) {
edits.push(edit) 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 edits
} }
fn suggest_qualified(
parsed_document: &ParsedDocument,
module: &CheckedModule,
identifier: &str,
range: &lsp_types::Range,
) -> Option<AnnotatedEdit> {
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( fn unknown_module(
compiler: &LspProject, compiler: &LspProject,
parsed_document: &ParsedDocument, parsed_document: &ParsedDocument,
@ -298,7 +363,7 @@ fn utf8_byte_array_is_hex_string(diagnostic: &lsp_types::Diagnostic) -> Vec<Anno
let mut edits = Vec::new(); let mut edits = Vec::new();
if let Some(serde_json::Value::String(ref value)) = diagnostic.data.as_ref() { if let Some(serde_json::Value::String(ref value)) = diagnostic.data.as_ref() {
edits.push(( edits.push(AnnotatedEdit::SimpleEdit(
"Prefix with #".to_string(), "Prefix with #".to_string(),
lsp_types::TextEdit { lsp_types::TextEdit {
range: diagnostic.range, range: diagnostic.range,
@ -311,7 +376,7 @@ fn utf8_byte_array_is_hex_string(diagnostic: &lsp_types::Diagnostic) -> Vec<Anno
} }
fn use_let(diagnostic: &lsp_types::Diagnostic) -> Vec<AnnotatedEdit> { fn use_let(diagnostic: &lsp_types::Diagnostic) -> Vec<AnnotatedEdit> {
vec![( vec![AnnotatedEdit::SimpleEdit(
"Use 'let' instead of 'expect'".to_string(), "Use 'let' instead of 'expect'".to_string(),
lsp_types::TextEdit { lsp_types::TextEdit {
range: diagnostic.range, range: diagnostic.range,
@ -324,7 +389,7 @@ fn unused_record_fields(diagnostic: &lsp_types::Diagnostic) -> Vec<AnnotatedEdit
let mut edits = Vec::new(); let mut edits = Vec::new();
if let Some(serde_json::Value::String(new_text)) = diagnostic.data.as_ref() { if let Some(serde_json::Value::String(new_text)) = diagnostic.data.as_ref() {
edits.push(( edits.push(AnnotatedEdit::SimpleEdit(
"Destructure using named fields".to_string(), "Destructure using named fields".to_string(),
lsp_types::TextEdit { lsp_types::TextEdit {
range: diagnostic.range, range: diagnostic.range,

View File

@ -638,7 +638,7 @@ impl Server {
self.send_work_done_notification( self.send_work_done_notification(
connection, connection,
lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin { lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title: "Compiling Aiken".into(), title: "Compiling Aiken project".into(),
cancellable: Some(false), cancellable: Some(false),
message: None, message: None,
percentage: None, percentage: None,

View File

@ -1,13 +1,26 @@
use crate::error::Error; use crate::error::Error;
use aiken_lang::{ast::Span, line_numbers::LineNumbers}; use aiken_lang::{ast::Span, line_numbers::LineNumbers};
use itertools::Itertools; use itertools::Itertools;
use lsp_types::TextEdit; use lsp_types::{notification::Notification, TextEdit};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use urlencoding::decode; use urlencoding::decode;
pub const COMPILING_PROGRESS_TOKEN: &str = "compiling-aiken"; pub const COMPILING_PROGRESS_TOKEN: &str = "compiling-aiken";
pub const CREATE_COMPILING_PROGRESS_TOKEN: &str = "create-compiling-progress-token"; pub const CREATE_COMPILING_PROGRESS_TOKEN: &str = "create-compiling-progress-token";
/// Trace some information from the server.
pub fn debug(connection: &lsp_server::Connection, message: impl serde::ser::Serialize) {
connection
.sender
.send(lsp_server::Message::Notification(
lsp_server::Notification {
method: lsp_types::notification::LogTrace::METHOD.to_string(),
params: serde_json::json! {{ "message": message }},
},
))
.expect("failed to send notification");
}
pub fn text_edit_replace(new_text: String) -> TextEdit { pub fn text_edit_replace(new_text: String) -> TextEdit {
TextEdit { TextEdit {
range: lsp_types::Range { range: lsp_types::Range {