From 1ee7922b85173ff5e130f30c90ba7963b6f1ac5d Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Tue, 10 Oct 2023 17:22:23 +1100 Subject: [PATCH] feat: workspace/didChangeConfiguration --- README.md | 2 +- src/backend/language_server.rs | 42 ++++++++++++- src/backend/mod.rs | 111 +++++++++++++++++++++++++++++---- 3 files changed, 141 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a721761..5fbaecd 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Language Server Protocol implementation for nushell and [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen) - [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics) -> `nu --ide-check` - [x] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration) +- [x] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration) - [ ] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check` -- [ ] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration) - [ ] raise a PR for `vscode-nushell-lang` to replace its wrapper/glue code with `nuls` ### stretch goals diff --git a/src/backend/language_server.rs b/src/backend/language_server.rs index 3dfe4f1..769ea57 100644 --- a/src/backend/language_server.rs +++ b/src/backend/language_server.rs @@ -6,10 +6,10 @@ use crate::{ nu::{run_compiler, IdeComplete, IdeGotoDef, IdeHover}, }; -use tower_lsp::jsonrpc::Result; #[allow(clippy::wildcard_imports)] use tower_lsp::lsp_types::*; -use tower_lsp::LanguageServer; +use tower_lsp::{jsonrpc::Result, lsp_types::notification::DidChangeConfiguration}; +use tower_lsp::{lsp_types::notification::Notification, LanguageServer}; #[tower_lsp::async_trait] impl LanguageServer for Backend { @@ -27,6 +27,14 @@ impl LanguageServer for Backend { }; } + async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { + if let Err(e) = self.try_did_change_configuration(params).await { + self.client + .log_message(MessageType::ERROR, format!("{e:?}")) + .await; + } + } + async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { self.client .log_message( @@ -65,6 +73,16 @@ impl LanguageServer for Backend { // panic: this is the only place we `OnceLock::set`, // so we've entered strange territory if something else writes to them first + self.can_change_configuration + .set(matches!( + params.capabilities.workspace, + Some(WorkspaceClientCapabilities { + did_change_configuration: Some(_), + .. + }) + )) + .expect("server value initialized out of sequence"); + self.can_lookup_configuration .set(matches!( params.capabilities.workspace, @@ -116,6 +134,26 @@ impl LanguageServer for Backend { } async fn initialized(&self, _params: InitializedParams) { + if *self.can_change_configuration.get().unwrap_or(&false) { + let method = String::from(DidChangeConfiguration::METHOD); + if let Err(e) = self + .client + .register_capability(vec![Registration { + id: method.clone(), + method, + register_options: None, + }]) + .await + { + self.client + .log_message( + MessageType::INFO, + format!("unable to register capability: {e:?}"), + ) + .await; + }; + } + self.client .log_message(MessageType::INFO, "server initialized!") .await; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 8906dce..11e4ccc 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::OnceLock; use std::time::{Duration, Instant}; use std::{ffi::OsStr, sync::RwLock}; @@ -9,6 +10,7 @@ use crate::{ }; use lsp_textdocument::{FullTextDocument, TextDocuments}; +use serde::Deserialize; use tower_lsp::lsp_types::notification::{ DidChangeTextDocument, DidCloseTextDocument, Notification, }; @@ -18,10 +20,13 @@ use tower_lsp::Client; use tower_lsp::{jsonrpc::Result, lsp_types::notification::DidOpenTextDocument}; pub(crate) struct Backend { + can_change_configuration: OnceLock, can_lookup_configuration: OnceLock, can_publish_diagnostics: OnceLock, client: Client, documents: RwLock, + document_settings: RwLock>, + global_settings: RwLock, last_validated: RwLock, } @@ -42,29 +47,73 @@ impl Backend { } async fn get_document_settings(&self, uri: &Url) -> Result { - if *self.can_lookup_configuration.get().unwrap_or(&false) { - let values = self - .client - .configuration(vec![ConfigurationItem { - scope_uri: Some(uri.clone()), - section: Some(String::from("nushellLanguageServer")), - }]) - .await?; - - if let Some(value) = values.into_iter().next() { - return Ok(serde_json::from_value::(value).unwrap_or_default()); + if !self.can_lookup_configuration.get().unwrap_or(&false) { + self.client + .log_message( + MessageType::INFO, + "no per-document settings lookup capability, returning global settings ...", + ) + .await; + let global_settings = self.global_settings.read().map_err(|e| { + tower_lsp::jsonrpc::Error::invalid_params(format!( + "cannot read global settings: {e:?}" + )) + })?; + return Ok(global_settings.clone()); + } + + { + self.client + .log_message( + MessageType::INFO, + "checking per-document settings cache ...", + ) + .await; + let document_settings = self.document_settings.read().map_err(|e| { + map_err_to_internal_error(&e, format!("cannot read per-document settings: {e:?}")) + })?; + if let Some(settings) = document_settings.get(uri) { + return Ok(settings.clone()); } } + self.client + .log_message( + MessageType::INFO, + "fetching per-document settings for cache ...", + ) + .await; + let values = self + .client + .configuration(vec![ConfigurationItem { + scope_uri: Some(uri.clone()), + section: Some(String::from("nushellLanguageServer")), + }]) + .await?; + if let Some(value) = values.into_iter().next() { + let settings: IdeSettings = serde_json::from_value(value).unwrap_or_default(); + let mut document_settings = self.document_settings.write().map_err(|e| { + map_err_to_internal_error(&e, format!("cannot write per-document settings: {e:?}")) + })?; + document_settings.insert(uri.clone(), settings.clone()); + return Ok(settings); + } + + self.client + .log_message(MessageType::INFO, "fallback, returning default settings") + .await; Ok(IdeSettings::default()) } pub fn new(client: Client) -> Self { Self { + can_change_configuration: OnceLock::new(), can_lookup_configuration: OnceLock::new(), can_publish_diagnostics: OnceLock::new(), client, documents: RwLock::new(TextDocuments::new()), + document_settings: RwLock::new(HashMap::new()), + global_settings: RwLock::new(IdeSettings::default()), last_validated: RwLock::new(Instant::now()), } } @@ -103,6 +152,40 @@ impl Backend { Ok(()) } + async fn try_did_change_configuration( + &self, + params: DidChangeConfigurationParams, + ) -> Result<()> { + if *self.can_lookup_configuration.get().unwrap_or(&false) { + let mut document_settings = self.document_settings.write().map_err(|e| { + map_err_to_internal_error(&e, format!("cannot write per-document settings: {e:?}")) + })?; + document_settings.clear(); + } else { + let settings: ClientSettingsPayload = + serde_json::from_value(params.settings).unwrap_or_default(); + let mut global_settings = self.global_settings.write().map_err(|e| { + map_err_to_internal_error(&e, format!("cannot write global settings: {e:?}")) + })?; + *global_settings = settings.nushell_language_server; + } + + // Revalidate all open text documents + let uris: Vec = { + let documents = self.documents.read().map_err(|e| { + tower_lsp::jsonrpc::Error::invalid_params(format!( + "cannot read from document cache: {e:?}" + )) + })?; + documents.documents().keys().cloned().collect() + }; + for uri in uris { + self.validate_document(&uri).await?; + } + + Ok(()) + } + fn try_did_close(&self, params: DidCloseTextDocumentParams) -> Result<()> { let mut documents = self.documents.write().map_err(|e| { map_err_to_internal_error(&e, format!("cannot write to document cache: {e:?}")) @@ -174,3 +257,9 @@ impl Backend { Ok(()) } } + +#[derive(Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct ClientSettingsPayload { + nushell_language_server: IdeSettings, +}