From 247d3646f4cfb5c1f98587850692055fa06a7d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Mon, 26 May 2025 12:08:14 +0800 Subject: [PATCH 1/6] feat(lsp): allow negative root pattern --- helix-core/src/syntax/config.rs | 40 +++++++++++++++++++++++++-------- helix-lsp/src/lib.rs | 31 +++++++++++++++++-------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 432611bb0..40132dd22 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 pattern.starts_with("!") { + let glob = globset::Glob::new(&pattern[1..]).map_err(serde::de::Error::custom)?; + negative_builder.add(glob); + } + // escaped pattern + else if pattern.starts_with("\\!") { + let glob = globset::Glob::new(&pattern[2..]).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 = positive_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..a2d99eadf 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -888,15 +888,28 @@ 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)) - { - return Err(StartupError::NoRequiredRootFound); - } + // 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() + && file_names + .iter() + .any(|name| negative_globset.is_match(name)) + { + return Err(StartupError::NoRequiredRootFound); + } + // positive + if !positive_globset.is_empty() + && !file_names + .iter() + .any(|name| positive_globset.is_match(name)) + { + return Err(StartupError::NoRequiredRootFound); } let (client, incoming, initialize_notify) = Client::start( From fd2746612f72c83d52306b98a2ad57a6cb9fc2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Mon, 26 May 2025 12:33:58 +0800 Subject: [PATCH 2/6] chore(nix): add clippy dependency --- flake.nix | 1 + 1 file changed, 1 insertion(+) 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) From 4f53d46be4aa030c7255e993d2cea14fb255f01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Mon, 26 May 2025 12:34:17 +0800 Subject: [PATCH 3/6] style: follow clippy strip style --- helix-core/src/syntax/config.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 40132dd22..3464d4823 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -389,13 +389,13 @@ where let mut negative_builder = globset::GlobSetBuilder::new(); for pattern in patterns { // negative pattern - if pattern.starts_with("!") { - let glob = globset::Glob::new(&pattern[1..]).map_err(serde::de::Error::custom)?; + 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 pattern.starts_with("\\!") { - let glob = globset::Glob::new(&pattern[2..]).map_err(serde::de::Error::custom)?; + else if let Some(stripped) = pattern.strip_prefix("\\!") { + let glob = globset::Glob::new(stripped).map_err(serde::de::Error::custom)?; positive_builder.add(glob); } // rest is positive From 32eef49276023ce1f80b02ae7194af47fd5f5d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Mon, 26 May 2025 13:01:22 +0800 Subject: [PATCH 4/6] docs: negative `required-root-patterns` --- book/src/languages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). From 4aa08d5b314a1d9922a27c8052cdac0d91559961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Tue, 27 May 2025 17:03:28 +0800 Subject: [PATCH 5/6] fix: escaped negative pattern --- helix-core/src/syntax/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 3464d4823..8356aae75 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -394,8 +394,8 @@ where negative_builder.add(glob); } // escaped pattern - else if let Some(stripped) = pattern.strip_prefix("\\!") { - let glob = globset::Glob::new(stripped).map_err(serde::de::Error::custom)?; + 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 From cdc095699020cf905268ce238e7e594aced2a0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=99=9A=E7=99=BD?= <18016038327@189.cn> Date: Tue, 27 May 2025 17:26:06 +0800 Subject: [PATCH 6/6] fix: typo negative root pattern builder --- helix-core/src/syntax/config.rs | 2 +- helix-lsp/src/lib.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 8356aae75..07e2af40f 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -406,7 +406,7 @@ where } let positive_globset = positive_builder.build().map_err(serde::de::Error::custom)?; - let negative_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)) } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index a2d99eadf..32c68bf46 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -896,12 +896,18 @@ fn start_client( .map(|entry| entry.file_name()) .collect(); // negative - if !negative_globset.is_empty() - && file_names + if !negative_globset.is_empty() { + if let Some(occurence) = file_names .iter() - .any(|name| negative_globset.is_match(name)) - { - return Err(StartupError::NoRequiredRootFound); + .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() @@ -909,6 +915,7 @@ fn start_client( .iter() .any(|name| positive_globset.is_match(name)) { + log::debug!("none of positive root patterns found for {}", name); return Err(StartupError::NoRequiredRootFound); }