From dfcd84272619832e9f8d0b183b69d5cb177871e1 Mon Sep 17 00:00:00 2001 From: ontley Date: Thu, 2 Nov 2023 11:09:47 +0100 Subject: [PATCH 1/8] Added required-root-patterns for situational lsp activation using globbing --- helix-core/src/syntax.rs | 3 ++ helix-lsp/src/client.rs | 21 +--------- helix-lsp/src/lib.rs | 88 ++++++++++++++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 881b45098a9e..aeb1e3ce613e 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -371,6 +371,9 @@ pub struct LanguageServerConfiguration { pub config: Option, #[serde(default = "default_timeout")] pub timeout: u64, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub required_root_patterns: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 341702c374ce..0d8465badb26 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -174,12 +174,11 @@ impl Client { args: &[String], config: Option, server_environment: HashMap, - root_markers: &[String], - manual_roots: &[PathBuf], + root_path: PathBuf, + root_uri: Option, id: usize, name: String, req_timeout: u64, - doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { // Resolve path to the binary let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; @@ -203,22 +202,6 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id, name.clone()); - let (workspace, workspace_is_cwd) = find_workspace(); - let workspace = path::get_normalized_path(&workspace); - let root = find_lsp_workspace( - doc_path - .and_then(|x| x.parent().and_then(|x| x.to_str())) - .unwrap_or("."), - root_markers, - manual_roots, - &workspace, - workspace_is_cwd, - ); - - // `root_uri` and `workspace_folder` can be empty in case there is no workspace - // `root_url` can not, use `workspace` as a fallback - let root_path = root.clone().unwrap_or_else(|| workspace.clone()); - let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); let workspace_folders = root_uri .clone() diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index b6a990659aaa..20205c51133c 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -672,7 +672,7 @@ impl Registry { doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result> { + ) -> Result>> { let config = self .syn_loader .language_server_configs() @@ -680,7 +680,7 @@ impl Registry { .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; let id = self.counter; self.counter += 1; - let NewClient(client, incoming) = start_client( + if let Some(NewClient(client, incoming)) = start_client( id, name, ls_config, @@ -688,9 +688,12 @@ impl Registry { doc_path, root_dirs, enable_snippets, - )?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - Ok(client) + )? { + self.incoming.push(UnboundedReceiverStream::new(incoming)); + Ok(Some(client)) + } else { + Ok(None) + } } /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, @@ -715,8 +718,8 @@ impl Registry { root_dirs, enable_snippets, ) { - Ok(client) => client, - error => return Some(error), + Ok(client) => client?, + Err(error) => return Some(Err(error)), }; let old_clients = self .inner @@ -756,13 +759,13 @@ impl Registry { root_dirs: &'a [PathBuf], enable_snippets: bool, ) -> impl Iterator>)> + 'a { - language_config.language_servers.iter().map( + language_config.language_servers.iter().filter_map( move |LanguageServerFeatures { name, .. }| { if let Some(clients) = self.inner.get(name) { if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) }) { - return (name.to_owned(), Ok(client.clone())); + return Some((name.to_owned(), Ok(client.clone()))); } } match self.start_client( @@ -773,13 +776,14 @@ impl Registry { enable_snippets, ) { Ok(client) => { + let client = client?; self.inner .entry(name.to_owned()) .or_default() .push(client.clone()); - (name.clone(), Ok(client)) + Some((name.clone(), Ok(client))) } - Err(err) => (name.to_owned(), Err(err)), + Err(err) => Some((name.to_owned(), Err(err))), } }, ) @@ -880,18 +884,70 @@ fn start_client( doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, -) -> Result { +) -> Result> { + let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); + let workspace = path::get_normalized_path(&workspace); + let root = find_lsp_workspace( + doc_path + .and_then(|x| x.parent().and_then(|x| x.to_str())) + .unwrap_or("."), + &config.roots, + config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), + &workspace, + workspace_is_cwd, + ); + + // `root_uri` and `workspace_folder` can be empty in case there is no workspace + // `root_url` can not, use `workspace` as a fallback + let root_path = root.clone().unwrap_or_else(|| workspace.clone()); + let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); + + let mut globset = globset::GlobSetBuilder::new(); + let required_root_patterns = &ls_config.required_root_patterns; + if !required_root_patterns.is_empty() { + for required_root_pattern in required_root_patterns { + match globset::Glob::new(required_root_pattern) { + Ok(glob) => { + globset.add(glob); + } + Err(err) => { + log::warn!( + "Failed to build glob '{}' for language server '{}'", + required_root_pattern, + name + ); + log::warn!("{}", err); + } + }; + } + match globset.build() { + Ok(glob) => { + if !root_path + .read_dir()? + .filter_map(|e| e.ok()) + .map(|e| e.file_name()) + .any(|filename| glob.is_match(filename)) + { + return Ok(None); + } + } + Err(err) => { + log::warn!("Failed to build globset for language server {name}"); + log::warn!("{}", err); + } + }; + } + let (client, incoming, initialize_notify) = Client::start( &ls_config.command, &ls_config.args, ls_config.config.clone(), ls_config.environment.clone(), - &config.roots, - config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), + root_path, + root_uri, id, name, ls_config.timeout, - doc_path, )?; let client = Arc::new(client); @@ -923,7 +979,7 @@ fn start_client( initialize_notify.notify_one(); }); - Ok(NewClient(client, incoming)) + Ok(Some(NewClient(client, incoming))) } /// Find an LSP workspace of a file using the following mechanism: From 3d90deac28d78d2ced89db368cdd8c5a7eb76b27 Mon Sep 17 00:00:00 2001 From: ontley Date: Thu, 2 Nov 2023 11:43:25 +0100 Subject: [PATCH 2/8] Replaced filter_map with flatten --- helix-lsp/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 20205c51133c..f8aa7f36e26f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -924,9 +924,9 @@ fn start_client( Ok(glob) => { if !root_path .read_dir()? - .filter_map(|e| e.ok()) - .map(|e| e.file_name()) - .any(|filename| glob.is_match(filename)) + .flatten() + .map(|entry| entry.file_name()) + .any(|entry| glob.is_match(entry)) { return Ok(None); } From 0485c5bb060c3a3894256c9ef00a61e416f3501d Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 00:00:45 +0100 Subject: [PATCH 3/8] updated book to include required-root-patterns option --- book/src/languages.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index 778489f8d236..f820a2315fa6 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -118,13 +118,14 @@ languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT These are the available options for a language server. -| Key | Description | -| ---- | ----------- | -| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` | -| `args` | A list of arguments to pass to the language server binary | -| `config` | LSP initialization options | -| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | -| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | +| Key | Description | +| ---- | ----------- | +| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` | +| `args` | A list of arguments to pass to the language server binary | +| `config` | LSP initialization options | +| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | +| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | +| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. | A `format` sub-table within `config` can be used to pass extra formatting options to [Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting). From 51d40c169d70e9976ec86dace755bf4f94260d42 Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 00:41:34 +0100 Subject: [PATCH 4/8] fixed wrong function name for path --- helix-lsp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 1e191d7c400a..7caaa89cae4e 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -894,7 +894,7 @@ fn start_client( enable_snippets: bool, ) -> Result> { let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); - let workspace = path::get_normalized_path(&workspace); + let workspace = path::normalize(&workspace); let root = find_lsp_workspace( doc_path .and_then(|x| x.parent().and_then(|x| x.to_str())) From 9909082f089f80142ee55234b6088a20a09809f0 Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 16:27:29 +0100 Subject: [PATCH 5/8] Added globset to helix-core. Moved globset building to config parsing. --- Cargo.lock | 1 + helix-core/Cargo.toml | 1 + helix-core/src/syntax.rs | 26 ++++++++++++++++++++++--- helix-lsp/src/lib.rs | 41 ++++++++-------------------------------- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73e548ae5a5f..a7ef8eb0504b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1240,6 +1240,7 @@ dependencies = [ "dunce", "encoding_rs", "etcetera", + "globset", "hashbrown 0.14.3", "helix-loader", "helix-stdx", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 8c63af8ef266..fb68ccc083db 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -52,6 +52,7 @@ textwrap = "0.16.0" nucleo.workspace = true parking_lot = "0.12" +globset = "0.4.14" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 99ff74d36041..9b82fa73ec4f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -10,6 +10,7 @@ use crate::{ use ahash::RandomState; use arc_swap::{ArcSwap, Guard}; use bitflags::bitflags; +use globset::GlobSet; use hashbrown::raw::RawTable; use slotmap::{DefaultKey as LayerId, HopSlotMap}; @@ -358,6 +359,22 @@ where serializer.end() } +fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let patterns = Vec::::deserialize(deserializer)?; + if patterns.is_empty() { + return Ok(None); + } + let mut builder = globset::GlobSetBuilder::new(); + for pattern in patterns { + let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?; + builder.add(glob); + } + builder.build().map(Some).map_err(serde::de::Error::custom) +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -371,9 +388,12 @@ pub struct LanguageServerConfiguration { pub config: Option, #[serde(default = "default_timeout")] pub timeout: u64, - #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub required_root_patterns: Vec, + #[serde( + default, + skip_serializing, + deserialize_with = "deserialize_required_root_patterns" + )] + pub required_root_patterns: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 7caaa89cae4e..27678b2c1201 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -910,40 +910,15 @@ fn start_client( let root_path = root.clone().unwrap_or_else(|| workspace.clone()); let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); - let mut globset = globset::GlobSetBuilder::new(); - let required_root_patterns = &ls_config.required_root_patterns; - if !required_root_patterns.is_empty() { - for required_root_pattern in required_root_patterns { - match globset::Glob::new(required_root_pattern) { - Ok(glob) => { - globset.add(glob); - } - Err(err) => { - log::warn!( - "Failed to build glob '{}' for language server '{}'", - required_root_pattern, - name - ); - log::warn!("{}", err); - } - }; + if let Some(globset) = &ls_config.required_root_patterns { + if !root_path + .read_dir()? + .flatten() + .map(|entry| entry.file_name()) + .any(|entry| globset.is_match(entry)) + { + return Ok(None); } - match globset.build() { - Ok(glob) => { - if !root_path - .read_dir()? - .flatten() - .map(|entry| entry.file_name()) - .any(|entry| glob.is_match(entry)) - { - return Ok(None); - } - } - Err(err) => { - log::warn!("Failed to build globset for language server {name}"); - log::warn!("{}", err); - } - }; } let (client, incoming, initialize_notify) = Client::start( From 334af9ce5d92da48a2c36352c1558e857816cfd7 Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 16:28:33 +0100 Subject: [PATCH 6/8] Normalize implements AsRef --- helix-lsp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 27678b2c1201..05764418f0a2 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -894,7 +894,7 @@ fn start_client( enable_snippets: bool, ) -> Result> { let (workspace, workspace_is_cwd) = helix_loader::find_workspace(); - let workspace = path::normalize(&workspace); + let workspace = path::normalize(workspace); let root = find_lsp_workspace( doc_path .and_then(|x| x.parent().and_then(|x| x.to_str())) From ca8ce123e8d77d2ae8ed84d5273a9b554101b0db Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 16:29:53 +0100 Subject: [PATCH 7/8] cargo fmt --- helix-core/src/transaction.rs | 8 ++++++-- helix-term/src/handlers/signature_help.rs | 13 ++++++------- helix-view/src/register.rs | 4 +++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index f5a49cc1100d..eaf22008e2a8 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -378,7 +378,9 @@ impl ChangeSet { macro_rules! map { ($map: expr, $i: expr) => { loop { - let Some((pos, assoc)) = positions.peek_mut() else { return; }; + let Some((pos, assoc)) = positions.peek_mut() else { + return; + }; if **pos < old_pos { // Positions are not sorted, revert to the last Operation that // contains this position and continue iterating from there. @@ -405,7 +407,9 @@ impl ChangeSet { debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); continue 'outer; } - let Some(new_pos) = $map(**pos, *assoc) else { break; }; + let Some(new_pos) = $map(**pos, *assoc) else { + break; + }; **pos = new_pos; positions.next(); } diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 3c746548ac8c..1bca1e14c4d4 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -118,8 +118,7 @@ pub fn request_signature_help( // Do not show the message if signature help was invoked // automatically on backspace, trigger characters, etc. if invoked == SignatureHelpInvoked::Manual { - editor - .set_error("No configured language server supports signature-help"); + editor.set_error("No configured language server supports signature-help"); } return; }; @@ -264,11 +263,11 @@ fn signature_help_post_insert_char_hook( let (view, doc) = current!(cx.editor); // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow let Some(language_server) = doc - .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .next() - else { - return Ok(()); - }; + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .next() + else { + return Ok(()); + }; let capabilities = language_server.capabilities(); diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 5592c6afd781..1e6680150184 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -233,7 +233,9 @@ fn read_from_clipboard<'a>( // If we're pasting the same values that we just yanked, re-use // the saved values. This allows pasting multiple selections // even when yanked to a clipboard. - let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) }; + let Some(values) = saved_values else { + return RegisterValues::new(iter::once(contents.into())); + }; if contents_are_saved(values, &contents) { RegisterValues::new(values.iter().map(Cow::from).rev()) From 7b9bb7aeb7fe801e35a242e68a8bc0b7ba688acb Mon Sep 17 00:00:00 2001 From: ontley Date: Fri, 9 Feb 2024 16:54:14 +0100 Subject: [PATCH 8/8] Revert "cargo fmt" This reverts commit ca8ce123e8d77d2ae8ed84d5273a9b554101b0db. --- helix-core/src/transaction.rs | 8 ++------ helix-term/src/handlers/signature_help.rs | 13 +++++++------ helix-view/src/register.rs | 4 +--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index eaf22008e2a8..f5a49cc1100d 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -378,9 +378,7 @@ impl ChangeSet { macro_rules! map { ($map: expr, $i: expr) => { loop { - let Some((pos, assoc)) = positions.peek_mut() else { - return; - }; + let Some((pos, assoc)) = positions.peek_mut() else { return; }; if **pos < old_pos { // Positions are not sorted, revert to the last Operation that // contains this position and continue iterating from there. @@ -407,9 +405,7 @@ impl ChangeSet { debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); continue 'outer; } - let Some(new_pos) = $map(**pos, *assoc) else { - break; - }; + let Some(new_pos) = $map(**pos, *assoc) else { break; }; **pos = new_pos; positions.next(); } diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 1bca1e14c4d4..3c746548ac8c 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -118,7 +118,8 @@ pub fn request_signature_help( // Do not show the message if signature help was invoked // automatically on backspace, trigger characters, etc. if invoked == SignatureHelpInvoked::Manual { - editor.set_error("No configured language server supports signature-help"); + editor + .set_error("No configured language server supports signature-help"); } return; }; @@ -263,11 +264,11 @@ fn signature_help_post_insert_char_hook( let (view, doc) = current!(cx.editor); // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow let Some(language_server) = doc - .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .next() - else { - return Ok(()); - }; + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .next() + else { + return Ok(()); + }; let capabilities = language_server.capabilities(); diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 1e6680150184..5592c6afd781 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -233,9 +233,7 @@ fn read_from_clipboard<'a>( // If we're pasting the same values that we just yanked, re-use // the saved values. This allows pasting multiple selections // even when yanked to a clipboard. - let Some(values) = saved_values else { - return RegisterValues::new(iter::once(contents.into())); - }; + let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) }; if contents_are_saved(values, &contents) { RegisterValues::new(values.iter().map(Cow::from).rev())