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:
After:
---
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) {