From dfcd84272619832e9f8d0b183b69d5cb177871e1 Mon Sep 17 00:00:00 2001 From: ontley Date: Thu, 2 Nov 2023 11:09:47 +0100 Subject: [PATCH] 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 881b45098..aeb1e3ce6 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 341702c37..0d8465bad 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 b6a990659..20205c511 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: