feat: publish errors as lsp diagnostic messages

This commit is contained in:
rvcas 2022-11-15 17:15:58 -05:00 committed by Lucas
parent f089eff97d
commit bff99b0cf2
10 changed files with 366 additions and 32 deletions

1
Cargo.lock generated
View File

@ -98,6 +98,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror", "thiserror",
"tracing", "tracing",
"url",
] ]
[[package]] [[package]]

View File

@ -77,6 +77,7 @@ pub enum ErrorKind {
#[error("unclosed {start}")] #[error("unclosed {start}")]
Unclosed { Unclosed {
start: Pattern, start: Pattern,
#[label]
before_span: Span, before_span: Span,
before: Option<Pattern>, before: Option<Pattern>,
}, },

View File

@ -55,7 +55,9 @@ pub enum Error {
#[error("duplicate type name {name}")] #[error("duplicate type name {name}")]
DuplicateTypeName { DuplicateTypeName {
#[label]
location: Span, location: Span,
#[label]
previous_location: Span, previous_location: Span,
name: String, name: String,
}, },
@ -79,6 +81,7 @@ pub enum Error {
#[error("{name} has incorrect type arity expected {expected} but given {given}")] #[error("{name} has incorrect type arity expected {expected} but given {given}")]
IncorrectTypeArity { IncorrectTypeArity {
#[label]
location: Span, location: Span,
name: String, name: String,
expected: usize, expected: usize,

View File

@ -20,3 +20,4 @@ serde = "1.0.147"
serde_json = "1.0.87" serde_json = "1.0.87"
thiserror = "1.0.37" thiserror = "1.0.37"
tracing = "0.1.37" tracing = "0.1.37"
url = "2.3.1"

View File

@ -26,4 +26,7 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
#[diagnostic(code(aiken::lsp::send))] #[diagnostic(code(aiken::lsp::send))]
Send(#[from] SendError<Message>), Send(#[from] SendError<Message>),
#[error(transparent)]
#[diagnostic(code(aiken::lsp::send))]
PathToUri(#[from] url::ParseError),
} }

View File

@ -5,6 +5,7 @@ use lsp_types::{
}; };
pub mod error; pub mod error;
mod line_numbers;
pub mod server; pub mod server;
use error::Error; use error::Error;
@ -48,7 +49,7 @@ fn capabilities() -> ServerCapabilities {
})), })),
}, },
)), )),
definition_provider: Some(OneOf::Left(true)), // definition_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)), document_formatting_provider: Some(OneOf::Left(true)),
..Default::default() ..Default::default()
} }

View File

