From 697960dfa1d31b547d713c9981b7ebc923026690 Mon Sep 17 00:00:00 2001 From: Alessandro Scandone Date: Tue, 25 Jun 2024 03:47:04 +0200 Subject: [PATCH] implemented config functionality --- compiler-core/src/language_server.rs | 1 + .../src/language_server/configuration.rs | 32 +++++ compiler-core/src/language_server/engine.rs | 15 ++ compiler-core/src/language_server/messages.rs | 80 ++++++++++- compiler-core/src/language_server/router.rs | 28 ++-- compiler-core/src/language_server/server.rs | 130 +++++++++++++++--- compiler-core/src/language_server/tests.rs | 12 +- 7 files changed, 268 insertions(+), 30 deletions(-) create mode 100644 compiler-core/src/language_server/configuration.rs diff --git a/compiler-core/src/language_server.rs b/compiler-core/src/language_server.rs index 0d03c56f9b5..9dacae0f2cf 100644 --- a/compiler-core/src/language_server.rs +++ b/compiler-core/src/language_server.rs @@ -1,6 +1,7 @@ mod code_action; mod compiler; mod completer; +mod configuration; mod engine; mod feedback; mod files; diff --git a/compiler-core/src/language_server/configuration.rs b/compiler-core/src/language_server/configuration.rs new file mode 100644 index 00000000000..269fcb949d3 --- /dev/null +++ b/compiler-core/src/language_server/configuration.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; +use std::sync::{Arc, RwLock}; + +pub type SharedConfig = Arc>; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Configuration { + #[serde(default = "InlayHintsConfig::default")] + pub inlay_hints: InlayHintsConfig, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsConfig { + #[serde(default = "InlayHintsConfig::default_pipelines")] + pub pipelines: bool, +} + +impl Default for InlayHintsConfig { + fn default() -> Self { + Self { + pipelines: Self::default_pipelines(), + } + } +} + +impl InlayHintsConfig { + fn default_pipelines() -> bool { + false + } +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 60cdb7b21ab..a4a7c63ab96 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -25,6 +25,7 @@ use std::sync::Arc; use super::{ code_action::{CodeActionBuilder, RedundantTupleInCaseSubject}, completer::Completer, + configuration::SharedConfig, src_offset_to_lsp_position, src_span_to_lsp_range, DownloadDependencies, MakeLocker, }; @@ -63,6 +64,9 @@ pub struct LanguageServerEngine { /// Used to know if to show the "View on HexDocs" link /// when hovering on an imported value hex_deps: std::collections::HashSet, + + /// Configuration the user has set in their editor. + pub(crate) user_config: SharedConfig, } impl<'a, IO, Reporter> LanguageServerEngine @@ -82,6 +86,7 @@ where progress_reporter: Reporter, io: FileSystemProxy, paths: ProjectPaths, + user_config: SharedConfig, ) -> Result { let locker = io.inner().make_locker(&paths, config.target)?; @@ -117,6 +122,7 @@ where compiler, paths, hex_deps, + user_config, }) } @@ -268,6 +274,15 @@ where pub fn inlay_hints(&mut self, params: lsp::InlayHintParams) -> Response> { self.respond(|this| { + let Ok(config) = this.user_config.read() else { + // TODO trace? + return Ok(vec![]); + }; + + if !config.inlay_hints.pipelines { + return Ok(vec![]); + } + let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(vec![]); }; diff --git a/compiler-core/src/language_server/messages.rs b/compiler-core/src/language_server/messages.rs index 8d8159e2a44..c8e17cd3e1a 100644 --- a/compiler-core/src/language_server/messages.rs +++ b/compiler-core/src/language_server/messages.rs @@ -1,3 +1,4 @@ +use crate::language_server::configuration::Configuration; use camino::Utf8PathBuf; use lsp::{ notification::{DidChangeWatchedFiles, DidOpenTextDocument}, @@ -8,11 +9,12 @@ use lsp_types::{ notification::{DidChangeTextDocument, DidCloseTextDocument, DidSaveTextDocument}, request::{CodeActionRequest, Completion, Formatting, HoverRequest, InlayHintRequest}, }; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; #[derive(Debug)] pub enum Message { Request(lsp_server::RequestId, Request), + Response(Response), Notification(Notification), } @@ -26,6 +28,11 @@ pub enum Request { ShowInlayHints(lsp::InlayHintParams), } +#[derive(Debug)] +pub enum Response { + Configuration(Configuration), +} + impl Request { fn extract(request: lsp_server::Request) -> Option { let id = request.id.clone(); @@ -67,6 +74,8 @@ pub enum Notification { SourceFileMatchesDisc { path: Utf8PathBuf }, /// gleam.toml has changed. ConfigFileChanged { path: Utf8PathBuf }, + /// The user edited a client config option + ConfigChanged, /// It's time to compile all open projects. CompilePlease, } @@ -113,6 +122,11 @@ impl Notification { }; Some(Message::Notification(notification)) } + + "workspace/didChangeConfiguration" => { + Some(Message::Notification(Notification::ConfigChanged)) + } + _ => None, } } @@ -130,15 +144,19 @@ pub enum Next { /// - 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. -/// +#[derive(Debug)] pub struct MessageBuffer { messages: Vec, + next_request_id: i32, + response_handlers: HashMap, } impl MessageBuffer { pub fn new() -> Self { Self { messages: Vec::new(), + next_request_id: 1, + response_handlers: Default::default(), } } @@ -198,7 +216,58 @@ impl MessageBuffer { Next::MorePlease } - fn response(&mut self, _: lsp_server::Response) -> Next { + pub fn make_request( + &mut self, + method: impl Into, + params: impl serde::Serialize, + handler: Option, + ) -> lsp_server::Request { + let id = self.next_request_id; + self.next_request_id += 1; + let request = lsp_server::Request { + id: id.into(), + method: method.into(), + params: serde_json::value::to_value(params).expect("serialisation should never fail"), + }; + + if let Some(handler) = handler { + _ = self.response_handlers.insert(id.into(), handler); + } + + request + } + + fn configuration_update_received(&mut self, result: serde_json::Value) -> Next { + let Some(first_el) = result.as_array().and_then(|a| a.first()) else { + return Next::MorePlease; + }; + + let parsed_config_result: Result = + serde_json::from_value(first_el.clone()); + + let Ok(parsed_config) = parsed_config_result else { + return Next::MorePlease; + }; + + let message = Message::Response(Response::Configuration(parsed_config)); + self.messages.push(message); + + Next::Handle(self.take_messages()) + } + + fn handle_response(&mut self, handler: ResponseHandler, result: serde_json::Value) -> Next { + match handler { + ResponseHandler::UpdateConfiguration => self.configuration_update_received(result), + } + } + + fn response(&mut self, response: lsp_server::Response) -> Next { + if let Some(handler) = self.response_handlers.remove(&response.id) { + if let Some(result) = response.result { + return self.handle_response(handler, result); + } + } + // We do not use or expect responses from the client currently. Next::MorePlease } @@ -243,3 +312,8 @@ where .extract::(N::METHOD) .expect("cast notification") } + +#[derive(Debug)] +pub enum ResponseHandler { + UpdateConfiguration, +} diff --git a/compiler-core/src/language_server/router.rs b/compiler-core/src/language_server/router.rs index 6f180edf80a..caaf9b24f5e 100644 --- a/compiler-core/src/language_server/router.rs +++ b/compiler-core/src/language_server/router.rs @@ -1,3 +1,4 @@ +use super::{configuration::SharedConfig, feedback::FeedbackBookKeeper}; use crate::{ build::SourceFingerprint, error::{FileIoAction, FileKind}, @@ -9,15 +10,12 @@ use crate::{ paths::ProjectPaths, Error, Result, }; +use camino::{Utf8Path, Utf8PathBuf}; use std::{ collections::{hash_map::Entry, HashMap}, time::SystemTime, }; -use camino::{Utf8Path, Utf8PathBuf}; - -use super::feedback::FeedbackBookKeeper; - /// The language server instance serves a language client, typically a text /// editor. The editor could have multiple Gleam projects open at once, so run /// an instance of the language server engine for each project. @@ -30,6 +28,7 @@ pub(crate) struct Router { io: FileSystemProxy, engines: HashMap>, progress_reporter: Reporter, + user_config: SharedConfig, } impl<'a, IO, Reporter> Router @@ -44,11 +43,16 @@ where // IO to be supplied from inside of gleam-core Reporter: ProgressReporter + Clone + 'a, { - pub fn new(progress_reporter: Reporter, io: FileSystemProxy) -> Self { + pub fn new( + progress_reporter: Reporter, + io: FileSystemProxy, + user_config: SharedConfig, + ) -> Self { Self { io, engines: HashMap::new(), progress_reporter, + user_config, } } @@ -84,8 +88,12 @@ where Ok(Some(match self.engines.entry(path.clone()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - let project = - Self::new_project(path, self.io.clone(), self.progress_reporter.clone())?; + let project = Self::new_project( + path, + self.io.clone(), + self.progress_reporter.clone(), + self.user_config.clone(), + )?; entry.insert(project) } })) @@ -123,19 +131,21 @@ where path: Utf8PathBuf, io: FileSystemProxy, progress_reporter: Reporter, + user_config: SharedConfig, ) -> Result, Error> { tracing::info!(?path, "creating_new_language_server_engine"); let paths = ProjectPaths::new(path); let config_path = paths.root_config(); let modification_time = io.modification_time(&config_path)?; let toml = io.read(&config_path)?; - let config = toml::from_str(&toml).map_err(|e| Error::FileIo { + let package_config = toml::from_str(&toml).map_err(|e| Error::FileIo { action: FileIoAction::Parse, kind: FileKind::File, path: config_path, err: Some(e.to_string()), })?; - let engine = LanguageServerEngine::new(config, progress_reporter, io, paths)?; + let engine = + LanguageServerEngine::new(package_config, progress_reporter, io, paths, user_config)?; let project = Project { engine, feedback: FeedbackBookKeeper::default(), diff --git a/compiler-core/src/language_server/server.rs b/compiler-core/src/language_server/server.rs index 266e367e825..f0bd3417eff 100644 --- a/compiler-core/src/language_server/server.rs +++ b/compiler-core/src/language_server/server.rs @@ -1,5 +1,6 @@ use super::{ - messages::{Message, MessageBuffer, Next, Notification, Request}, + configuration::SharedConfig, + messages::{Message, MessageBuffer, Next, Notification, Request, Response, ResponseHandler}, progress::ConnectionProgressReporter, }; use crate::{ @@ -18,8 +19,8 @@ use crate::{ use camino::{Utf8Path, Utf8PathBuf}; use debug_ignore::DebugIgnore; use lsp_types::{ - self as lsp, HoverProviderCapability, InitializeParams, Position, PublishDiagnosticsParams, - Range, TextEdit, Url, + self as lsp, ConfigurationItem, HoverProviderCapability, InitializeParams, Position, + PublishDiagnosticsParams, Range, TextEdit, Url, }; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -42,6 +43,8 @@ pub struct LanguageServer<'a, IO> { router: Router>, changed_projects: HashSet, io: FileSystemProxy, + message_buffer: MessageBuffer, + config: SharedConfig, } impl<'a, IO> LanguageServer<'a, IO> @@ -57,23 +60,29 @@ where let initialise_params = initialisation_handshake(connection); let reporter = ConnectionProgressReporter::new(connection, &initialise_params); let io = FileSystemProxy::new(io); - let router = Router::new(reporter, io.clone()); + + let config = SharedConfig::default(); + let router = Router::new(reporter, io.clone(), config.clone()); + Ok(Self { connection: connection.into(), initialise_params, changed_projects: HashSet::new(), outside_of_project_feedback: FeedbackBookKeeper::default(), + message_buffer: MessageBuffer::new(), router, io, + config, }) } pub fn run(&mut self) -> Result<()> { self.start_watching_gleam_toml(); - let mut buffer = MessageBuffer::new(); + self.start_watching_config(); + let _ = self.request_configuration(); loop { - match buffer.receive(*self.connection) { + match self.message_buffer.receive(*self.connection) { Next::Stop => break, Next::MorePlease => (), Next::Handle(messages) => { @@ -91,9 +100,37 @@ where match message { Message::Request(id, request) => self.handle_request(id, request), Message::Notification(notification) => self.handle_notification(notification), + Message::Response(response) => self.handle_response(response), } } + fn handle_response(&mut self, response: Response) { + match response { + Response::Configuration(updated_config) => { + { + let mut config = self.config.write().expect("cannot write config"); + *config = updated_config; + } + + let _ = self.inlay_hints_refresh(); + } + } + } + + fn send_request( + &mut self, + method: &str, + params: impl serde::Serialize, + handler: Option, + ) { + let request = self.message_buffer.make_request(method, params, handler); + + self.connection + .sender + .send(lsp_server::Message::Request(request)) + .unwrap_or_else(|_| panic!("send {method}")); + } + fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) { let (payload, feedback) = match request { Request::Format(param) => self.format(param), @@ -125,6 +162,7 @@ where self.cache_file_in_memory(path, text) } Notification::ConfigFileChanged { path } => self.watched_files_changed(path), + Notification::ConfigChanged => self.request_configuration(), }; self.publish_feedback(feedback); } @@ -160,6 +198,35 @@ where } } + fn start_watching_config(&mut self) { + let supports_configuration = self + .initialise_params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.did_change_configuration) + .map(|wf| wf.dynamic_registration == Some(true)) + .unwrap_or(false); + + if !supports_configuration { + tracing::warn!("lsp_client_cannot_watch_configuration"); + return; + } + + let watch_config = lsp::Registration { + id: "watch-user-configuration".into(), + method: "workspace/didChangeConfiguration".into(), + register_options: None, + }; + self.send_request( + "client/registerCapability", + lsp::RegistrationParams { + registrations: vec![watch_config], + }, + None, + ); + } + fn start_watching_gleam_toml(&mut self) { let supports_watch_files = self .initialise_params @@ -190,18 +257,14 @@ where .expect("workspace/didChangeWatchedFiles to json"), ), }; - let request = lsp_server::Request { - id: 1.into(), - method: "client/registerCapability".into(), - params: serde_json::value::to_value(lsp::RegistrationParams { + + self.send_request( + "client/registerCapability", + lsp::RegistrationParams { registrations: vec![watch_config], - }) - .expect("client/registerCapability to json"), - }; - self.connection - .sender - .send(lsp_server::Message::Request(request)) - .expect("send client/registerCapability"); + }, + None, + ); } fn publish_messages(&self, messages: Vec) { @@ -261,6 +324,39 @@ where } } + fn inlay_hints_refresh(&mut self) -> Feedback { + self.send_request("workspace/inlayHint/refresh", (), None); + Feedback::default() + } + + fn request_configuration(&mut self) -> Feedback { + let supports_configuration = self + .initialise_params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.configuration) + .unwrap_or(false); + + if !supports_configuration { + tracing::warn!("lsp_client_cannot_request_configuration"); + return Feedback::default(); + } + + self.send_request( + "workspace/configuration", + lsp::ConfigurationParams { + items: vec![ConfigurationItem { + scope_uri: None, + section: Some("gleam".into()), + }], + }, + Some(ResponseHandler::UpdateConfiguration), + ); + + Feedback::default() + } + fn path_error_response(&mut self, path: Utf8PathBuf, error: crate::Error) -> (Json, Feedback) { let feedback = match self.router.project_for_path(path) { Ok(Some(project)) => project.feedback.error(error), diff --git a/compiler-core/src/language_server/tests.rs b/compiler-core/src/language_server/tests.rs index d0132ed7271..d453e35acd2 100644 --- a/compiler-core/src/language_server/tests.rs +++ b/compiler-core/src/language_server/tests.rs @@ -7,7 +7,7 @@ mod inlay_hints; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, time::SystemTime, }; @@ -33,6 +33,8 @@ use crate::{ Result, }; +use super::configuration::{Configuration, InlayHintsConfig}; + pub const LSP_TEST_ROOT_PACKAGE_NAME: &str = "app"; #[derive(Debug, Clone, PartialEq, Eq)] @@ -350,6 +352,13 @@ fn add_path_dep(engine: &mut LanguageServerEngine, n ) } +fn default_test_config() -> Configuration { + Configuration { + path: None, + inlay_hints: InlayHintsConfig { pipelines: true }, + } +} + fn setup_engine( io: &LanguageServerTestIO, ) -> LanguageServerEngine { @@ -360,6 +369,7 @@ fn setup_engine( io.clone(), FileSystemProxy::new(io.clone()), io.paths.clone(), + Arc::new(RwLock::new(default_test_config())), ) .unwrap() }