search: Add support for case-sensitivity pattern items (#34762)

This Pull Request introduces support for pattern items in the buffer
search. It does so by splitting the `query` methods into two new
methods:

- `BufferSearchBar.raw_query` – returns the text from the search query
editor
- `BufferSearchBar.query` - returns the search query with pattern items
removed

Whenever the search query is updated, processing of the
`EditorEvent::Edited` event ends up calling the
`BufferSearchBar.apply_pattern_items` method, which parses the pattern
items from the raw query, and updates the buffer search bar's search
options accordingly. This `apply_pattern_items` function avoids updating
the `BufferSearchBar.default_options` field in order to be able to reset
the search options when a pattern items is removed. Lastly, new pattern
items can easily be added by updating the `PATTERN_ITEMS` array.

### Screen Capture


https://github.com/user-attachments/assets/ebd83c38-e480-4c24-9b8c-6edde69cf392

---

Closes #32390

Release Notes:

- Added support for the `\c` and `\C` query pattern items to control
case-sensitivity in buffer search

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Dino
2025-08-27 19:27:02 -06:00
committed by GitHub
co-authored by Conrad Irwin
parent 4e1a901059
commit 0a9f407872
3 changed files with 146 additions and 9 deletions
+131 -1
View File
@@ -143,7 +143,7 @@ impl SearchQuery {
pub fn regex(
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
mut case_sensitive: bool,
include_ignored: bool,
one_match_per_line: bool,
files_to_include: PathMatcher,
@@ -153,6 +153,14 @@ impl SearchQuery {
) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
if let Some((case_sensitive_from_pattern, new_query)) =
Self::case_sensitive_from_pattern(&query)
{
case_sensitive = case_sensitive_from_pattern;
query = new_query
}
if whole_word {
let mut word_query = String::new();
if let Some(first) = query.get(0..1)
@@ -192,6 +200,45 @@ impl SearchQuery {
})
}
/// Extracts case sensitivity settings from pattern items in the provided
/// query and returns the same query, with the pattern items removed.
///
/// The following pattern modifiers are supported:
///
/// - `\c` (case_sensitive: false)
/// - `\C` (case_sensitive: true)
///
/// If no pattern item were found, `None` will be returned.
fn case_sensitive_from_pattern(query: &str) -> Option<(bool, String)> {
if !(query.contains("\\c") || query.contains("\\C")) {
return None;
}
let mut was_escaped = false;
let mut new_query = String::new();
let mut is_case_sensitive = None;
for c in query.chars() {
if was_escaped {
if c == 'c' {
is_case_sensitive = Some(false);
} else if c == 'C' {
is_case_sensitive = Some(true);
} else {
new_query.push('\\');
new_query.push(c);
}
was_escaped = false
} else if c == '\\' {
was_escaped = true
} else {
new_query.push(c);
}
}
is_case_sensitive.map(|c| (c, new_query))
}
pub fn from_proto(message: proto::SearchQuery) -> Result<Self> {
let files_to_include = if message.files_to_include.is_empty() {
message
@@ -596,4 +643,87 @@ mod tests {
}
}
}
#[test]
fn test_case_sensitive_pattern_items() {
let case_sensitive = false;
let search_query = SearchQuery::regex(
"test\\C",
false,
case_sensitive,
false,
false,
Default::default(),
Default::default(),
false,
None,
)
.expect("Should be able to create a regex SearchQuery");
assert_eq!(
search_query.case_sensitive(),
true,
"Case sensitivity should be enabled when \\C pattern item is present in the query."
);
let case_sensitive = true;
let search_query = SearchQuery::regex(
"test\\c",
true,
case_sensitive,
false,
false,
Default::default(),
Default::default(),
false,
None,
)
.expect("Should be able to create a regex SearchQuery");
assert_eq!(
search_query.case_sensitive(),
false,
"Case sensitivity should be disabled when \\c pattern item is present, even if initially set to true."
);
let case_sensitive = false;
let search_query = SearchQuery::regex(
"test\\c\\C",
false,
case_sensitive,
false,
false,
Default::default(),
Default::default(),
false,
None,
)
.expect("Should be able to create a regex SearchQuery");
assert_eq!(
search_query.case_sensitive(),
true,
"Case sensitivity should be enabled when \\C is the last pattern item, even after a \\c."
);
let case_sensitive = false;
let search_query = SearchQuery::regex(
"tests\\\\C",
false,
case_sensitive,
false,
false,
Default::default(),
Default::default(),
false,
None,
)
.expect("Should be able to create a regex SearchQuery");
assert_eq!(
search_query.case_sensitive(),
false,
"Case sensitivity should not be enabled when \\C pattern item is preceded by a backslash."
);
}
}
+14 -7
View File
@@ -1516,18 +1516,25 @@ mod tests {
cx,
)
});
let cx = cx.add_empty_window();
let editor =
cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
let search_bar = cx.new_window_entity(|window, cx| {
let mut editor = None;
let window = cx.add_window(|window, cx| {
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
"keymaps/default-macos.json",
cx,
)
.unwrap();
cx.bind_keys(default_key_bindings);
editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
let mut search_bar = BufferSearchBar::new(None, window, cx);
search_bar.set_active_pane_item(Some(&editor), window, cx);
search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
search_bar.show(window, cx);
search_bar
});
let search_bar = window.root(cx).unwrap();
(editor, search_bar, cx)
let cx = VisualTestContext::from_window(*window, cx).into_mut();
(editor.unwrap(), search_bar, cx)
}
#[gpui::test]
+1 -1
View File
@@ -1139,7 +1139,7 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut Context<Self>) -> Option<SearchQuery> {
// Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
let text = self.query_editor.read(cx).text(cx);
let text = self.search_query_text(cx);
let open_buffers = if self.included_opened_only {
Some(self.open_buffers(cx))
} else {