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} {{
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} -> // ... pet.{variant_Cat} -> // ...
{variant_Dog} -> // ... {variant_Dog} -> // ...
}} }}
}} }}
You must import its constructors explicitly to use them, or prefix them with the module's name.
"# "#
, 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(|| {
if TypeConstructor::might_be(name) {
Error::UnknownTypeConstructor {
location: *location, location: *location,
name: name.to_string(), name: name.to_string(),
variables: self.environment.local_value_names(), 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 {