@ -0,0 +1,54 @@
#[allow(dead_code)]
#[derive(Debug)]
pub struct LineNumbers {
line_starts: Vec<usize>,
length: usize,
}
impl LineNumbers {
pub fn new(src: &str) -> Self {
Self {
length: src.len() as usize,
line_starts: std::iter::once(0)
.chain(src.match_indices('\n').map(|(i, _)| i + 1))
.collect(),
}
}
/// Get the line number for a byte index
pub fn line_number(&self, byte_index: usize) -> usize {
self.line_starts
.binary_search(&byte_index)
.unwrap_or_else(|next_line| next_line - 1) as usize
+ 1
}
// TODO: handle unicode characters that may be more than 1 byte in width
pub fn line_and_column_number(&self, byte_index: usize) -> LineColumn {
let line = self.line_number(byte_index);
let column = byte_index
- self
.line_starts
.get(line as usize - 1)
.copied()
.unwrap_or_default()
+ 1;
LineColumn { line, column }
}
// TODO: handle unicode characters that may be more than 1 byte in width
/// 0 indexed line and character to byte index
#[allow(dead_code)]
pub fn byte_index(&self, line: usize, character: usize) -> usize {
match self.line_starts.get((line) as usize) {
Some(line_index) => *line_index + character,
None => self.length,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LineColumn {
pub line: usize,
pub column: usize,
}

View File

@ -1,15 +1,21 @@
use std::{collections::HashMap, fs, path::Path}; use std::{
collections::{HashMap, HashSet},
error::Error,
fs,
path::{Path, PathBuf},
};
use aiken_lang::{ast::ModuleKind, parser}; use aiken_lang::{ast::ModuleKind, parser};
use aiken_project::config; use aiken_project::{config, error::Error as ProjectError};
use lsp_server::{Connection, Message}; use lsp_server::{Connection, Message};
use lsp_types::{ use lsp_types::{
notification::{DidChangeTextDocument, Notification}, notification::{DidChangeTextDocument, Notification, PublishDiagnostics, ShowMessage},
request::{Formatting, Request}, request::{Formatting, Request},
DocumentFormattingParams, InitializeParams, TextEdit, DocumentFormattingParams, InitializeParams, TextEdit,
}; };
use miette::Diagnostic;
use crate::error::Error; use crate::{error::Error as ServerError, line_numbers::LineNumbers};
#[allow(dead_code)] #[allow(dead_code)]
pub struct Server { pub struct Server {
@ -19,6 +25,18 @@ pub struct Server {
edited: HashMap<String, String>, edited: HashMap<String, String>,
initialize_params: InitializeParams, initialize_params: InitializeParams,
/// Files for which there are active diagnostics
published_diagnostics: HashSet<lsp_types::Url>,
/// Diagnostics that have been emitted by the compiler but not yet published
/// to the client
stored_diagnostics: HashMap<PathBuf, Vec<lsp_types::Diagnostic>>,
/// Diagnostics that have been emitted by the compiler but not yet published
/// to the client. These are likely locationless Aiken diagnostics, as LSP
/// diagnostics always need a location.
stored_messages: Vec<lsp_types::ShowMessageParams>,
} }
impl Server { impl Server {
@ -27,10 +45,15 @@ impl Server {
config, config,
edited: HashMap::new(), edited: HashMap::new(),
initialize_params, initialize_params,
published_diagnostics: HashSet::new(),
stored_diagnostics: HashMap::new(),
stored_messages: Vec::new(),
} }
} }
pub fn listen(&mut self, connection: Connection) -> Result<(), Error> { pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
self.publish_stored_diagnostics(&connection)?;
for msg in &connection.receiver { for msg in &connection.receiver {
tracing::debug!("Got message: {:#?}", msg); tracing::debug!("Got message: {:#?}", msg);
@ -44,6 +67,8 @@ impl Server {
let response = self.handle_request(req)?; let response = self.handle_request(req)?;
self.publish_stored_diagnostics(&connection)?;
connection.sender.send(Message::Response(response))?; connection.sender.send(Message::Response(response))?;
} }
Message::Response(_) => todo!(), Message::Response(_) => todo!(),
@ -59,7 +84,7 @@ impl Server {
fn handle_request( fn handle_request(
&mut self, &mut self,
request: lsp_server::Request, request: lsp_server::Request,
) -> Result<lsp_server::Response, Error> { ) -> Result<lsp_server::Response, ServerError> {
let id = request.id.clone(); let id = request.id.clone();
match request.method.as_str() { match request.method.as_str() {
@ -78,12 +103,45 @@ impl Server {
result: Some(result), result: Some(result),
}) })
} }
Err(_) => { Err(err) => match err {
todo!("transform project errors in lsp diagnostic") ProjectError::List(errors) => {
} for error in errors {
if error.source_code().is_some() {
self.process_diagnostic(error)?;
}
}
Ok(lsp_server::Response {
id,
error: None,
result: Some(serde_json::json!(null)),
})
}
error => {
if error.source_code().is_some() {
self.process_diagnostic(error)?;
Ok(lsp_server::Response {
id,
error: None,
result: Some(serde_json::json!(null)),
})
} else {
Ok(lsp_server::Response {
id,
error: Some(lsp_server::ResponseError {
code: 1, // We should assign a code to each error.
message: format!("{:?}", error),
data: None,
}),
result: None,
})
}
}
},
} }
} }
unsupported => Err(Error::UnsupportedLspRequest { unsupported => Err(ServerError::UnsupportedLspRequest {
request: unsupported.to_string(), request: unsupported.to_string(),
}), }),
} }
@ -93,7 +151,7 @@ impl Server {
&mut self, &mut self,
_connection: &lsp_server::Connection, _connection: &lsp_server::Connection,
notification: lsp_server::Notification, notification: lsp_server::Notification,
) -> Result<(), Error> { ) -> Result<(), ServerError> {
match notification.method.as_str() { match notification.method.as_str() {
DidChangeTextDocument::METHOD => { DidChangeTextDocument::METHOD => {
let params = cast_notification::<DidChangeTextDocument>(notification)?; let params = cast_notification::<DidChangeTextDocument>(notification)?;
@ -102,7 +160,7 @@ impl Server {
let path = params.text_document.uri.path().to_string(); let path = params.text_document.uri.path().to_string();
if let Some(changes) = params.content_changes.into_iter().next() { if let Some(changes) = params.content_changes.into_iter().next() {
let _ = self.edited.insert(path, changes.text); self.edited.insert(path, changes.text);
} }
Ok(()) Ok(())
@ -111,10 +169,7 @@ impl Server {
} }
} }
fn format( fn format(&mut self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>, ProjectError> {
&mut self,
params: DocumentFormattingParams,
) -> Result<Vec<TextEdit>, aiken_project::error::Error> {
let path = params.text_document.uri.path(); let path = params.text_document.uri.path();
let mut new_text = String::new(); let mut new_text = String::new();
@ -139,9 +194,182 @@ impl Server {
Ok(vec![text_edit_replace(new_text)]) Ok(vec![text_edit_replace(new_text)])
} }
/// Publish all stored diagnostics to the client.
/// Any previously publish diagnostics are cleared before the new set are
/// published to the client.
fn publish_stored_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
self.clear_all_diagnostics(connection)?;
for (path, diagnostics) in self.stored_diagnostics.drain() {
let uri = path_to_uri(path)?;
// Record that we have published diagnostics to this file so we can
// clear it later when they are outdated.
self.published_diagnostics.insert(uri.clone());
// Publish the diagnostics
let params = lsp_types::PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
};
let notification = lsp_server::Notification {
method: PublishDiagnostics::METHOD.to_string(),
params: serde_json::to_value(params)?,
};
connection
.sender
.send(lsp_server::Message::Notification(notification))?;
}
for message in self.stored_messages.drain(..) {
let notification = lsp_server::Notification {
method: ShowMessage::METHOD.to_string(),
params: serde_json::to_value(message)?,
};
connection
.sender
.send(lsp_server::Message::Notification(notification))?;
}
Ok(())
}
/// Clear all diagnostics that have been previously published to the client
fn clear_all_diagnostics(&mut self, connection: &Connection) -> Result<(), ServerError> {
for file in self.published_diagnostics.drain() {
let params = lsp_types::PublishDiagnosticsParams {
uri: file,
diagnostics: vec![],
version: None,
};
let notification = lsp_server::Notification {
method: PublishDiagnostics::METHOD.to_string(),
params: serde_json::to_value(params)?,
};
connection
.sender
.send(lsp_server::Message::Notification(notification))?;
}
Ok(())
}
/// Convert Aiken diagnostics into 1 or more LSP diagnostics and store them
/// so that they can later be published to the client with
/// `publish_stored_diagnostics`
///
/// If the Aiken diagnostic cannot be converted to LSP diagnostic (due to it
/// not having a location) it is stored as a message suitable for use with
/// the `showMessage` notification instead.
///
fn process_diagnostic(&mut self, error: ProjectError) -> Result<(), ServerError> {
let (severity, typ) = match error.severity() {
Some(severity) => match severity {
miette::Severity::Error => (
lsp_types::DiagnosticSeverity::ERROR,
lsp_types::MessageType::ERROR,
),
miette::Severity::Warning => (
lsp_types::DiagnosticSeverity::WARNING,
lsp_types::MessageType::WARNING,
),
miette::Severity::Advice => (
lsp_types::DiagnosticSeverity::HINT,
lsp_types::MessageType::INFO,
),
},
None => (
lsp_types::DiagnosticSeverity::ERROR,
lsp_types::MessageType::ERROR,
),
};
let mut text = match error.source() {
Some(err) => err.to_string(),
None => error.to_string(),
};
if let (Some(mut labels), Some(path), Some(src)) =
(error.labels(), error.path(), error.src())
{
if let Some(labeled_span) = labels.next() {
if let Some(label) = labeled_span.label() {
text.push_str("\n\n");
text.push_str(label);
if !label.ends_with(['.', '?']) {
text.push('.');
}
}
let line_numbers = LineNumbers::new(&src);
let start = line_numbers.line_and_column_number(labeled_span.inner().offset());
let end = line_numbers.line_and_column_number(
labeled_span.inner().offset() + labeled_span.inner().len(),
);
let lsp_diagnostic = lsp_types::Diagnostic {
range: lsp_types::Range::new(
lsp_types::Position {
line: start.line as u32 - 1,
character: start.column as u32 - 1,
},
lsp_types::Position {
line: end.line as u32 - 1,
character: end.column as u32 - 1,
},
),
severity: Some(severity),
code: error
.code()
.map(|c| lsp_types::NumberOrString::String(c.to_string())),
code_description: None,
source: None,
message: text.clone(),
related_information: None,
tags: None,
data: None,
};
let path = path.canonicalize()?;
self.push_diagnostic(path.clone(), lsp_diagnostic.clone());
if let Some(hint) = error.help() {
let lsp_hint = lsp_types::Diagnostic {
severity: Some(lsp_types::DiagnosticSeverity::HINT),
message: hint.to_string(),
..lsp_diagnostic
};
self.push_diagnostic(path, lsp_hint);
}
}
} else {
self.stored_messages
.push(lsp_types::ShowMessageParams { typ, message: text })
}
Ok(())
}
fn push_diagnostic(&mut self, path: PathBuf, diagnostic: lsp_types::Diagnostic) {
self.stored_diagnostics
.entry(path)
.or_default()
.push(diagnostic);
}
} }
fn cast_request<R>(request: lsp_server::Request) -> Result<R::Params, Error> fn cast_request<R>(request: lsp_server::Request) -> Result<R::Params, ServerError>
where where
R: lsp_types::request::Request, R: lsp_types::request::Request,
R::Params: serde::de::DeserializeOwned, R::Params: serde::de::DeserializeOwned,
@ -151,7 +379,7 @@ where
Ok(params) Ok(params)
} }
fn cast_notification<N>(notification: lsp_server::Notification) -> Result<N::Params, Error> fn cast_notification<N>(notification: lsp_server::Notification) -> Result<N::Params, ServerError>
where where
N: lsp_types::notification::Notification, N: lsp_types::notification::Notification,
N::Params: serde::de::DeserializeOwned, N::Params: serde::de::DeserializeOwned,
@ -176,3 +404,13 @@ fn text_edit_replace(new_text: String) -> TextEdit {
new_text, new_text,
} }
} }
fn path_to_uri(path: PathBuf) -> Result<lsp_types::Url, ServerError> {
let mut file: String = "file://".into();
file.push_str(&path.as_os_str().to_string_lossy());
let uri = lsp_types::Url::parse(&file)?;
Ok(uri)
}

