Merge pull request #753 from aiken-lang/lsp/quickfix-unknowns
Lsp/quickfix unknowns
This commit is contained in:
commit
d6f74b5932
|
@ -78,6 +78,34 @@ impl TypedModule {
|
||||||
.find_map(|definition| definition.find_node(byte_index))
|
.find_map(|definition| definition.find_node(byte_index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_definition(&self, name: &str) -> bool {
|
||||||
|
self.definitions.iter().any(|def| match def {
|
||||||
|
Definition::Fn(f) => f.public && f.name == name,
|
||||||
|
Definition::TypeAlias(alias) => alias.public && alias.alias == name,
|
||||||
|
Definition::ModuleConstant(cst) => cst.public && cst.name == name,
|
||||||
|
Definition::DataType(t) => t.public && t.name == name,
|
||||||
|
Definition::Use(_) => false,
|
||||||
|
Definition::Test(_) => false,
|
||||||
|
Definition::Validator(_) => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_constructor(&self, name: &str) -> bool {
|
||||||
|
self.definitions.iter().any(|def| match def {
|
||||||
|
Definition::DataType(t) if t.public && !t.opaque => t
|
||||||
|
.constructors
|
||||||
|
.iter()
|
||||||
|
.any(|constructor| constructor.name == name),
|
||||||
|
Definition::DataType(_) => false,
|
||||||
|
Definition::Fn(_) => false,
|
||||||
|
Definition::TypeAlias(_) => false,
|
||||||
|
Definition::ModuleConstant(_) => false,
|
||||||
|
Definition::Use(_) => false,
|
||||||
|
Definition::Test(_) => false,
|
||||||
|
Definition::Validator(_) => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate_module_name(&self) -> Result<(), Error> {
|
pub fn validate_module_name(&self) -> Result<(), Error> {
|
||||||
if self.name == "aiken" || self.name == "aiken/builtin" {
|
if self.name == "aiken" || self.name == "aiken/builtin" {
|
||||||
return Err(Error::ReservedModuleName {
|
return Err(Error::ReservedModuleName {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub trait ExtraData {
|
||||||
|
fn extra_data(&self) -> Option<String>;
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ use std::sync::{
|
||||||
|
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
pub mod builtins;
|
pub mod builtins;
|
||||||
|
pub mod error;
|
||||||
pub mod expr;
|
pub mod expr;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod gen_uplc;
|
pub mod gen_uplc;
|
||||||
|
|
|
@ -309,10 +309,13 @@ impl<'a> Environment<'a> {
|
||||||
location: Span,
|
location: Span,
|
||||||
) -> Result<&ValueConstructor, Error> {
|
) -> Result<&ValueConstructor, Error> {
|
||||||
match module {
|
match module {
|
||||||
None => self.scope.get(name).ok_or_else(|| Error::UnknownVariable {
|
None => self
|
||||||
|
.scope
|
||||||
|
.get(name)
|
||||||
|
.ok_or_else(|| Error::UnknownTypeConstructor {
|
||||||
location,
|
location,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
variables: self.local_value_names(),
|
constructors: self.local_constructor_names(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
|
@ -577,6 +580,14 @@ impl<'a> Environment<'a> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn local_constructor_names(&self) -> Vec<String> {
|
||||||
|
self.scope
|
||||||
|
.keys()
|
||||||
|
.filter(|&t| t.chars().next().unwrap_or_default().is_uppercase())
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn make_type_vars(
|
fn make_type_vars(
|
||||||
&mut self,
|
&mut self,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::Type;
|
use super::Type;
|
||||||
|
use crate::error::ExtraData;
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern},
|
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern},
|
||||||
expr::{self, UntypedExpr},
|
expr::{self, UntypedExpr},
|
||||||
|
@ -798,7 +799,7 @@ Perhaps, try the following:
|
||||||
suggest_neighbor(name, constructors.iter(), "Did you forget to import it?")
|
suggest_neighbor(name, constructors.iter(), "Did you forget to import it?")
|
||||||
))]
|
))]
|
||||||
UnknownTypeConstructor {
|
UnknownTypeConstructor {
|
||||||
#[label]
|
#[label("unknown constructor")]
|
||||||
location: Span,
|
location: Span,
|
||||||
name: String,
|
name: String,
|
||||||
constructors: Vec<String>,
|
constructors: Vec<String>,
|
||||||
|
@ -922,6 +923,64 @@ The best thing to do from here is to remove it."#))]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExtraData for Error {
|
||||||
|
fn extra_data(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Error::CastDataNoAnn { .. }
|
||||||
|
| Error::CouldNotUnify { .. }
|
||||||
|
| Error::CyclicTypeDefinitions { .. }
|
||||||
|
| Error::DuplicateArgument { .. }
|
||||||
|
| Error::DuplicateConstName { .. }
|
||||||
|
| Error::DuplicateField { .. }
|
||||||
|
| Error::DuplicateImport { .. }
|
||||||
|
| Error::DuplicateName { .. }
|
||||||
|
| Error::DuplicateTypeName { .. }
|
||||||
|
| Error::DuplicateVarInPattern { .. }
|
||||||
|
| Error::ExtraVarInAlternativePattern { .. }
|
||||||
|
| Error::FunctionTypeInData { .. }
|
||||||
|
| Error::ImplicitlyDiscardedExpression { .. }
|
||||||
|
| Error::IncorrectFieldsArity { .. }
|
||||||
|
| Error::IncorrectFunctionCallArity { .. }
|
||||||
|
| Error::IncorrectPatternArity { .. }
|
||||||
|
| Error::IncorrectTupleArity { .. }
|
||||||
|
| Error::IncorrectTypeArity { .. }
|
||||||
|
| Error::IncorrectValidatorArity { .. }
|
||||||
|
| Error::KeywordInModuleName { .. }
|
||||||
|
| Error::LastExpressionIsAssignment { .. }
|
||||||
|
| Error::LogicalOpChainMissingExpr { .. }
|
||||||
|
| Error::MissingVarInAlternativePattern { .. }
|
||||||
|
| Error::MultiValidatorEqualArgs { .. }
|
||||||
|
| Error::NonLocalClauseGuardVariable { .. }
|
||||||
|
| Error::NotATuple { .. }
|
||||||
|
| Error::NotExhaustivePatternMatch { .. }
|
||||||
|
| Error::NotFn { .. }
|
||||||
|
| Error::PositionalArgumentAfterLabeled { .. }
|
||||||
|
| Error::PrivateTypeLeak { .. }
|
||||||
|
| Error::RecordAccessUnknownType { .. }
|
||||||
|
| Error::RecordUpdateInvalidConstructor { .. }
|
||||||
|
| Error::RecursiveType { .. }
|
||||||
|
| Error::RedundantMatchClause { .. }
|
||||||
|
| Error::TupleIndexOutOfBound { .. }
|
||||||
|
| Error::UnexpectedLabeledArg { .. }
|
||||||
|
| Error::UnexpectedLabeledArgInPattern { .. }
|
||||||
|
| Error::UnknownLabels { .. }
|
||||||
|
| Error::UnknownModuleField { .. }
|
||||||
|
| Error::UnknownModuleType { .. }
|
||||||
|
| Error::UnknownModuleValue { .. }
|
||||||
|
| Error::UnknownRecordField { .. }
|
||||||
|
| Error::UnnecessarySpreadOperator { .. }
|
||||||
|
| Error::UpdateMultiConstructorType { .. }
|
||||||
|
| Error::ValidatorImported { .. }
|
||||||
|
| Error::ValidatorMustReturnBool { .. } => None,
|
||||||
|
|
||||||
|
Error::UnknownType { name, .. }
|
||||||
|
| Error::UnknownTypeConstructor { name, .. }
|
||||||
|
| Error::UnknownVariable { name, .. }
|
||||||
|
| Error::UnknownModule { name, .. } => Some(name.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn call_situation(mut self) -> Self {
|
pub fn call_situation(mut self) -> Self {
|
||||||
if let Error::UnknownRecordField {
|
if let Error::UnknownRecordField {
|
||||||
|
@ -1522,6 +1581,30 @@ pub enum Warning {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExtraData for Warning {
|
||||||
|
fn extra_data(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Warning::AllFieldsRecordUpdate { .. }
|
||||||
|
| Warning::ImplicitlyDiscardedResult { .. }
|
||||||
|
| Warning::NoFieldsRecordUpdate { .. }
|
||||||
|
| Warning::PubInValidatorModule { .. }
|
||||||
|
| Warning::SingleConstructorExpect { .. }
|
||||||
|
| Warning::SingleWhenClause { .. }
|
||||||
|
| Warning::Todo { .. }
|
||||||
|
| Warning::UnexpectedTypeHole { .. }
|
||||||
|
| Warning::UnusedConstructor { .. }
|
||||||
|
| Warning::UnusedImportedModule { .. }
|
||||||
|
| Warning::UnusedImportedValue { .. }
|
||||||
|
| Warning::UnusedPrivateFunction { .. }
|
||||||
|
| Warning::UnusedPrivateModuleConstant { .. }
|
||||||
|
| Warning::UnusedType { .. }
|
||||||
|
| Warning::UnusedVariable { .. }
|
||||||
|
| Warning::Utf8ByteArrayIsValidHexString { .. }
|
||||||
|
| Warning::ValidatorInLibraryModule { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum UnifyErrorSituation {
|
pub enum UnifyErrorSituation {
|
||||||
/// Clauses in a case expression were found to return different types.
|
/// Clauses in a case expression were found to return different types.
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
use crate::{line_numbers::LineNumbers, utils::span_to_lsp_range};
|
||||||
|
use aiken_lang::ast::{Definition, ModuleKind, Span, UntypedDefinition, Use};
|
||||||
|
use aiken_project::module::CheckedModule;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// A freshly parsed module alongside its line numbers.
|
||||||
|
pub struct ParsedDocument {
|
||||||
|
definitions: Vec<UntypedDefinition>,
|
||||||
|
line_numbers: LineNumbers,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AnnotatedEdit = (String, lsp_types::TextEdit);
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub fn parse_document(document: &lsp_types::TextDocumentIdentifier) -> Option<ParsedDocument> {
|
||||||
|
let file_path = document
|
||||||
|
.uri
|
||||||
|
.to_file_path()
|
||||||
|
.expect("invalid text document uri?");
|
||||||
|
|
||||||
|
let source_code = fs::read_to_string(file_path).ok()?;
|
||||||
|
|
||||||
|
let line_numbers = LineNumbers::new(&source_code);
|
||||||
|
|
||||||
|
// NOTE: The 'ModuleKind' second argument doesn't matter. This is just added to the final
|
||||||
|
// object but has no influence on the parsing.
|
||||||
|
let (untyped_module, _) = aiken_lang::parser::module(&source_code, ModuleKind::Lib).ok()?;
|
||||||
|
|
||||||
|
Some(ParsedDocument {
|
||||||
|
definitions: untyped_module.definitions,
|
||||||
|
line_numbers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert some text at the given location.
|
||||||
|
fn insert_text(at: usize, line_numbers: &LineNumbers, new_text: String) -> lsp_types::TextEdit {
|
||||||
|
let range = span_to_lsp_range(Span { start: at, end: at }, line_numbers);
|
||||||
|
lsp_types::TextEdit { range, new_text }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a suitable location (Span) in the import list. The boolean in the answer indicates
|
||||||
|
/// whether the import is a newline or not. It is set to 'false' when adding a qualified import
|
||||||
|
/// to an existing list.
|
||||||
|
impl ParsedDocument {
|
||||||
|
pub fn import(
|
||||||
|
&self,
|
||||||
|
import: &CheckedModule,
|
||||||
|
unqualified: Option<&str>,
|
||||||
|
) -> Option<AnnotatedEdit> {
|
||||||
|
let import_path = import.name.split('/').collect_vec();
|
||||||
|
|
||||||
|
let mut last_import = None;
|
||||||
|
|
||||||
|
for def in self.definitions.iter() {
|
||||||
|
match def {
|
||||||
|
Definition::Use(Use {
|
||||||
|
location,
|
||||||
|
module: existing_module,
|
||||||
|
unqualified: unqualified_list,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
last_import = Some(*location);
|
||||||
|
|
||||||
|
if import_path != existing_module.as_slice() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match unqualified {
|
||||||
|
// There's already a matching qualified import, so we have nothing to do.
|
||||||
|
None => return None,
|
||||||
|
Some(unqualified) => {
|
||||||
|
let mut last_unqualified = None;
|
||||||
|
|
||||||
|
// Insert lexicographically, assuming unqualified imports are already
|
||||||
|
// ordered. If they are not, it doesn't really matter where we insert
|
||||||
|
// anyway.
|
||||||
|
for existing_unqualified in unqualified_list {
|
||||||
|
last_unqualified = Some(existing_unqualified.location);
|
||||||
|
|
||||||
|
let existing_name = existing_unqualified
|
||||||
|
.as_name
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&existing_unqualified.name);
|
||||||
|
|
||||||
|
// The unqualified import already exist, nothing to do.
|
||||||
|
if unqualified == existing_name {
|
||||||
|
return None;
|
||||||
|
// Current import is lexicographically greater, we can insert before
|
||||||
|
} else if unqualified < existing_name.as_str() {
|
||||||
|
return Some(self.insert_qualified_before(
|
||||||
|
import,
|
||||||
|
unqualified,
|
||||||
|
existing_unqualified.location,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match last_unqualified {
|
||||||
|
// Only happens if 'unqualified_list' is empty, in which case, we
|
||||||
|
// simply create a new unqualified list of import.
|
||||||
|
None => {
|
||||||
|
Some(self.add_new_qualified(import, unqualified, *location))
|
||||||
|
}
|
||||||
|
// Happens if the new qualified import is lexicographically after
|
||||||
|
// all existing ones.
|
||||||
|
Some(location) => {
|
||||||
|
Some(self.insert_qualified_after(import, unqualified, location))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the search above didn't lead to anything, we simply insert the import either:
|
||||||
|
//
|
||||||
|
// (a) After the last import statement if any;
|
||||||
|
// (b) As the first statement in the module.
|
||||||
|
Some(self.add_new_import_line(import, unqualified, last_import))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_qualified_before(
|
||||||
|
&self,
|
||||||
|
import: &CheckedModule,
|
||||||
|
unqualified: &str,
|
||||||
|
location: Span,
|
||||||
|
) -> AnnotatedEdit {
|
||||||
|
let title = format!("Use '{}' from {}", unqualified, import.name);
|
||||||
|
(
|
||||||
|
title,
|
||||||
|
insert_text(
|
||||||
|
location.start,
|
||||||
|
&self.line_numbers,
|
||||||
|
format!("{}, ", unqualified),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_qualified_after(
|
||||||
|
&self,
|
||||||
|
import: &CheckedModule,
|
||||||
|
unqualified: &str,
|
||||||
|
location: Span,
|
||||||
|
) -> AnnotatedEdit {
|
||||||
|
let title = format!("Use '{}' from {}", unqualified, import.name);
|
||||||
|
(
|
||||||
|
title,
|
||||||
|
insert_text(
|
||||||
|
location.end,
|
||||||
|
&self.line_numbers,
|
||||||
|
format!(", {}", unqualified),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_new_qualified(
|
||||||
|
&self,
|
||||||
|
import: &CheckedModule,
|
||||||
|
unqualified: &str,
|
||||||
|
location: Span,
|
||||||
|
) -> AnnotatedEdit {
|
||||||
|
let title = format!("Use '{}' from {}", unqualified, import.name);
|
||||||
|
(
|
||||||
|
title,
|
||||||
|
insert_text(
|
||||||
|
location.end,
|
||||||
|
&self.line_numbers,
|
||||||
|
format!(".{{{}}}", unqualified),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_new_import_line(
|
||||||
|
&self,
|
||||||
|
import: &CheckedModule,
|
||||||
|
unqualified: Option<&str>,
|
||||||
|
location: Option<Span>,
|
||||||
|
) -> AnnotatedEdit {
|
||||||
|
let import_line = format!(
|
||||||
|
"use {}{}",
|
||||||
|
import.name,
|
||||||
|
match unqualified {
|
||||||
|
Some(unqualified) => format!(".{{{}}}", unqualified),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let title = format!("Add new import line: {import_line}");
|
||||||
|
|
||||||
|
(
|
||||||
|
title,
|
||||||
|
match location {
|
||||||
|
None => insert_text(0, &self.line_numbers, format!("{import_line}\n")),
|
||||||
|
Some(Span { end, .. }) => {
|
||||||
|
insert_text(end, &self.line_numbers, format!("\n{import_line}"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,10 @@ use lsp_server::Connection;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
mod cast;
|
mod cast;
|
||||||
|
mod edits;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod line_numbers;
|
mod line_numbers;
|
||||||
|
mod quickfix;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
@ -60,6 +62,7 @@ fn capabilities() -> lsp_types::ServerCapabilities {
|
||||||
// work_done_progress: None,
|
// work_done_progress: None,
|
||||||
// },
|
// },
|
||||||
// }),
|
// }),
|
||||||
|
code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)),
|
||||||
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
||||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
use crate::{
|
||||||
|
edits::{self, AnnotatedEdit, ParsedDocument},
|
||||||
|
server::lsp_project::LspProject,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
const UNKNOWN_VARIABLE: &str = "aiken::check::unknown::variable";
|
||||||
|
const UNKNOWN_TYPE: &str = "aiken::check::unknown::type";
|
||||||
|
const UNKNOWN_CONSTRUCTOR: &str = "aiken::check::unknown::type_constructor";
|
||||||
|
const UNKNOWN_MODULE: &str = "aiken::check::unknown::module";
|
||||||
|
|
||||||
|
/// Errors for which we can provide quickfixes
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
pub enum Quickfix {
|
||||||
|
UnknownIdentifier,
|
||||||
|
UnknownModule,
|
||||||
|
UnknownConstructor,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_code(diagnostic: &lsp_types::Diagnostic, expected: &str) -> bool {
|
||||||
|
diagnostic.code == Some(lsp_types::NumberOrString::String(expected.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert whether a diagnostic can be automatically fixed. Note that diagnostics often comes in
|
||||||
|
/// two severities, an error and hint; so we must be careful only addressing errors.
|
||||||
|
pub fn assert(diagnostic: &lsp_types::Diagnostic) -> Option<Quickfix> {
|
||||||
|
let is_error = diagnostic.severity == Some(lsp_types::DiagnosticSeverity::ERROR);
|
||||||
|
|
||||||
|
if !is_error {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if match_code(diagnostic, UNKNOWN_VARIABLE) {
|
||||||
|
return Some(Quickfix::UnknownIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if match_code(diagnostic, UNKNOWN_TYPE) {
|
||||||
|
return Some(Quickfix::UnknownIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if match_code(diagnostic, UNKNOWN_CONSTRUCTOR) {
|
||||||
|
return Some(Quickfix::UnknownConstructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if match_code(diagnostic, UNKNOWN_MODULE) {
|
||||||
|
return Some(Quickfix::UnknownModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quickfix(
|
||||||
|
compiler: &LspProject,
|
||||||
|
text_document: &lsp_types::TextDocumentIdentifier,
|
||||||
|
diagnostic: &lsp_types::Diagnostic,
|
||||||
|
quickfix: &Quickfix,
|
||||||
|
) -> Vec<lsp_types::CodeAction> {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
|
if let Some(ref parsed_document) = edits::parse_document(text_document) {
|
||||||
|
if let Some(serde_json::Value::String(ref data)) = diagnostic.data {
|
||||||
|
let edits = match quickfix {
|
||||||
|
Quickfix::UnknownIdentifier => unknown_identifier(compiler, parsed_document, data),
|
||||||
|
Quickfix::UnknownModule => unknown_module(compiler, parsed_document, data),
|
||||||
|
Quickfix::UnknownConstructor => {
|
||||||
|
unknown_constructor(compiler, parsed_document, data)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (title, edit) in edits.into_iter() {
|
||||||
|
let mut changes = HashMap::new();
|
||||||
|
changes.insert(text_document.uri.clone(), vec![edit]);
|
||||||
|
actions.push(lsp_types::CodeAction {
|
||||||
|
title,
|
||||||
|
kind: Some(lsp_types::CodeActionKind::QUICKFIX),
|
||||||
|
diagnostics: Some(vec![diagnostic.clone()]),
|
||||||
|
is_preferred: Some(true),
|
||||||
|
disabled: None,
|
||||||
|
data: None,
|
||||||
|
command: None,
|
||||||
|
edit: Some(lsp_types::WorkspaceEdit {
|
||||||
|
changes: Some(changes),
|
||||||
|
document_changes: None,
|
||||||
|
change_annotations: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown_identifier(
|
||||||
|
compiler: &LspProject,
|
||||||
|
parsed_document: &ParsedDocument,
|
||||||
|
var_name: &str,
|
||||||
|
) -> Vec<AnnotatedEdit> {
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
|
||||||
|
for module in compiler.project.modules() {
|
||||||
|
if module.ast.has_definition(var_name) {
|
||||||
|
if let Some(edit) = parsed_document.import(&module, Some(var_name)) {
|
||||||
|
edits.push(edit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edits
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown_constructor(
|
||||||
|
compiler: &LspProject,
|
||||||
|
parsed_document: &ParsedDocument,
|
||||||
|
constructor_name: &str,
|
||||||
|
) -> Vec<AnnotatedEdit> {
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
|
||||||
|
for module in compiler.project.modules() {
|
||||||
|
if module.ast.has_constructor(constructor_name) {
|
||||||
|
if let Some(edit) = parsed_document.import(&module, Some(constructor_name)) {
|
||||||
|
edits.push(edit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edits
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown_module(
|
||||||
|
compiler: &LspProject,
|
||||||
|
parsed_document: &ParsedDocument,
|
||||||
|
module_name: &str,
|
||||||
|
) -> Vec<AnnotatedEdit> {
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
|
||||||
|
for module in compiler.project.modules() {
|
||||||
|
if module.name.ends_with(module_name) {
|
||||||
|
if let Some(edit) = parsed_document.import(&module, None) {
|
||||||
|
edits.push(edit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edits
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
|
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{Definition, Located, ModuleKind, Span, Use},
|
ast::{Definition, Located, ModuleKind, Span, Use},
|
||||||
|
error::ExtraData,
|
||||||
parser,
|
parser,
|
||||||
tipo::pretty::Printer,
|
tipo::pretty::Printer,
|
||||||
};
|
};
|
||||||
|
@ -23,7 +24,8 @@ use lsp_types::{
|
||||||
Notification, Progress, PublishDiagnostics, ShowMessage,
|
Notification, Progress, PublishDiagnostics, ShowMessage,
|
||||||
},
|
},
|
||||||
request::{
|
request::{
|
||||||
Completion, Formatting, GotoDefinition, HoverRequest, Request, WorkDoneProgressCreate,
|
CodeActionRequest, Completion, Formatting, GotoDefinition, HoverRequest, Request,
|
||||||
|
WorkDoneProgressCreate,
|
||||||
},
|
},
|
||||||
DocumentFormattingParams, InitializeParams, TextEdit,
|
DocumentFormattingParams, InitializeParams, TextEdit,
|
||||||
};
|
};
|
||||||
|
@ -33,6 +35,7 @@ use crate::{
|
||||||
cast::{cast_notification, cast_request},
|
cast::{cast_notification, cast_request},
|
||||||
error::Error as ServerError,
|
error::Error as ServerError,
|
||||||
line_numbers::LineNumbers,
|
line_numbers::LineNumbers,
|
||||||
|
quickfix,
|
||||||
utils::{
|
utils::{
|
||||||
path_to_uri, span_to_lsp_range, text_edit_replace, uri_to_module_name,
|
path_to_uri, span_to_lsp_range, text_edit_replace, uri_to_module_name,
|
||||||
COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
|
COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
|
||||||
|
@ -41,7 +44,7 @@ use crate::{
|
||||||
|
|
||||||
use self::lsp_project::LspProject;
|
use self::lsp_project::LspProject;
|
||||||
|
|
||||||
mod lsp_project;
|
pub mod lsp_project;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -288,6 +291,7 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HoverRequest::METHOD => {
|
HoverRequest::METHOD => {
|
||||||
let params = cast_request::<HoverRequest>(request)?;
|
let params = cast_request::<HoverRequest>(request)?;
|
||||||
|
|
||||||
|
@ -324,6 +328,33 @@ impl Server {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CodeActionRequest::METHOD => {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
|
if let Some(ref compiler) = self.compiler {
|
||||||
|
let params = cast_request::<CodeActionRequest>(request)
|
||||||
|
.expect("cast code action request");
|
||||||
|
|
||||||
|
for diagnostic in params.context.diagnostics.iter() {
|
||||||
|
if let Some(strategy) = quickfix::assert(diagnostic) {
|
||||||
|
let quickfixes = quickfix::quickfix(
|
||||||
|
compiler,
|
||||||
|
¶ms.text_document,
|
||||||
|
diagnostic,
|
||||||
|
&strategy,
|
||||||
|
);
|
||||||
|
actions.extend(quickfixes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lsp_server::Response {
|
||||||
|
id,
|
||||||
|
error: None,
|
||||||
|
result: Some(serde_json::to_value(actions)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
unsupported => Err(ServerError::UnsupportedLspRequest {
|
unsupported => Err(ServerError::UnsupportedLspRequest {
|
||||||
request: unsupported.to_string(),
|
request: unsupported.to_string(),
|
||||||
}),
|
}),
|
||||||
|
@ -445,7 +476,6 @@ impl Server {
|
||||||
fn module_for_uri(&self, uri: &url::Url) -> Option<&CheckedModule> {
|
fn module_for_uri(&self, uri: &url::Url) -> Option<&CheckedModule> {
|
||||||
self.compiler.as_ref().and_then(|compiler| {
|
self.compiler.as_ref().and_then(|compiler| {
|
||||||
let module_name = uri_to_module_name(uri, &self.root).expect("uri to module name");
|
let module_name = uri_to_module_name(uri, &self.root).expect("uri to module name");
|
||||||
|
|
||||||
compiler.modules.get(&module_name)
|
compiler.modules.get(&module_name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -591,7 +621,7 @@ impl Server {
|
||||||
/// the `showMessage` notification instead.
|
/// the `showMessage` notification instead.
|
||||||
fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError>
|
fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError>
|
||||||
where
|
where
|
||||||
E: Diagnostic + GetSource,
|
E: Diagnostic + GetSource + ExtraData,
|
||||||
{
|
{
|
||||||
let (severity, typ) = match error.severity() {
|
let (severity, typ) = match error.severity() {
|
||||||
Some(severity) => match severity {
|
Some(severity) => match severity {
|
||||||
|
@ -642,7 +672,7 @@ impl Server {
|
||||||
message,
|
message,
|
||||||
related_information: None,
|
related_information: None,
|
||||||
tags: None,
|
tags: None,
|
||||||
data: None,
|
data: error.extra_data().map(serde_json::Value::String),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
|
|
@ -38,8 +38,6 @@ impl LspProject {
|
||||||
|
|
||||||
self.project.restore(checkpoint);
|
self.project.restore(checkpoint);
|
||||||
|
|
||||||
result?;
|
|
||||||
|
|
||||||
let modules = self.project.modules();
|
let modules = self.project.modules();
|
||||||
|
|
||||||
for mut module in modules.into_iter() {
|
for mut module in modules.into_iter() {
|
||||||
|
@ -61,6 +59,8 @@ impl LspProject {
|
||||||
self.modules.insert(module.name.to_string(), module);
|
self.modules.insert(module.name.to_string(), module);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{self, BinOp, Span},
|
ast::{self, BinOp, Span},
|
||||||
|
error::ExtraData,
|
||||||
parser::error::ParseError,
|
parser::error::ParseError,
|
||||||
tipo,
|
tipo,
|
||||||
};
|
};
|
||||||
|
@ -172,6 +173,34 @@ impl From<Error> for Vec<Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExtraData for Error {
|
||||||
|
fn extra_data(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Error::DuplicateModule { .. } => None,
|
||||||
|
Error::FileIo { .. } => None,
|
||||||
|
Error::Format { .. } => None,
|
||||||
|
Error::StandardIo { .. } => None,
|
||||||
|
Error::Blueprint { .. } => None,
|
||||||
|
Error::MissingManifest { .. } => None,
|
||||||
|
Error::TomlLoading { .. } => None,
|
||||||
|
Error::ImportCycle { .. } => None,
|
||||||
|
Error::Parse { .. } => None,
|
||||||
|
Error::Type { error, .. } => error.extra_data(),
|
||||||
|
Error::TestFailure { .. } => None,
|
||||||
|
Error::Http { .. } => None,
|
||||||
|
Error::ZipExtract { .. } => None,
|
||||||
|
Error::JoinError { .. } => None,
|
||||||
|
Error::UnknownPackageVersion { .. } => None,
|
||||||
|
Error::UnableToResolvePackage { .. } => None,
|
||||||
|
Error::Json { .. } => None,
|
||||||
|
Error::MalformedStakeAddress { .. } => None,
|
||||||
|
Error::NoValidatorNotFound { .. } => None,
|
||||||
|
Error::MoreThanOneValidatorFound { .. } => None,
|
||||||
|
Error::Module { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait GetSource {
|
pub trait GetSource {
|
||||||
fn path(&self) -> Option<PathBuf>;
|
fn path(&self) -> Option<PathBuf>;
|
||||||
fn src(&self) -> Option<String>;
|
fn src(&self) -> Option<String>;
|
||||||
|
@ -481,6 +510,16 @@ pub enum Warning {
|
||||||
DependencyAlreadyExists { name: PackageName },
|
DependencyAlreadyExists { name: PackageName },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExtraData for Warning {
|
||||||
|
fn extra_data(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Warning::NoValidators { .. } => None,
|
||||||
|
Warning::DependencyAlreadyExists { .. } => None,
|
||||||
|
Warning::Type { warning, .. } => warning.extra_data(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl GetSource for Warning {
|
impl GetSource for Warning {
|
||||||
fn path(&self) -> Option<PathBuf> {
|
fn path(&self) -> Option<PathBuf> {
|
||||||
match self {
|
match self {
|
||||||
|
|
Loading…
Reference in New Issue