// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Gleam contributors use camino::Utf8PathBuf; use lsp::{DefinitionRequest, DidChangeWatchedFilesNotification, DidOpenTextDocumentNotification}; use lsp_types::{ self as lsp, CodeActionRequest, CompletionRequest, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidSaveTextDocumentNotification, DocumentFormattingRequest, DocumentHighlightRequest, DocumentSymbolRequest, FoldingRangeRequest, HoverRequest, PrepareRenameRequest, ReferencesRequest, RenameRequest, SignatureHelpRequest, TextDocumentContentChangeEvent, TypeDefinitionRequest, WillRenameFilesRequest, }; use std::time::Duration; #[derive(Debug)] pub enum Message { Request(lsp_server::RequestId, Request), Notification(Notification), } #[derive(Debug)] pub enum Request { Format(lsp::DocumentFormattingParams), Hover(lsp::HoverParams), GoToDefinition(lsp::DefinitionParams), GoToTypeDefinition(lsp::TypeDefinitionParams), Completion(lsp::CompletionParams), CodeAction(lsp::CodeActionParams), SignatureHelp(lsp::SignatureHelpParams), DocumentSymbol(lsp::DocumentSymbolParams), FoldingRange(lsp::FoldingRangeParams), PrepareRename(lsp::PrepareRenameParams), Rename(lsp::RenameParams), FindReferences(lsp::ReferenceParams), DocumentHighlight(lsp::DocumentHighlightParams), RenameFiles(lsp::RenameFilesParams), } impl Request { fn extract(request: lsp_server::Request) -> Option { let id = request.id.clone(); match request.method.as_str() { "textDocument/formatting" => { let params = cast_request::(request); Some(Message::Request(id, Request::Format(params))) } "textDocument/hover" => { let params = cast_request::(request); Some(Message::Request(id, Request::Hover(params))) } "textDocument/definition" => { let params = cast_request::(request); Some(Message::Request(id, Request::GoToDefinition(params))) } "textDocument/completion" => { let params = cast_request::(request); Some(Message::Request(id, Request::Completion(params))) } "textDocument/codeAction" => { let params = cast_request::(request); Some(Message::Request(id, Request::CodeAction(params))) } "textDocument/signatureHelp" => { let params = cast_request::(request); Some(Message::Request(id, Request::SignatureHelp(params))) } "textDocument/documentSymbol" => { let params = cast_request::(request); Some(Message::Request(id, Request::DocumentSymbol(params))) } "textDocument/foldingRange" => { let params = cast_request::(request); Some(Message::Request(id, Request::FoldingRange(params))) } "textDocument/rename" => { let params = cast_request::(request); Some(Message::Request(id, Request::Rename(params))) } "textDocument/prepareRename" => { let params = cast_request::(request); Some(Message::Request(id, Request::PrepareRename(params))) } "textDocument/typeDefinition" => { let params = cast_request::(request); Some(Message::Request(id, Request::GoToTypeDefinition(params))) } "textDocument/references" => { let params = cast_request::(request); Some(Message::Request(id, Request::FindReferences(params))) } "textDocument/documentHighlight" => { let params = cast_request::(request); Some(Message::Request(id, Request::DocumentHighlight(params))) } "workspace/willRenameFiles" => { let params = cast_request::(request); Some(Message::Request(id, Request::RenameFiles(params))) } _ => None, } } } #[derive(Debug)] pub enum Notification { /// A Gleam file has been modified in memory, and the new text is provided. SourceFileChangedInMemory { path: Utf8PathBuf, text: String }, /// A Gleam file has been opened in the editor. SourceFileOpened { path: Utf8PathBuf, text: String }, /// A Gleam file has been closed in the editor. SourceFileClosed { path: Utf8PathBuf }, /// A gleam file has been saved to disk SourceFileSaved { path: Utf8PathBuf }, /// gleam.toml has changed. ConfigFileChanged { path: Utf8PathBuf }, /// It's time to compile all open projects. CompilePlease, } impl Notification { fn extract(notification: lsp_server::Notification) -> Option { match notification.method.as_str() { "textDocument/didOpen" => { let params = cast_notification::(notification); let notification = Notification::SourceFileOpened { path: super::path(¶ms.text_document.uri), text: params.text_document.text, }; Some(Message::Notification(notification)) } "textDocument/didChange" => { let params = cast_notification::(notification); let TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(change) = params.content_changes.into_iter().next_back()? else { return None; }; let notification = Notification::SourceFileChangedInMemory { path: super::path(¶ms.text_document.text_document_identifier.uri), text: change.text, }; Some(Message::Notification(notification)) } "textDocument/didSave" => { let params = cast_notification::(notification); let notification = Notification::SourceFileSaved { path: super::path(¶ms.text_document.uri), }; Some(Message::Notification(notification)) } "textDocument/didClose" => { let params = cast_notification::(notification); let notification = Notification::SourceFileClosed { path: super::path(¶ms.text_document.uri), }; Some(Message::Notification(notification)) } "workspace/didChangeWatchedFiles" => { let params = cast_notification::(notification); let notification = Notification::ConfigFileChanged { path: super::path(¶ms.changes.into_iter().next_back()?.uri), }; Some(Message::Notification(notification)) } _ => None, } } } pub enum Next { MorePlease, Handle(Vec), Stop, } /// The message buffer pulls messages from the client until one of the following /// happens: /// - A shutdown request is received. /// - A short pause in messages is detected, indicating the programmer has /// stopped typing for a moment and would benefit from feedback. /// - A request type message is received, which requires an immediate response. /// pub struct MessageBuffer { messages: Vec, } impl MessageBuffer { pub fn new() -> Self { Self { messages: Vec::new(), } } pub fn receive(&mut self, conn: &lsp_server::Connection) -> Next { let pause = Duration::from_millis(100); // If the buffer is empty, wait indefinitely for the first message. // If the buffer is not empty, wait for a short time to see if more messages are // coming before processing the ones we have. let message = if self.messages.is_empty() { Some(conn.receiver.recv().expect("Receiving LSP message")) } else { conn.receiver.recv_timeout(pause).ok() }; // If have have not received a message then it means there is a pause in the // messages from the client, implying the programmer has stopped typing. Process // the currently enqueued messages. let message = match message { Some(message) => message, None => { // A compile please message it added in the instance of this // pause of activity so that the client gets feedback on the // state of the code as it is now. self.push_compile_please_message(); return Next::Handle(self.take_messages()); } }; match message { lsp_server::Message::Request(r) if self.shutdown(conn, &r) => Next::Stop, lsp_server::Message::Request(r) => self.request(r), lsp_server::Message::Response(r) => self.response(r), lsp_server::Message::Notification(n) => self.notification(n), } } fn request(&mut self, r: lsp_server::Request) -> Next { let Some(message) = Request::extract(r) else { return Next::MorePlease; }; // Compile the code prior to attempting to process the response, to // ensure that the response is based on the latest code. self.push_compile_please_message(); self.messages.push(message); Next::Handle(self.take_messages()) } fn notification(&mut self, n: lsp_server::Notification) -> Next { // A new notification telling us that an edit has been made, or // something along those lines. if let Some(message) = Notification::extract(n) { self.messages.push(message); } // Ask for more messages (or a pause), at which point we'll start processing. Next::MorePlease } fn response(&mut self, _: lsp_server::Response) -> Next { // We do not use or expect responses from the client currently. Next::MorePlease } /// Add a `CompilePlease` message which will prompt the engine to compile /// the projects. /// fn push_compile_please_message(&mut self) { let message = Notification::CompilePlease; let value = Message::Notification(message); self.messages.push(value); } fn take_messages(&mut self) -> Vec { std::mem::take(&mut self.messages) } fn shutdown( &mut self, connection: &lsp_server::Connection, request: &lsp_server::Request, ) -> bool { connection.handle_shutdown(request).expect("LSP shutdown") } } fn cast_request(request: lsp_server::Request) -> R::Params where R: lsp::Request, R::Params: serde::de::DeserializeOwned, { let (_, params) = request.extract(R::METHOD.as_str()).expect("cast request"); params } fn cast_notification(notification: lsp_server::Notification) -> N::Params where N: lsp::Notification, N::Params: serde::de::DeserializeOwned, { notification .extract::(N::METHOD.as_str()) .expect("cast notification") }