From 015913afea8e5ed4bd92cd800831f67054d7b6a9 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:00:31 -0500 Subject: [PATCH] search: Support brace syntax in project search include/exclude patterns (#47860) Closes #47527 ## Summary The include/exclude filters in Project Search now support standard glob brace syntax like `{a,b}`. **Before:** `crates/{search,project}/**/file.rs` would error with "unclosed alternate group" **After:** Pattern correctly matches files in both `crates/search/` and `crates/project/` ## Implementation Added `split_glob_patterns()` function that splits by comma only when not inside braces, preserving the `{a,b}` syntax that `globset` natively supports. Release Notes: - Added support for `{a,b}` glob syntax in project search include/exclude filters Here is the screenshot of before and after. Before: {0233D673-E876-4CFC-81BC-E0DE778CA382} After: {321F7C80-13A0-4478-BCE9-530F1824A9E2} --- crates/search/src/project_search.rs | 53 +++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3c2bfea77aa1f0f409ce78a3a69e612aa30715e2..7c85077c488371e611fdadf0baf7ee94f49fe511 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -71,6 +71,32 @@ actions!( ] ); +fn split_glob_patterns(text: &str) -> Vec<&str> { + let mut patterns = Vec::new(); + let mut pattern_start = 0; + let mut brace_depth: usize = 0; + let mut escaped = false; + + for (index, character) in text.char_indices() { + if escaped { + escaped = false; + continue; + } + match character { + '\\' => escaped = true, + '{' => brace_depth += 1, + '}' => brace_depth = brace_depth.saturating_sub(1), + ',' if brace_depth == 0 => { + patterns.push(&text[pattern_start..index]); + pattern_start = index + 1; + } + _ => {} + } + } + patterns.push(&text[pattern_start..]); + patterns +} + #[derive(Default)] struct ActiveSettings(HashMap, ProjectSearchSettings>); @@ -1381,8 +1407,8 @@ impl ProjectSearchView { fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result { let path_style = self.entity.read(cx).project.read(cx).path_style(cx); - let queries = text - .split(',') + let queries = split_glob_patterns(&text) + .into_iter() .map(str::trim) .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) .map(str::to_owned) @@ -2517,6 +2543,29 @@ pub mod tests { use util_macros::perf; use workspace::{DeploySearch, MultiWorkspace}; + #[test] + fn test_split_glob_patterns() { + assert_eq!(split_glob_patterns("a,b,c"), vec!["a", "b", "c"]); + assert_eq!(split_glob_patterns("a, b, c"), vec!["a", " b", " c"]); + assert_eq!( + split_glob_patterns("src/{a,b}/**/*.rs"), + vec!["src/{a,b}/**/*.rs"] + ); + assert_eq!( + split_glob_patterns("src/{a,b}/*.rs, tests/**/*.rs"), + vec!["src/{a,b}/*.rs", " tests/**/*.rs"] + ); + assert_eq!(split_glob_patterns("{a,b},{c,d}"), vec!["{a,b}", "{c,d}"]); + assert_eq!(split_glob_patterns("{{a,b},{c,d}}"), vec!["{{a,b},{c,d}}"]); + assert_eq!(split_glob_patterns(""), vec![""]); + assert_eq!(split_glob_patterns("a"), vec!["a"]); + // Escaped characters should not be treated as special + assert_eq!(split_glob_patterns(r"a\,b,c"), vec![r"a\,b", "c"]); + assert_eq!(split_glob_patterns(r"\{a,b\}"), vec![r"\{a", r"b\}"]); + assert_eq!(split_glob_patterns(r"a\\,b"), vec![r"a\\", "b"]); + assert_eq!(split_glob_patterns(r"a\\\,b"), vec![r"a\\\,b"]); + } + #[perf] #[gpui::test] async fn test_project_search(cx: &mut TestAppContext) {