use std::{ collections::{HashMap, HashSet}, error::Error, fs, path::{Path, PathBuf}, }; use aiken_lang::{ast::ModuleKind, parser}; use aiken_project::{config, error::Error as ProjectError}; use lsp_server::{Connection, Message}; use lsp_types::{ notification::{ DidChangeTextDocument, DidSaveTextDocument, Notification, PublishDiagnostics, ShowMessage, }, request::{Formatting, Request}, DocumentFormattingParams, InitializeParams, TextEdit, }; use miette::Diagnostic; use crate::{error::Error as ServerError, line_numbers::LineNumbers}; #[allow(dead_code)] pub struct Server { config: Option, /// Files that have been edited in memory 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 { pub fn new(initialize_params: InitializeParams, config: Option) -> Self { Self { 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<(), ServerError> { self.publish_stored_diagnostics(&connection)?; for msg in &connection.receiver { tracing::debug!("Got message: {:#?}", msg); match msg { Message::Request(req) => { if connection.handle_shutdown(&req)? { return Ok(()); } tracing::debug!("Get request: {:#?}", req); let response = self.handle_request(req)?; self.publish_stored_diagnostics(&connection)?; connection.sender.send(Message::Response(response))?; } Message::Response(_) => todo!(), Message::Notification(notification) => { self.handle_notification(&connection, notification)? } } } Ok(()) } fn handle_request( &mut self, request: lsp_server::Request, ) -> Result { let id = request.id.clone(); match request.method.as_str() { Formatting::METHOD => { let params = cast_request::(request)?; let result = self.format(params); match result { Ok(text_edit) => { let result = serde_json::to_value(text_edit)?; Ok(lsp_server::Response { id, error: None, result: Some(result), }) } 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(ServerError::UnsupportedLspRequest { request: unsupported.to_string(), }), } } fn handle_notification( &mut self, _connection: &lsp_server::Connection, notification: lsp_server::Notification, ) -> Result<(), ServerError> { match notification.method.as_str() { DidSaveTextDocument::METHOD => { let params = cast_notification::(notification)?; self.edited.remove(params.text_document.uri.path()); Ok(()) } DidChangeTextDocument::METHOD => { let params = cast_notification::(notification)?; // A file has changed in the editor so store a copy of the new content in memory let path = params.text_document.uri.path().to_string(); if let Some(changes) = params.content_changes.into_iter().next() { self.edited.insert(path, changes.text); } Ok(()) } _ => Ok(()), } } fn format(&mut self, params: DocumentFormattingParams) -> Result, ProjectError> { let path = params.text_document.uri.path(); let mut new_text = String::new(); match self.edited.get(path) { Some(src) => { let (module, extra) = parser::module(src, ModuleKind::Lib).map_err(|errs| { aiken_project::error::Error::from_parse_errors(errs, Path::new(path), src) })?; aiken_lang::format::pretty(&mut new_text, module, extra, src); } None => { let src = fs::read_to_string(path)?; let (module, extra) = parser::module(&src, ModuleKind::Lib).map_err(|errs| { aiken_project::error::Error::from_parse_errors(errs, Path::new(path), &src) })?; aiken_lang::format::pretty(&mut new_text, module, extra, &src); } } 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 where R: lsp_types::request::Request, R::Params: serde::de::DeserializeOwned, { let (_, params) = request.extract(R::METHOD)?; Ok(params) } fn cast_notification(notification: lsp_server::Notification) -> Result where N: lsp_types::notification::Notification, N::Params: serde::de::DeserializeOwned, { let params = notification.extract::(N::METHOD)?; Ok(params) } fn text_edit_replace(new_text: String) -> TextEdit { TextEdit { range: lsp_types::Range { start: lsp_types::Position { line: 0, character: 0, }, end: lsp_types::Position { line: u32::MAX, character: 0, }, }, 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) }