diff --git a/book/src/languages.md b/book/src/languages.md index ea18e9c39..3dbbed222 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -142,7 +142,7 @@ These are the available options for a language server. | `config` | Language server 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. | +| `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. Patterns can be negated with `!` prefix (`\\!` for escape). | 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). diff --git a/flake.nix b/flake.nix index c3b4ce5d5..bd8b15a74 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,7 @@ lld cargo-flamegraph rust-bin.nightly.latest.rust-analyzer + rust-bin.nightly.latest.clippy ] ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) ++ (lib.optional stdenv.isLinux lldb) diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 432611bb0..07e2af40f 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -374,20 +374,41 @@ where serializer.end() } -fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result, D::Error> +/// Deserialize required-root-patterns with globset. +/// +/// This function returns a tuple of positive globset and negative globset. +fn deserialize_required_root_patterns<'de, D>( + deserializer: D, +) -> Result<(GlobSet, GlobSet), 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(); + + let mut positive_builder = globset::GlobSetBuilder::new(); + let mut negative_builder = globset::GlobSetBuilder::new(); for pattern in patterns { - let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?; - builder.add(glob); + // negative pattern + if let Some(stripped) = pattern.strip_prefix("!") { + let glob = globset::Glob::new(stripped).map_err(serde::de::Error::custom)?; + negative_builder.add(glob); + } + // escaped pattern + else if let Some(_stripped) = pattern.strip_prefix("\\!") { + let glob = globset::Glob::new(&pattern[1..]).map_err(serde::de::Error::custom)?; + positive_builder.add(glob); + } + // rest is positive + else { + let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?; + positive_builder.add(glob); + } } - builder.build().map(Some).map_err(serde::de::Error::custom) + + let positive_globset = positive_builder.build().map_err(serde::de::Error::custom)?; + let negative_globset = negative_builder.build().map_err(serde::de::Error::custom)?; + + Ok((positive_globset, negative_globset)) } #[derive(Debug, Serialize, Deserialize)] @@ -403,12 +424,13 @@ pub struct LanguageServerConfiguration { pub config: Option, #[serde(default = "default_timeout")] pub timeout: u64, + /// Positive and negative patterns. #[serde( default, skip_serializing, deserialize_with = "deserialize_required_root_patterns" )] - pub required_root_patterns: Option, + pub required_root_patterns: (GlobSet, GlobSet), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 567e8a702..32c68bf46 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -888,16 +888,36 @@ 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()); - 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)) + // match negative and positive root patterns + let (positive_globset, negative_globset) = &ls_config.required_root_patterns; + let file_names: Vec<_> = root_path + .read_dir()? + .flatten() + .map(|entry| entry.file_name()) + .collect(); + // negative + if !negative_globset.is_empty() { + if let Some(occurence) = file_names + .iter() + .find(|name| negative_globset.is_match(name)) { + log::debug!( + "negative root pattern {} met for {}", + occurence.to_string_lossy(), + name + ); return Err(StartupError::NoRequiredRootFound); } } + // positive + if !positive_globset.is_empty() + && !file_names + .iter() + .any(|name| positive_globset.is_match(name)) + { + log::debug!("none of positive root patterns found for {}", name); + return Err(StartupError::NoRequiredRootFound); + } let (client, incoming, initialize_notify) = Client::start( &ls_config.command,