View File

@ -127,6 +127,36 @@ impl Error {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
matches!(self, Error::List(errors) if errors.is_empty()) matches!(self, Error::List(errors) if errors.is_empty())
} }
pub fn path(&self) -> Option<PathBuf> {
match self {
Error::DuplicateModule { second, .. } => Some(second.to_path_buf()),
Error::FileIo { .. } => None,
Error::Format { .. } => None,
Error::StandardIo(_) => None,
Error::ImportCycle { .. } => None,
Error::List(_) => None,
Error::Parse { path, .. } => Some(path.to_path_buf()),
Error::Type { path, .. } => Some(path.to_path_buf()),
Error::ValidatorMustReturnBool { path, .. } => Some(path.to_path_buf()),
Error::WrongValidatorArity { path, .. } => Some(path.to_path_buf()),
}
}
pub fn src(&self) -> Option<String> {
match self {
Error::DuplicateModule { .. } => None,
Error::FileIo { .. } => None,
Error::Format { .. } => None,
Error::StandardIo(_) => None,
Error::ImportCycle { .. } => None,
Error::List(_) => None,
Error::Parse { src, .. } => Some(src.to_string()),
Error::Type { src, .. } => Some(src.to_string()),
Error::ValidatorMustReturnBool { src, .. } => Some(src.to_string()),
Error::WrongValidatorArity { src, .. } => Some(src.to_string()),
}
}
} }
impl Debug for Error { impl Debug for Error {
@ -149,6 +179,10 @@ impl Debug for Error {
} }
impl Diagnostic for Error { impl Diagnostic for Error {
fn severity(&self) -> Option<miette::Severity> {
Some(miette::Severity::Error)
}
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> { fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
match self { match self {
Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")), Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")),
@ -233,6 +267,10 @@ pub enum Warning {
} }
impl Diagnostic for Warning { impl Diagnostic for Warning {
fn severity(&self) -> Option<miette::Severity> {
Some(miette::Severity::Warning)
}
fn source_code(&self) -> Option<&dyn SourceCode> { fn source_code(&self) -> Option<&dyn SourceCode> {
match self { match self {
Warning::Type { src, .. } => Some(src), Warning::Type { src, .. } => Some(src),

View File

@ -4,16 +4,12 @@ use sample/spend
pub type Redeemer { pub type Redeemer {
signer: ByteArray, signer: ByteArray,
amount: Int amount: Int,
} }
pub type Reen { pub type Reen {
Buy{ Buy { signer: ByteArray, amount: Int }
signer: ByteArray,
amount: Int
}
Sell Sell
} }
pub fn twice(f: fn(Int) -> Int, initial: Int) -> Int { pub fn twice(f: fn(Int) -> Int, initial: Int) -> Int {
@ -28,7 +24,7 @@ pub fn add_two(x: Int) -> Int {
twice(add_one, x) twice(add_one, x)
} }
pub fn final_check(z: Int){ pub fn final_check(z: Int) {
z < 4 z < 4
} }
@ -37,10 +33,8 @@ pub fn spend(
rdmr: Redeemer, rdmr: Redeemer,
ctx: spend.ScriptContext, ctx: spend.ScriptContext,
) -> Bool { ) -> Bool {
let a = datum.fin
let a = datum.fin a
|> add_two
a |> final_check
|> add_two
|> final_check
} }