From 123e729137cbd26d4c18cbaa7df9dbbdf3c8ccd6 Mon Sep 17 00:00:00 2001 From: rvcas Date: Mon, 14 Nov 2022 14:08:18 -0500 Subject: [PATCH] fix: when formatting we need to use the in memory edited version --- crates/lsp/src/error.rs | 29 +++++++ crates/lsp/src/lib.rs | 155 +++++++--------------------------- crates/lsp/src/server.rs | 178 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 126 deletions(-) create mode 100644 crates/lsp/src/error.rs create mode 100644 crates/lsp/src/server.rs diff --git a/crates/lsp/src/error.rs b/crates/lsp/src/error.rs new file mode 100644 index 00000000..5ee30d61 --- /dev/null +++ b/crates/lsp/src/error.rs @@ -0,0 +1,29 @@ +use std::io; + +use crossbeam_channel::SendError; +use lsp_server::{ExtractError, Message}; + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error(transparent)] + #[diagnostic(code(aiken::lsp::server_capabilities))] + ServerCapabilities(#[from] serde_json::Error), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::server_init))] + ServerInit(#[from] lsp_server::ProtocolError), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::io))] + Io(#[from] io::Error), + #[error("Unsupported LSP request: {request}")] + #[diagnostic(code(aiken::lsp::unsupported_lsp_request))] + UnsupportedLspRequest { request: String }, + #[error(transparent)] + #[diagnostic(code(aiken::lsp::cast_request))] + CastRequest(#[from] ExtractError), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::cast_notification))] + CastNotification(#[from] ExtractError), + #[error(transparent)] + #[diagnostic(code(aiken::lsp::send))] + Send(#[from] SendError), +} diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index f60a2cde..9f34b819 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -1,34 +1,15 @@ -use std::{fs, io, path::Path}; - -use aiken_lang::{ast::ModuleKind, parser}; -use crossbeam_channel::SendError; -use lsp_server::{Connection, ExtractError, Message}; +use lsp_server::Connection; use lsp_types::{ - request::{Formatting, Request}, - DocumentFormattingParams, InitializeParams, OneOf, ServerCapabilities, TextEdit, + OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, TextDocumentSyncSaveOptions, }; -#[derive(Debug, thiserror::Error, miette::Diagnostic)] -pub enum Error { - #[error(transparent)] - #[diagnostic(code(aiken::lsp::server_capabilities))] - ServerCapabilities(#[from] serde_json::Error), - #[error(transparent)] - #[diagnostic(code(aiken::lsp::server_init))] - ServerInit(#[from] lsp_server::ProtocolError), - #[error(transparent)] - #[diagnostic(code(aiken::lsp::io))] - Io(#[from] io::Error), - #[error("Unsupported LSP request: {request}")] - #[diagnostic(code(aiken::lsp::unsupported_lsp_request))] - UnsupportedLspRequest { request: String }, - #[error(transparent)] - #[diagnostic(code(aiken::lsp::cast_request))] - CastRequest(#[from] ExtractError), - #[error(transparent)] - #[diagnostic(code(aiken::lsp::send))] - Send(#[from] SendError), -} +pub mod error; +pub mod server; + +use error::Error; + +use crate::server::Server; pub fn start() -> Result<(), Error> { tracing::info!("Aiken language server starting"); @@ -38,16 +19,14 @@ pub fn start() -> Result<(), Error> { let (connection, io_threads) = Connection::stdio(); // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). - let server_capabilities = serde_json::to_value(&ServerCapabilities { - definition_provider: Some(OneOf::Left(true)), - document_formatting_provider: Some(OneOf::Left(true)), - ..Default::default() - })?; + let server_capabilities = serde_json::to_value(&capabilities())?; let initialization_params = connection.initialize(server_capabilities)?; - let initialization_params = serde_json::from_value(initialization_params)?; + let initialize_params = serde_json::from_value(initialization_params)?; - main_loop(connection, initialization_params)?; + let mut server = Server::new(initialize_params, None); + + server.listen(connection)?; io_threads.join()?; @@ -56,97 +35,21 @@ pub fn start() -> Result<(), Error> { Ok(()) } -fn main_loop(connection: Connection, _params: InitializeParams) -> Result<(), Error> { - 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 = handle_request(req)?; - - connection.sender.send(Message::Response(response))?; - } - Message::Response(_) => todo!(), - Message::Notification(notif) => tracing::debug!("Get notification: {:#?}", notif), - } - } - - Ok(()) -} - -fn handle_request(request: lsp_server::Request) -> Result { - let id = request.id.clone(); - - match request.method.as_str() { - Formatting::METHOD => { - let params = cast_request::(request)?; - - let result = 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(_) => { - todo!("transform project errors in lsp diagnostic") - } - } - } - unsupported => Err(Error::UnsupportedLspRequest { - request: unsupported.to_string(), - }), - } -} - -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 format(params: DocumentFormattingParams) -> Result, aiken_project::error::Error> { - let path = params.text_document.uri.path(); - let mut new_text = String::new(); - - 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)]) -} - -fn text_edit_replace(new_text: String) -> TextEdit { - TextEdit { - range: lsp_types::Range { - start: lsp_types::Position { - line: 0, - character: 0, +fn capabilities() -> ServerCapabilities { + ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: None, + change: Some(TextDocumentSyncKind::FULL), + will_save: None, + will_save_wait_until: None, + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { + include_text: Some(false), + })), }, - end: lsp_types::Position { - line: u32::MAX, - character: 0, - }, - }, - new_text, + )), + definition_provider: Some(OneOf::Left(true)), + document_formatting_provider: Some(OneOf::Left(true)), + ..Default::default() } } diff --git a/crates/lsp/src/server.rs b/crates/lsp/src/server.rs new file mode 100644 index 00000000..702dc07f --- /dev/null +++ b/crates/lsp/src/server.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, fs, path::Path}; + +use aiken_lang::{ast::ModuleKind, parser}; +use aiken_project::config; +use lsp_server::{Connection, Message}; +use lsp_types::{ + notification::{DidChangeTextDocument, Notification}, + request::{Formatting, Request}, + DocumentFormattingParams, InitializeParams, TextEdit, +}; + +use crate::error::Error; + +#[allow(dead_code)] +pub struct Server { + config: Option, + + /// Files that have been edited in memory + edited: HashMap, + + initialize_params: InitializeParams, +} + +impl Server { + pub fn new(initialize_params: InitializeParams, config: Option) -> Self { + Self { + config, + edited: HashMap::new(), + initialize_params, + } + } + + pub fn listen(&mut self, connection: Connection) -> Result<(), Error> { + 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)?; + + 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(_) => { + todo!("transform project errors in lsp diagnostic") + } + } + } + unsupported => Err(Error::UnsupportedLspRequest { + request: unsupported.to_string(), + }), + } + } + + fn handle_notification( + &mut self, + _connection: &lsp_server::Connection, + notification: lsp_server::Notification, + ) -> Result<(), Error> { + match notification.method.as_str() { + 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() { + let _ = self.edited.insert(path, changes.text); + } + + Ok(()) + } + _ => Ok(()), + } + } + + fn format( + &mut self, + params: DocumentFormattingParams, + ) -> Result, aiken_project::error::Error> { + 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)]) + } +} + +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, + } +}