diff --git a/Cargo.lock b/Cargo.lock index e33564c7..929183f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,7 @@ dependencies = [ "serde_json", "thiserror", "tracing", + "url", ] [[package]] diff --git a/crates/lang/src/parser/error.rs b/crates/lang/src/parser/error.rs index 7d287834..cc161972 100644 --- a/crates/lang/src/parser/error.rs +++ b/crates/lang/src/parser/error.rs @@ -77,6 +77,7 @@ pub enum ErrorKind { #[error("unclosed {start}")] Unclosed { start: Pattern, + #[label] before_span: Span, before: Option, }, diff --git a/crates/lang/src/tipo/error.rs b/crates/lang/src/tipo/error.rs index 0a800dde..bc470490 100644 --- a/crates/lang/src/tipo/error.rs +++ b/crates/lang/src/tipo/error.rs @@ -55,7 +55,9 @@ pub enum Error { #[error("duplicate type name {name}")] DuplicateTypeName { + #[label] location: Span, + #[label] previous_location: Span, name: String, }, @@ -79,6 +81,7 @@ pub enum Error { #[error("{name} has incorrect type arity expected {expected} but given {given}")] IncorrectTypeArity { + #[label] location: Span, name: String, expected: usize, diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index c9b70f1b..df474741 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -20,3 +20,4 @@ serde = "1.0.147" serde_json = "1.0.87" thiserror = "1.0.37" tracing = "0.1.37" +url = "2.3.1" diff --git a/crates/lsp/src/error.rs b/crates/lsp/src/error.rs index 5ee30d61..dfeb2dd5 100644 --- a/crates/lsp/src/error.rs +++ b/crates/lsp/src/error.rs @@ -26,4 +26,7 @@ pub enum Error { #[error(transparent)] #[diagnostic(code(aiken::lsp::send))] Send(#[from] SendError), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::send))] + PathToUri(#[from] url::ParseError), } diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 9f34b819..38ceae90 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -5,6 +5,7 @@ use lsp_types::{ }; pub mod error; +mod line_numbers; pub mod server; 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)), ..Default::default() } diff --git a/crates/lsp/src/line_numbers.rs b/crates/lsp/src/line_numbers.rs new file mode 100644 index 00000000..9feabea9 --- /dev/null +++ b/crates/lsp/src/line_numbers.rs @@ -0,0 +1,54 @@ +#[allow(dead_code)] +#[derive(Debug)] +pub struct LineNumbers { + line_starts: Vec, + 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, +} diff --git a/crates/lsp/src/server.rs b/crates/lsp/src/server.rs index 702dc07f..a9743f27 100644 --- a/crates/lsp/src/server.rs +++ b/crates/lsp/src/server.rs @@ -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_project::config; +use aiken_project::{config, error::Error as ProjectError}; use lsp_server::{Connection, Message}; use lsp_types::{ - notification::{DidChangeTextDocument, Notification}, + notification::{DidChangeTextDocument, Notification, PublishDiagnostics, ShowMessage}, request::{Formatting, Request}, DocumentFormattingParams, InitializeParams, TextEdit, }; +use miette::Diagnostic; -use crate::error::Error; +use crate::{error::Error as ServerError, line_numbers::LineNumbers}; #[allow(dead_code)] pub struct Server { @@ -19,6 +25,18 @@ pub struct Server { edited: HashMap, initialize_params: InitializeParams, + + /// Files for which there are active diagnostics + published_diagnostics: HashSet, + + /// Diagnostics that have been emitted by the compiler but not yet published + /// to the client + stored_diagnostics: HashMap>, + + /// 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, } impl Server { @@ -27,10 +45,15 @@ impl Server { config, edited: HashMap::new(), 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 { tracing::debug!("Got message: {:#?}", msg); @@ -44,6 +67,8 @@ impl Server { let response = self.handle_request(req)?; + self.publish_stored_diagnostics(&connection)?; + connection.sender.send(Message::Response(response))?; } Message::Response(_) => todo!(), @@ -59,7 +84,7 @@ impl Server { fn handle_request( &mut self, request: lsp_server::Request, - ) -> Result { + ) -> Result { let id = request.id.clone(); match request.method.as_str() { @@ -78,12 +103,45 @@ impl Server { result: Some(result), }) } - Err(_) => { - todo!("transform project errors in lsp diagnostic") - } + Err(err) => match err { + 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(), }), } @@ -93,7 +151,7 @@ impl Server { &mut self, _connection: &lsp_server::Connection, notification: lsp_server::Notification, - ) -> Result<(), Error> { + ) -> Result<(), ServerError> { match notification.method.as_str() { DidChangeTextDocument::METHOD => { let params = cast_notification::(notification)?; @@ -102,7 +160,7 @@ impl Server { let path = params.text_document.uri.path().to_string(); if let Some(changes) = params.content_changes.into_iter().next() { - let _ = self.edited.insert(path, changes.text); + self.edited.insert(path, changes.text); } Ok(()) @@ -111,10 +169,7 @@ impl Server { } } - fn format( - &mut self, - params: DocumentFormattingParams, - ) -> Result, aiken_project::error::Error> { + fn format(&mut self, params: DocumentFormattingParams) -> Result, ProjectError> { let path = params.text_document.uri.path(); let mut new_text = String::new(); @@ -139,9 +194,182 @@ impl Server { 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(request: lsp_server::Request) -> Result +fn cast_request(request: lsp_server::Request) -> Result where R: lsp_types::request::Request, R::Params: serde::de::DeserializeOwned, @@ -151,7 +379,7 @@ where Ok(params) } -fn cast_notification(notification: lsp_server::Notification) -> Result +fn cast_notification(notification: lsp_server::Notification) -> Result where N: lsp_types::notification::Notification, N::Params: serde::de::DeserializeOwned, @@ -176,3 +404,13 @@ fn text_edit_replace(new_text: String) -> TextEdit { new_text, } } + +fn path_to_uri(path: PathBuf) -> Result { + 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) +} diff --git a/crates/project/src/error.rs b/crates/project/src/error.rs index 6629b8a4..aa525388 100644 --- a/crates/project/src/error.rs +++ b/crates/project/src/error.rs @@ -127,6 +127,36 @@ impl Error { pub fn is_empty(&self) -> bool { matches!(self, Error::List(errors) if errors.is_empty()) } + + pub fn path(&self) -> Option { + 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 { + 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 { @@ -149,6 +179,10 @@ impl Debug for Error { } impl Diagnostic for Error { + fn severity(&self) -> Option { + Some(miette::Severity::Error) + } + fn code<'a>(&'a self) -> Option> { match self { Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")), @@ -233,6 +267,10 @@ pub enum Warning { } impl Diagnostic for Warning { + fn severity(&self) -> Option { + Some(miette::Severity::Warning) + } + fn source_code(&self) -> Option<&dyn SourceCode> { match self { Warning::Type { src, .. } => Some(src), diff --git a/examples/sample/validators/swap.ak b/examples/sample/validators/swap.ak index df0ac60b..c83f8d68 100644 --- a/examples/sample/validators/swap.ak +++ b/examples/sample/validators/swap.ak @@ -4,16 +4,12 @@ use sample/spend pub type Redeemer { signer: ByteArray, - amount: Int + amount: Int, } pub type Reen { - Buy{ - signer: ByteArray, - amount: Int - } + Buy { signer: ByteArray, amount: Int } Sell - } 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) } -pub fn final_check(z: Int){ +pub fn final_check(z: Int) { z < 4 } @@ -37,10 +33,8 @@ pub fn spend( rdmr: Redeemer, ctx: spend.ScriptContext, ) -> Bool { - - let a = datum.fin - - a - |> add_two - |> final_check + let a = datum.fin + a + |> add_two + |> final_check }