From 0a9f40787216f18ee2f4dc79211877d1af496284 Mon Sep 17 00:00:00 2001 From: Dino Date: Thu, 28 Aug 2025 02:27:02 +0100 Subject: [PATCH] search: Add support for case-sensitivity pattern items (#34762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/project/src/search.rs | 132 +++++++++++++++++++++++++++- crates/search/src/buffer_search.rs | 21 +++-- crates/search/src/project_search.rs | 2 +- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index ee216a99763282d8d30a0b17c3df3b8da3213db7..f2c6091e0cb00b8da1a752e3d25afe3389e8c818 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -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 { 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 { 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." + ); + } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b2096d48ef5ba4e3b74eb0955bb11ef32cb5498a..92992dced6066d7242ad15f666de5a3b98f76ed0 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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(); + + let cx = VisualTestContext::from_window(*window, cx).into_mut(); - (editor, search_bar, cx) + (editor.unwrap(), search_bar, cx) } #[gpui::test] diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588afa2acf75aba1091407bd6f8b83d51ce..1ee959f111bd5741a655551aa71030fd9d7c15c9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1139,7 +1139,7 @@ impl ProjectSearchView { fn build_search_query(&mut self, cx: &mut Context) -> Option { // 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 {