From 83f00ce3c46cfe68a4c80473307203c8e5b1ab10 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Wed, 16 Jul 2025 12:33:47 +0100 Subject: [PATCH 01/15] feat(buffer_search): allow pattern items in search query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the way the search query in the buffer search bar is handled in order to support pattern items. Right now only two pattern items are supported: - `\c` – When \c is provided in the search query, it disables the case sensitivity search option - `\C` – When \C is provided in the search query, it enables the case sensitivity search option This feature takes precedence over the `BufferSearchBar.search_options`, but it ensures that, if any search option was previously enabled/disabled and it was disabled/enabled by a pattern item, it'll return to its initial state when the pattern item is deleted. --- crates/search/src/buffer_search.rs | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a38dc8c35b..ad412e556a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -45,6 +45,10 @@ pub use registrar::DivRegistrar; use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; +static PATTERN_ITEMS: [(&str, &SearchOptions, bool); 2] = [ + ("\\c", &SearchOptions::CASE_SENSITIVE, false), + ("\\C", &SearchOptions::CASE_SENSITIVE, true), +]; /// Opens the buffer search interface with the specified configuration. #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] @@ -810,10 +814,22 @@ impl BufferSearchBar { }); } - pub fn query(&self, cx: &App) -> String { + /// Returns the raw query string without any of the pattern items removed. + pub fn raw_query(&self, cx: &App) -> String { self.query_editor.read(cx).text(cx) } + /// Returns the sanitized query string with pattern items removed. + pub fn query(&self, cx: &App) -> String { + // TODO: Probably replace this with a regex (\\[cC])? I assume + // performance is going to be not so great with a big list of pattern + // items. + PATTERN_ITEMS.iter().fold( + self.query_editor.read(cx).text(cx), + |string, (pattern, _, _)| string.replace(pattern, ""), + ) + } + pub fn replacement(&self, cx: &mut App) -> String { self.replacement_editor.read(cx).text(cx) } @@ -909,6 +925,17 @@ impl BufferSearchBar { } } + pub fn disable_search_option( + &mut self, + search_option: SearchOptions, + window: &mut Window, + cx: &mut Context, + ) { + if self.search_options.contains(search_option) { + self.toggle_search_option(search_option, window, cx) + } + } + pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context) { self.search_options = search_options; self.adjust_query_regex_language(cx); @@ -1032,6 +1059,7 @@ impl BufferSearchBar { editor::EditorEvent::Blurred => self.query_editor_focused = false, editor::EditorEvent::Edited { .. } => { self.smartcase(window, cx); + self.apply_pattern_items(cx); self.clear_matches(window, cx); let search = self.update_matches(false, window, cx); @@ -1441,6 +1469,28 @@ impl BufferSearchBar { } } + // Determines which pattern items are present in the search query and + // updates the search options accordingly. + fn apply_pattern_items(&mut self, cx: &mut Context) { + // Start from the default search options to ensure that any search + // option that is not to be updated does not changed. + // For example, if `\c` was present in the query and the case + // sensitivity was initially enabled, removing `\c` from the query + // should re-enable case sensitivity. + let mut search_options = self.default_options.clone(); + let query = self.raw_query(cx); + + for (pattern, search_option, value) in PATTERN_ITEMS { + match (query.contains(pattern), value) { + (true, true) => search_options.set(*search_option, value), + (true, false) => search_options.set(*search_option, value), + (false, _) => (), + } + } + + self.set_search_options(search_options, cx); + } + fn adjust_query_regex_language(&self, cx: &mut App) { let enable = self.search_options.contains(SearchOptions::REGEX); let query_buffer = self From 3d1113a3f367754ae9c1b708282e5ca825c97968 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Wed, 16 Jul 2025 16:32:57 +0100 Subject: [PATCH 02/15] refactor: use regex to remove pattern items from search query This might be a bit of an overkill for the time being, but I expect that when the array of pattern items grows, it might be faster to replace the pattern items with a regex instead of iterating over the array. --- Cargo.lock | 1 + crates/search/Cargo.toml | 1 + crates/search/src/buffer_search.rs | 27 ++++++++++++++++++++------- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..5017c42b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14568,6 +14568,7 @@ dependencies = [ "client", "collections", "editor", + "fancy-regex 0.14.0", "futures 0.3.31", "gpui", "language", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 613f229d4d..20a69694dd 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -42,6 +42,7 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true +fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ad412e556a..591981818f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -13,6 +13,7 @@ use editor::{ DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; +use fancy_regex::Regex; use futures::channel::oneshot; use gpui::{ Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, @@ -45,6 +46,11 @@ pub use registrar::DivRegistrar; use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; + +/// Array of supported pattern items and their corresponding search options and +/// value. +/// When any of the patterns is present in the search query, the corresponding +/// search option, and value, is applied. static PATTERN_ITEMS: [(&str, &SearchOptions, bool); 2] = [ ("\\c", &SearchOptions::CASE_SENSITIVE, false), ("\\C", &SearchOptions::CASE_SENSITIVE, true), @@ -126,6 +132,7 @@ pub struct BufferSearchBar { editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, regex_language: Option>, + pattern_items_regex: Regex, } impl BufferSearchBar { @@ -647,6 +654,15 @@ impl BufferSearchBar { .detach_and_log_err(cx); } + let pattern_items_regex = Regex::new( + &PATTERN_ITEMS + .iter() + .map(|(pattern, _, _)| fancy_regex::escape(pattern)) + .collect::>() + .join("|"), + ) + .unwrap(); + Self { query_editor, query_editor_focused: false, @@ -674,6 +690,7 @@ impl BufferSearchBar { editor_scroll_handle: ScrollHandle::new(), editor_needed_width: px(0.), regex_language: None, + pattern_items_regex, } } @@ -821,13 +838,9 @@ impl BufferSearchBar { /// Returns the sanitized query string with pattern items removed. pub fn query(&self, cx: &App) -> String { - // TODO: Probably replace this with a regex (\\[cC])? I assume - // performance is going to be not so great with a big list of pattern - // items. - PATTERN_ITEMS.iter().fold( - self.query_editor.read(cx).text(cx), - |string, (pattern, _, _)| string.replace(pattern, ""), - ) + self.pattern_items_regex + .replace_all(&self.raw_query(cx), "") + .into_owned() } pub fn replacement(&self, cx: &mut App) -> String { From e17af0400aeb42c615b455a27a84e652fe7c524a Mon Sep 17 00:00:00 2001 From: dinocosta Date: Wed, 16 Jul 2025 16:35:05 +0100 Subject: [PATCH 03/15] test(buffer_search): add test for pattern items Add a very simple test that verifies the behaviour of `apply_pattern_items` when the search query is updated. In practice it would be best to be able to actually simulate the whole flow where `EditorEvent::Edited` is triggered but I haven't managed to figure out how to do that just yet. --- crates/search/src/buffer_search.rs | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 591981818f..e59c5944d3 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2883,6 +2883,105 @@ mod tests { }); } + #[gpui::test] + async fn test_pattern_items(cx: &mut TestAppContext) { + let (_editor, search_bar, cx) = init_test(cx); + + update_search_settings( + SearchSettings { + button: true, + whole_word: false, + case_sensitive: false, + include_ignored: false, + regex: false, + }, + cx, + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!( + search_bar.search_options, + SearchOptions::NONE, + "Should have no search options enabled by default" + ); + + // Update the search query's text to `test\C` to check if the + // search option is correctly applied. + let query = "test\\C"; + search_bar.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + + search_bar.apply_pattern_items(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled when \\C pattern item is present" + ); + + // Remove `\\C` from the query to check if the search option is + // correctly reverted to its default state. + let query = "test"; + search_bar.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + + search_bar.apply_pattern_items(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::NONE, + "Should have case sensitivity disable when \\C pattern item is removed" + ); + + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled by default" + ); + + // Update the search query's text to `test\c` to check if the + // case sensitivity search option is correctly disabled. + let query = "test\\c"; + search_bar.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + + search_bar.apply_pattern_items(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::NONE, + "Should have no case sensitivity enabled when \\c pattern item is present" + ); + + // Remove `\\c` from the query to check if the search option is + // correctly reverted to its default state. + let query = "test"; + search_bar.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + + search_bar.apply_pattern_items(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled when \\c pattern item is removed" + ); + }); + } + fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { From f1ba9b3d3d436015e5d803fac15740e74d9f619d Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 22 Jul 2025 15:23:45 +0100 Subject: [PATCH 04/15] fix: fix clippy issues --- crates/search/src/buffer_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e59c5944d3..d91b9824d1 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1490,7 +1490,7 @@ impl BufferSearchBar { // For example, if `\c` was present in the query and the case // sensitivity was initially enabled, removing `\c` from the query // should re-enable case sensitivity. - let mut search_options = self.default_options.clone(); + let mut search_options = self.default_options; let query = self.raw_query(cx); for (pattern, search_option, value) in PATTERN_ITEMS { From 0ab17c95fb97121a1598bcce5536f43f0c13b1bb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 23 Jul 2025 01:03:38 -0600 Subject: [PATCH 05/15] Set up test properly --- crates/search/src/buffer_search.rs | 85 +++++++++++++----------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d91b9824d1..e1cbfd3051 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1577,18 +1577,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).as_mut(); + + (editor.unwrap(), search_bar, cx) } #[gpui::test] @@ -2899,40 +2906,31 @@ mod tests { ); search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.show(window, cx); assert_eq!( search_bar.search_options, SearchOptions::NONE, "Should have no search options enabled by default" ); - // Update the search query's text to `test\C` to check if the - // search option is correctly applied. - let query = "test\\C"; - search_bar.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); - }); + cx.focus_view(&search_bar.query_editor, window); + }); - search_bar.apply_pattern_items(cx); + cx.simulate_input("test\\C"); + + search_bar.update_in(cx, |search_bar, _, _| { assert_eq!( search_bar.search_options, SearchOptions::CASE_SENSITIVE, "Should have case sensitivity enabled when \\C pattern item is present" ); + }); - // Remove `\\C` from the query to check if the search option is - // correctly reverted to its default state. - let query = "test"; - search_bar.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); - }); - - search_bar.apply_pattern_items(cx); + // Remove `\\C` from the query to check if the search option is + // correctly reverted to its default state. + cx.simulate_keystrokes("backspace backspace"); + search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!(search_bar.raw_query(cx), "test"); assert_eq!( search_bar.search_options, SearchOptions::NONE, @@ -2945,35 +2943,24 @@ mod tests { SearchOptions::CASE_SENSITIVE, "Should have case sensitivity enabled by default" ); + }); + cx.run_until_parked(); - // Update the search query's text to `test\c` to check if the - // case sensitivity search option is correctly disabled. - let query = "test\\c"; - search_bar.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); - }); - - search_bar.apply_pattern_items(cx); + cx.simulate_input("\\c"); + cx.run_until_parked(); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\c"); assert_eq!( search_bar.search_options, SearchOptions::NONE, "Should have no case sensitivity enabled when \\c pattern item is present" ); + }); - // Remove `\\c` from the query to check if the search option is - // correctly reverted to its default state. - let query = "test"; - search_bar.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); - }); + cx.simulate_keystrokes("backspace backspace"); - search_bar.apply_pattern_items(cx); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test"); assert_eq!( search_bar.search_options, SearchOptions::CASE_SENSITIVE, From aa075940578550735c2f743c001d55bba883ef87 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Wed, 23 Jul 2025 10:04:25 +0100 Subject: [PATCH 06/15] refactor: use regex over fancy-regex in pattern-items implementation --- Cargo.lock | 2 +- crates/search/Cargo.toml | 2 +- crates/search/src/buffer_search.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5017c42b6f..e649207720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14568,12 +14568,12 @@ dependencies = [ "client", "collections", "editor", - "fancy-regex 0.14.0", "futures 0.3.31", "gpui", "language", "menu", "project", + "regex", "schemars", "serde", "serde_json", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 20a69694dd..95e568704b 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -42,7 +42,7 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true -fancy-regex.workspace = true +regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e1cbfd3051..c26dfb2701 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -13,7 +13,6 @@ use editor::{ DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; -use fancy_regex::Regex; use futures::channel::oneshot; use gpui::{ Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, @@ -25,6 +24,7 @@ use project::{ search::SearchQuery, search_history::{SearchHistory, SearchHistoryCursor}, }; +use regex::Regex; use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; @@ -657,7 +657,7 @@ impl BufferSearchBar { let pattern_items_regex = Regex::new( &PATTERN_ITEMS .iter() - .map(|(pattern, _, _)| fancy_regex::escape(pattern)) + .map(|(pattern, _, _)| regex::escape(pattern)) .collect::>() .join("|"), ) From e53d9e3d017e5186c4b2278706171c0e49d28f81 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 29 Jul 2025 19:15:50 +0100 Subject: [PATCH 07/15] refactor: only apply pattern items if regex is enabled --- crates/search/src/buffer_search.rs | 63 +++++++++++++++++++----------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c26dfb2701..ee1077ad6d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1483,25 +1483,28 @@ impl BufferSearchBar { } // Determines which pattern items are present in the search query and - // updates the search options accordingly. + // updates the search options accordingly, only if the regex search option + // is enabled. fn apply_pattern_items(&mut self, cx: &mut Context) { - // Start from the default search options to ensure that any search - // option that is not to be updated does not changed. - // For example, if `\c` was present in the query and the case - // sensitivity was initially enabled, removing `\c` from the query - // should re-enable case sensitivity. - let mut search_options = self.default_options; - let query = self.raw_query(cx); + if self.search_options.contains(SearchOptions::REGEX) { + // Start from the default search options to ensure that any search + // option that is not to be updated does not changed. + // For example, if `\c` was present in the query and the case + // sensitivity was initially enabled, removing `\c` from the query + // should re-enable case sensitivity. + let mut search_options = self.default_options; + let query = self.raw_query(cx); - for (pattern, search_option, value) in PATTERN_ITEMS { - match (query.contains(pattern), value) { - (true, true) => search_options.set(*search_option, value), - (true, false) => search_options.set(*search_option, value), - (false, _) => (), + for (pattern, search_option, value) in PATTERN_ITEMS { + match (query.contains(pattern), value) { + (true, true) => search_options.set(*search_option, value), + (true, false) => search_options.set(*search_option, value), + (false, _) => (), + } } - } - self.set_search_options(search_options, cx); + self.set_search_options(search_options, cx); + } } fn adjust_query_regex_language(&self, cx: &mut App) { @@ -2921,8 +2924,24 @@ mod tests { search_bar.update_in(cx, |search_bar, _, _| { assert_eq!( search_bar.search_options, - SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when \\C pattern item is present" + SearchOptions::NONE, + "Should not apply pattern items if regex not enabled" + ); + }); + + cx.simulate_keystrokes("backspace backspace"); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); + }); + + cx.simulate_input("\\C"); + + search_bar.update_in(cx, |search_bar, _, _| { + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled when \\C pattern item is present and regex is enabled" ); }); @@ -2933,14 +2952,14 @@ mod tests { assert_eq!(search_bar.raw_query(cx), "test"); assert_eq!( search_bar.search_options, - SearchOptions::NONE, - "Should have case sensitivity disable when \\C pattern item is removed" + SearchOptions::REGEX, + "Should have case sensitivity disabled when \\C pattern item is removed" ); search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); assert_eq!( search_bar.search_options, - SearchOptions::CASE_SENSITIVE, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, "Should have case sensitivity enabled by default" ); }); @@ -2952,7 +2971,7 @@ mod tests { assert_eq!(search_bar.raw_query(cx), "test\\c"); assert_eq!( search_bar.search_options, - SearchOptions::NONE, + SearchOptions::REGEX, "Should have no case sensitivity enabled when \\c pattern item is present" ); }); @@ -2963,7 +2982,7 @@ mod tests { assert_eq!(search_bar.raw_query(cx), "test"); assert_eq!( search_bar.search_options, - SearchOptions::CASE_SENSITIVE, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, "Should have case sensitivity enabled when \\c pattern item is removed" ); }); From 7366112f5683f16bfd07b03a1793bbb49c962412 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Mon, 18 Aug 2025 22:28:39 +0100 Subject: [PATCH 08/15] fix: fix issue with case-sensitivity turning off after first character This commit fixes an issue with the pattern items implementation where, in vim mode, deploying the buffer search through `/` and then typing any character would automatically disable the regex setting. After some research this was tracked down to the fact that the `default_options` don't actually contain all of the settings that are enabled when the buffer search bar is shown with `/`, as the vim layer is the one that ends up calling the `set_search_options` method with the `SearchOptions::REGEX` option enabled. Calling this method doesn't actually update the `default_options`, and I believe this should probably not be changed. As such, in order to prevent this issue, this commit introduces a new `pattern_item_options` vector to the `BufferSearchBar`, which simply keeps track of the search options that were either enabled/disabled by pattern items in the search query, that actually had impact on the search options. By keeping track of this, one can easily just revert the options, by calling `toggle_search_option`, seeing as we only keep track of the ones that actually had any effect on the options, and revert to all of the options that were enabled when the buffer search bar was deployed, be it on the `default_options` or not. I believe the current implementation can probably be improved with a simple `SearchOptions` and then using the `exclusion` or `intersection` methods but have yet to dedicate more time to this. On the other hand, there's yet another issue that surfaced while working on this: 1. Type `Main\c\C\c` in the search query 2. Confirm that case-sensitivity is enabled This happens because we're not actually keeping track of the order the pattern items are applied in the search query, so the order they're defined in `PATTERN_ITEMS` takes precendence, we'll likely need to capture the matches so we can then actually leverage the order to determine what the final option should be. --- crates/search/src/buffer_search.rs | 67 ++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ee1077ad6d..8f8fc544a4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -121,6 +121,11 @@ pub struct BufferSearchBar { pending_search: Option>, search_options: SearchOptions, default_options: SearchOptions, + /// List of search options and its state derived from the pattern items in + /// the search query. Keeping track of these values allows us to determine + /// which search options are enabled/disabled by the pattern items, as well + /// as reverting any changes made to the search options. + pattern_item_options: Vec<(SearchOptions, bool)>, configured_options: SearchOptions, query_error: Option, dismissed: bool, @@ -675,6 +680,7 @@ impl BufferSearchBar { default_options: search_options, configured_options: search_options, search_options, + pattern_item_options: Vec::new(), pending_search: None, query_error: None, dismissed: true, @@ -1485,24 +1491,59 @@ impl BufferSearchBar { // Determines which pattern items are present in the search query and // updates the search options accordingly, only if the regex search option // is enabled. + // + // TODO: How to deal with the case where cancelling pattern items are + // available? For example, `bananas\c\C` or `bananas\C\c`. We'd probably + // need to actually capture the matches with a Regex, so we can iterate over + // them in order and get the correct order, for example, finding `\c\C\c` in + // a string would eventually equate to `[(CASE_SENSITIVE, false), + // (CASE_INSENSITIVE, true), (CASE_SENSITIVE, false)]` + // + // TODO: Reimplement this so that we can simply use another `SearchOptions` + // for the pattern items, so that excluding that second one from the + // `self.search_options` leads to the search options before pattern items + // options were applied. fn apply_pattern_items(&mut self, cx: &mut Context) { if self.search_options.contains(SearchOptions::REGEX) { - // Start from the default search options to ensure that any search - // option that is not to be updated does not changed. - // For example, if `\c` was present in the query and the case - // sensitivity was initially enabled, removing `\c` from the query - // should re-enable case sensitivity. - let mut search_options = self.default_options; + // Recalculate the search options before the pattern items were + // applied, so we can reapply them and determine which ones are + // actually affecting the search options. + // TODO: This can probably be improved by simply verifying if any new + // pattern items are available, seeing as it should be incremental. + let mut search_options = self.search_options; + self.pattern_item_options + .iter() + .rev() + .for_each(|(search_option, _value)| { + // TODO: Do we actually care about the `value`? If the + // search option was added to `pattern_item_options`, we + // already know that it affected the `search_options` so + // simply toggleing it should have the desired effect of + // reverting the changes. + search_options.toggle(*search_option); + }); + + let mut pattern_item_options = Vec::new(); let query = self.raw_query(cx); - for (pattern, search_option, value) in PATTERN_ITEMS { - match (query.contains(pattern), value) { - (true, true) => search_options.set(*search_option, value), - (true, false) => search_options.set(*search_option, value), - (false, _) => (), - } - } + PATTERN_ITEMS + .iter() + .filter(|(pattern, _, _)| query.contains(pattern)) + .for_each(|(_pattern, search_option, value)| { + match (search_options.contains(**search_option), value) { + (true, false) => { + search_options.toggle(**search_option); + pattern_item_options.push((**search_option, false)); + } + (false, true) => { + search_options.toggle(**search_option); + pattern_item_options.push((**search_option, true)); + } + (_, _) => {} + } + }); + self.pattern_item_options = pattern_item_options; self.set_search_options(search_options, cx); } } From 09470b9d577b58f05bb9ee263c7a06f7fa6b78ac Mon Sep 17 00:00:00 2001 From: dinocosta Date: Mon, 18 Aug 2025 22:57:54 +0100 Subject: [PATCH 09/15] fix: fix issue with conflicting pattern items Fix issue where having conflicting pattern items in the search query could lead to erroneous results. For example, if the search query was `test\c\C\c`, one would expect for the case sensitivity option to be disabled, seeing as `\c` is the last pattern item. However, since the code was simply iterating over `PATTERN_ITEMS`, it did not take into consideration that: 1. The same pattern item could appear more than once in the query 2. The order of the pattern item in the query should take precedence over the order of the pattern item in `PATTERN_ITEMS` As such, this commit fixes that so that the implementation actually iterates over the pattern items capture in the query, ensuring that, if `\c\C\c` is found, the last pattern item is the one that will be applied. --- crates/search/src/buffer_search.rs | 62 +++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8f8fc544a4..bf8c6e1ed8 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -51,6 +51,8 @@ const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; /// value. /// When any of the patterns is present in the search query, the corresponding /// search option, and value, is applied. +// TODO: Should this be updated to an HashMap so we can easily determine the +// search option for a given pattern? static PATTERN_ITEMS: [(&str, &SearchOptions, bool); 2] = [ ("\\c", &SearchOptions::CASE_SENSITIVE, false), ("\\C", &SearchOptions::CASE_SENSITIVE, true), @@ -1526,10 +1528,16 @@ impl BufferSearchBar { let mut pattern_item_options = Vec::new(); let query = self.raw_query(cx); - PATTERN_ITEMS - .iter() - .filter(|(pattern, _, _)| query.contains(pattern)) - .for_each(|(_pattern, search_option, value)| { + self.pattern_items_regex + .captures_iter(&query) + .map(|capture| capture.extract()) + .map(|(str, [])| { + PATTERN_ITEMS + .iter() + .find(|(pattern, _, _)| *pattern == str) + .expect("should only capture valid pattern items") + }) + .for_each(|(_, search_option, value)| { match (search_options.contains(**search_option), value) { (true, false) => { search_options.toggle(**search_option); @@ -3017,6 +3025,50 @@ mod tests { ); }); + cx.simulate_input("\\C"); + cx.run_until_parked(); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\c\\C"); + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled when \\C pattern item is present, even if preceeded by \\c" + ); + }); + + cx.simulate_input("\\c"); + cx.run_until_parked(); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\c\\C\\c"); + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX, + "Should have no case sensitivity enabled when \\c pattern item is present, even if preceeded by \\C" + ); + }); + + cx.simulate_keystrokes("backspace backspace"); + + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\c\\C"); + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Should have case sensitivity enabled when suffix \\c pattern item is removed" + ); + }); + + cx.simulate_keystrokes("backspace backspace"); + + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\c"); + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX, + "Should have case sensitivity disabled when suffix \\C pattern item is removed and a \\c pattern item is still present" + ); + }); + cx.simulate_keystrokes("backspace backspace"); search_bar.update(cx, |search_bar, cx| { @@ -3024,7 +3076,7 @@ mod tests { assert_eq!( search_bar.search_options, SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when \\c pattern item is removed" + "Should have case sensitivity enabled when all pattern items are removed and original search options are restored" ); }); } From f7473c80ae26bc10487223a11db37d295782fc38 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Mon, 18 Aug 2025 23:37:57 +0100 Subject: [PATCH 10/15] fix: fix issue with \ preceding pattern items With the current implementation of pattern items, it's impossible to search for strings like `\c` in buffers as, even when using `\\c`, the `\c` suffix will be interpreted as a pattern item. This commit updates the regex used to find the pattern items so as to ensure that the preceding character is never a `\` and, if it is, it's considered that the user is searching for a slash instead of trying to use a pattern item. --- Cargo.lock | 2 +- crates/search/Cargo.toml | 2 +- crates/search/src/buffer_search.rs | 38 ++++++++++++++++++++---------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e649207720..5017c42b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14568,12 +14568,12 @@ dependencies = [ "client", "collections", "editor", + "fancy-regex 0.14.0", "futures 0.3.31", "gpui", "language", "menu", "project", - "regex", "schemars", "serde", "serde_json", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 95e568704b..20a69694dd 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -42,7 +42,7 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true -regex.workspace = true +fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index bf8c6e1ed8..d1e3467c58 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -13,6 +13,7 @@ use editor::{ DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; +use fancy_regex::Regex; use futures::channel::oneshot; use gpui::{ Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, @@ -24,7 +25,6 @@ use project::{ search::SearchQuery, search_history::{SearchHistory, SearchHistoryCursor}, }; -use regex::Regex; use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; @@ -54,8 +54,8 @@ const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; // TODO: Should this be updated to an HashMap so we can easily determine the // search option for a given pattern? static PATTERN_ITEMS: [(&str, &SearchOptions, bool); 2] = [ - ("\\c", &SearchOptions::CASE_SENSITIVE, false), - ("\\C", &SearchOptions::CASE_SENSITIVE, true), + ("c", &SearchOptions::CASE_SENSITIVE, false), + ("C", &SearchOptions::CASE_SENSITIVE, true), ]; /// Opens the buffer search interface with the specified configuration. @@ -661,13 +661,13 @@ impl BufferSearchBar { .detach_and_log_err(cx); } - let pattern_items_regex = Regex::new( - &PATTERN_ITEMS + let pattern_items_regex = Regex::new(&format!( + r"(?>() - .join("|"), - ) + .map(|(pattern, _, _)| *pattern) + .collect::() + )) .unwrap(); Self { @@ -1528,13 +1528,16 @@ impl BufferSearchBar { let mut pattern_item_options = Vec::new(); let query = self.raw_query(cx); + // TODO: Maybe avoid so many unwrap/expect calls here. self.pattern_items_regex .captures_iter(&query) - .map(|capture| capture.extract()) - .map(|(str, [])| { + .map(|capture| capture.unwrap()) + .map(|capture| { + let pattern_item = capture.get(1).unwrap().as_str(); + PATTERN_ITEMS .iter() - .find(|(pattern, _, _)| *pattern == str) + .find(|(pattern, _, _)| pattern_item.ends_with(*pattern)) .expect("should only capture valid pattern items") }) .for_each(|(_, search_option, value)| { @@ -3079,6 +3082,17 @@ mod tests { "Should have case sensitivity enabled when all pattern items are removed and original search options are restored" ); }); + + cx.simulate_input("\\\\c"); + cx.run_until_parked(); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.raw_query(cx), "test\\\\c"); + assert_eq!( + search_bar.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Should still have case sensitivity enabled when pattern item is preceded by another \\" + ); + }); } fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { From 835f613fd66152cb9259795d76aac5241728eaae Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 19 Aug 2025 19:05:30 +0100 Subject: [PATCH 11/15] refactor: reimplement pattern items as an enum Update the way pattern items are implemented so that, instead of saving these in an array with the character representation, the search option and its value, it now is a `PatternItem` enum, and the enum implements several methods: - `character` - Returns the character representation for the pattern item item, without the backslash. - `search_option` - Returns the search option and its desired value. The `TryFrom<&str>` trait is also implemented for `PatternItem` so that we can simply build a `PatternItem` from a string like `"\\c"`. --- crates/search/src/buffer_search.rs | 100 +++++++++-------------------- crates/search/src/search.rs | 43 +++++++++++++ 2 files changed, 72 insertions(+), 71 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d1e3467c58..7b9c27fc55 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,9 +1,10 @@ mod registrar; use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, - SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + FocusSearch, NextHistoryQuery, PatternItem, PreviousHistoryQuery, ReplaceAll, ReplaceNext, + SearchOption, SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, + SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, + ToggleWholeWord, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; @@ -47,17 +48,6 @@ use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; -/// Array of supported pattern items and their corresponding search options and -/// value. -/// When any of the patterns is present in the search query, the corresponding -/// search option, and value, is applied. -// TODO: Should this be updated to an HashMap so we can easily determine the -// search option for a given pattern? -static PATTERN_ITEMS: [(&str, &SearchOptions, bool); 2] = [ - ("c", &SearchOptions::CASE_SENSITIVE, false), - ("C", &SearchOptions::CASE_SENSITIVE, true), -]; - /// Opens the buffer search interface with the specified configuration. #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] #[action(namespace = buffer_search)] @@ -123,11 +113,11 @@ pub struct BufferSearchBar { pending_search: Option>, search_options: SearchOptions, default_options: SearchOptions, - /// List of search options and its state derived from the pattern items in - /// the search query. Keeping track of these values allows us to determine - /// which search options are enabled/disabled by the pattern items, as well - /// as reverting any changes made to the search options. - pattern_item_options: Vec<(SearchOptions, bool)>, + /// List of search options that were enabled or disabled by the pattern + /// items in the search query. + /// By toggling each search option in reverse order, one can obtain the + /// original search options before pattern items were applied. + pattern_item_options: Vec, configured_options: SearchOptions, query_error: Option, dismissed: bool, @@ -663,9 +653,9 @@ impl BufferSearchBar { let pattern_items_regex = Regex::new(&format!( r"(?() )) .unwrap(); @@ -1493,64 +1483,32 @@ impl BufferSearchBar { // Determines which pattern items are present in the search query and // updates the search options accordingly, only if the regex search option // is enabled. - // - // TODO: How to deal with the case where cancelling pattern items are - // available? For example, `bananas\c\C` or `bananas\C\c`. We'd probably - // need to actually capture the matches with a Regex, so we can iterate over - // them in order and get the correct order, for example, finding `\c\C\c` in - // a string would eventually equate to `[(CASE_SENSITIVE, false), - // (CASE_INSENSITIVE, true), (CASE_SENSITIVE, false)]` - // - // TODO: Reimplement this so that we can simply use another `SearchOptions` - // for the pattern items, so that excluding that second one from the - // `self.search_options` leads to the search options before pattern items - // options were applied. fn apply_pattern_items(&mut self, cx: &mut Context) { if self.search_options.contains(SearchOptions::REGEX) { - // Recalculate the search options before the pattern items were - // applied, so we can reapply them and determine which ones are - // actually affecting the search options. - // TODO: This can probably be improved by simply verifying if any new - // pattern items are available, seeing as it should be incremental. - let mut search_options = self.search_options; - self.pattern_item_options - .iter() - .rev() - .for_each(|(search_option, _value)| { - // TODO: Do we actually care about the `value`? If the - // search option was added to `pattern_item_options`, we - // already know that it affected the `search_options` so - // simply toggleing it should have the desired effect of - // reverting the changes. + // Determine what the search options were before the pattern items + // were applied, so we can reapply them and determine which ones + // actually have an effect on the search options, which are the ones + // we need to keep track of. + let mut search_options = self.pattern_item_options.iter().rev().fold( + self.search_options, + |mut search_options, search_option| { search_options.toggle(*search_option); - }); + search_options + }, + ); - let mut pattern_item_options = Vec::new(); let query = self.raw_query(cx); + let mut pattern_item_options = Vec::new(); - // TODO: Maybe avoid so many unwrap/expect calls here. self.pattern_items_regex .captures_iter(&query) - .map(|capture| capture.unwrap()) - .map(|capture| { - let pattern_item = capture.get(1).unwrap().as_str(); - - PATTERN_ITEMS - .iter() - .find(|(pattern, _, _)| pattern_item.ends_with(*pattern)) - .expect("should only capture valid pattern items") - }) - .for_each(|(_, search_option, value)| { - match (search_options.contains(**search_option), value) { - (true, false) => { - search_options.toggle(**search_option); - pattern_item_options.push((**search_option, false)); - } - (false, true) => { - search_options.toggle(**search_option); - pattern_item_options.push((**search_option, true)); - } - (_, _) => {} + .filter_map(|capture| capture.ok()?.get(1)) + .filter_map(|capture| PatternItem::try_from(capture.as_str()).ok()) + .map(|pattern_item| pattern_item.search_option()) + .for_each(|(search_option, value)| { + if search_options.contains(search_option) != value { + search_options.toggle(search_option); + pattern_item_options.push(search_option); } }); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 65e59fd5de..e97d508f10 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -204,3 +204,46 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { }) }); } + +/// A `PatternItem` is a character, preceded by a backslash, that can be used to +/// modify the search options. +/// For example, using `\c` in a search query will make the search +/// case-insensitive, while `\C` will make it case-sensitive. +enum PatternItem { + CaseSensitiveFalse, + CaseSensitiveTrue, +} + +impl TryFrom<&str> for PatternItem { + type Error = anyhow::Error; + + fn try_from(str: &str) -> Result { + match str { + "\\c" => Ok(Self::CaseSensitiveFalse), + "\\C" => Ok(Self::CaseSensitiveTrue), + _ => anyhow::bail!("Invalid pattern item: {}", str), + } + } +} + +impl PatternItem { + /// Representation of the pattern item as a single character, without the + /// backslash. + fn character(&self) -> char { + match self { + Self::CaseSensitiveFalse => 'c', + Self::CaseSensitiveTrue => 'C', + } + } + + fn search_option(&self) -> (SearchOptions, bool) { + match self { + Self::CaseSensitiveFalse => (SearchOptions::CASE_SENSITIVE, false), + Self::CaseSensitiveTrue => (SearchOptions::CASE_SENSITIVE, true), + } + } + + fn all_variants() -> &'static [Self] { + &[Self::CaseSensitiveFalse, Self::CaseSensitiveTrue] + } +} From 7f9e589491b9fabc698ae2340e25433446e3067d Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 19 Aug 2025 23:23:07 +0100 Subject: [PATCH 12/15] refactor: introduce pattern_items module --- crates/search/src/buffer_search.rs | 62 ++----- crates/search/src/pattern_items.rs | 254 +++++++++++++++++++++++++++++ crates/search/src/search.rs | 44 +---- 3 files changed, 268 insertions(+), 92 deletions(-) create mode 100644 crates/search/src/pattern_items.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7b9c27fc55..ac9d48df46 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,10 +1,10 @@ mod registrar; use crate::{ - FocusSearch, NextHistoryQuery, PatternItem, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOption, SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, - SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, - ToggleWholeWord, + FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, + SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + pattern_items::PatternItems, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; @@ -14,7 +14,6 @@ use editor::{ DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; -use fancy_regex::Regex; use futures::channel::oneshot; use gpui::{ Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, @@ -113,11 +112,10 @@ pub struct BufferSearchBar { pending_search: Option>, search_options: SearchOptions, default_options: SearchOptions, - /// List of search options that were enabled or disabled by the pattern - /// items in the search query. - /// By toggling each search option in reverse order, one can obtain the - /// original search options before pattern items were applied. - pattern_item_options: Vec, + /// Pattern items that have been applied from the query to the search + /// options. Tracking these allows us to revert the search options to their + /// original state as pattern items are removed from the query. + pattern_items: PatternItems, configured_options: SearchOptions, query_error: Option, dismissed: bool, @@ -129,7 +127,6 @@ pub struct BufferSearchBar { editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, regex_language: Option>, - pattern_items_regex: Regex, } impl BufferSearchBar { @@ -651,15 +648,6 @@ impl BufferSearchBar { .detach_and_log_err(cx); } - let pattern_items_regex = Regex::new(&format!( - r"(?() - )) - .unwrap(); - Self { query_editor, query_editor_focused: false, @@ -672,7 +660,7 @@ impl BufferSearchBar { default_options: search_options, configured_options: search_options, search_options, - pattern_item_options: Vec::new(), + pattern_items: Default::default(), pending_search: None, query_error: None, dismissed: true, @@ -688,7 +676,6 @@ impl BufferSearchBar { editor_scroll_handle: ScrollHandle::new(), editor_needed_width: px(0.), regex_language: None, - pattern_items_regex, } } @@ -836,9 +823,7 @@ impl BufferSearchBar { /// Returns the sanitized query string with pattern items removed. pub fn query(&self, cx: &App) -> String { - self.pattern_items_regex - .replace_all(&self.raw_query(cx), "") - .into_owned() + PatternItems::clean_query(&self.raw_query(cx)) } pub fn replacement(&self, cx: &mut App) -> String { @@ -1489,31 +1474,10 @@ impl BufferSearchBar { // were applied, so we can reapply them and determine which ones // actually have an effect on the search options, which are the ones // we need to keep track of. - let mut search_options = self.pattern_item_options.iter().rev().fold( - self.search_options, - |mut search_options, search_option| { - search_options.toggle(*search_option); - search_options - }, - ); - let query = self.raw_query(cx); - let mut pattern_item_options = Vec::new(); - - self.pattern_items_regex - .captures_iter(&query) - .filter_map(|capture| capture.ok()?.get(1)) - .filter_map(|capture| PatternItem::try_from(capture.as_str()).ok()) - .map(|pattern_item| pattern_item.search_option()) - .for_each(|(search_option, value)| { - if search_options.contains(search_option) != value { - search_options.toggle(search_option); - pattern_item_options.push(search_option); - } - }); - - self.pattern_item_options = pattern_item_options; - self.set_search_options(search_options, cx); + let search_options = self.pattern_items.revert(self.search_options); + self.pattern_items = PatternItems::from_search_options(search_options, &query); + self.set_search_options(self.pattern_items.apply(search_options), cx); } } diff --git a/crates/search/src/pattern_items.rs b/crates/search/src/pattern_items.rs new file mode 100644 index 0000000000..fa4cbeac06 --- /dev/null +++ b/crates/search/src/pattern_items.rs @@ -0,0 +1,254 @@ +use crate::SearchOptions; +use anyhow; +use fancy_regex::Regex; +use std::sync::LazyLock; + +/// A `PatternItem` is a character, preceded by a backslash, that can be used to +/// modify the search options. +/// For example, using `\c` in a search query will make the search +/// case-insensitive, while `\C` will make it case-sensitive. +#[derive(Clone, Debug, PartialEq)] +enum PatternItem { + CaseSensitiveFalse, + CaseSensitiveTrue, +} + +/// Regex for matching pattern items in a search query. +pub static PATTERN_ITEMS_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(&format!( + r"(?() + )) + .expect("Failed to compile pattern items regex") +}); + +impl TryFrom<&str> for PatternItem { + type Error = anyhow::Error; + + fn try_from(str: &str) -> Result { + match str { + "\\c" => Ok(Self::CaseSensitiveFalse), + "\\C" => Ok(Self::CaseSensitiveTrue), + _ => anyhow::bail!("Invalid pattern item: {}", str), + } + } +} + +impl PatternItem { + /// Representation of the pattern item as a single character, without the + /// backslash. + pub fn character(&self) -> char { + match self { + Self::CaseSensitiveFalse => 'c', + Self::CaseSensitiveTrue => 'C', + } + } + + pub fn search_option(&self) -> (SearchOptions, bool) { + match self { + Self::CaseSensitiveFalse => (SearchOptions::CASE_SENSITIVE, false), + Self::CaseSensitiveTrue => (SearchOptions::CASE_SENSITIVE, true), + } + } + + fn all_variants() -> &'static [Self] { + &[Self::CaseSensitiveFalse, Self::CaseSensitiveTrue] + } +} + +#[derive(Default)] +pub struct PatternItems { + items: Vec, +} + +impl PatternItems { + /// Builds the list of pattern items that, from the provided search options + /// and query, do actually affect the search options. + /// For example, if search options is `SearchOptions::CASE_SENSITIVE`, and + /// the query only contains `\C`, then the pattern item will not have an + /// effect on the sarch options. + pub fn from_search_options(search_options: SearchOptions, query: &str) -> Self { + let mut search_options = search_options; + let mut pattern_items: Vec = Vec::new(); + + Self::extract_from_query(query) + .iter() + .for_each(|pattern_item| { + let (search_option, value) = pattern_item.search_option(); + + if search_options.contains(search_option) != value { + search_options.toggle(search_option); + pattern_items.push(pattern_item.clone()); + } + }); + + Self { + items: pattern_items, + } + } + + /// Replaces all pattern items in the provided string with an empty string. + pub fn clean_query(str: &str) -> String { + PATTERN_ITEMS_REGEX.replace_all(str, "").into_owned() + } + + /// Calculates what the provided search options looked liked before the + /// pattern items were applied. + pub fn revert(&self, search_options: SearchOptions) -> SearchOptions { + let mut result = search_options; + + self.items + .iter() + .rev() + .map(PatternItem::search_option) + .for_each(|(search_option, value)| result.set(search_option, !value)); + + result + } + + /// Returns the search options after applying the pattern items. + pub fn apply(&self, search_options: SearchOptions) -> SearchOptions { + let mut search_options = search_options; + + self.items + .iter() + .map(PatternItem::search_option) + .for_each(|(search_option, value)| search_options.set(search_option, value)); + + search_options + } + + /// Extracts all pattern items from the provided string. + fn extract_from_query(str: &str) -> Vec { + PATTERN_ITEMS_REGEX + .captures_iter(str) + .filter_map(|capture| capture.ok()?.get(1)) + .filter_map(|capture| PatternItem::try_from(capture.as_str()).ok()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clean_query() { + let query = "Main\\c\\C"; + let cleaned_query = PatternItems::clean_query(query); + assert_eq!(cleaned_query, "Main"); + } + + #[test] + fn test_apply() { + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + assert_eq!(pattern_items.apply(search_options), search_options); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + assert_eq!(pattern_items.apply(search_options), SearchOptions::NONE); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + assert_eq!(pattern_items.apply(search_options), search_options); + + let search_options = SearchOptions::NONE; + let query = "Main\\c\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + assert_eq!( + pattern_items.apply(search_options), + SearchOptions::CASE_SENSITIVE + ); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c\\C\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + assert_eq!(pattern_items.apply(search_options), SearchOptions::NONE); + } + + #[test] + fn test_revert() { + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + let updated_search_options = pattern_items.apply(search_options); + assert_eq!(updated_search_options, SearchOptions::NONE); + assert_eq!(pattern_items.revert(updated_search_options), search_options); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + let updated_search_options = pattern_items.apply(search_options); + assert_eq!(updated_search_options, SearchOptions::NONE); + assert_eq!(pattern_items.revert(updated_search_options), search_options); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + let updated_search_options = pattern_items.apply(search_options); + assert_eq!(updated_search_options, search_options); + assert_eq!(pattern_items.revert(updated_search_options), search_options); + + let search_options = SearchOptions::NONE; + let query = "Main\\c\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + let updated_search_options = pattern_items.apply(search_options); + assert_eq!(updated_search_options, SearchOptions::CASE_SENSITIVE); + assert_eq!(pattern_items.revert(updated_search_options), search_options); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c\\C\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + let updated_search_options = pattern_items.apply(search_options); + assert_eq!(updated_search_options, SearchOptions::NONE); + assert_eq!(pattern_items.revert(updated_search_options), search_options); + } + + #[test] + fn test_extract_from_query() { + let query = "Main\\c\\C\\c"; + let pattern_items = PatternItems::extract_from_query(query); + assert_eq!( + pattern_items, + vec![ + PatternItem::CaseSensitiveFalse, + PatternItem::CaseSensitiveTrue, + PatternItem::CaseSensitiveFalse + ] + ); + } + + #[test] + fn test_from_search_options() { + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + + assert_eq!(pattern_items.items, vec![]); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\c"; + let pattern_items = PatternItems::from_search_options(search_options, query); + + assert_eq!(pattern_items.items, vec![PatternItem::CaseSensitiveFalse]); + + let search_options = SearchOptions::CASE_SENSITIVE; + let query = "Main\\C\\c\\C"; + let pattern_items = PatternItems::from_search_options(search_options, query); + + assert_eq!( + pattern_items.items, + vec![ + PatternItem::CaseSensitiveFalse, + PatternItem::CaseSensitiveTrue + ] + ); + } +} diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index e97d508f10..4d216cca00 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -14,6 +14,7 @@ pub use search_status_button::SEARCH_ICON; use crate::project_search::ProjectSearchBar; pub mod buffer_search; +pub mod pattern_items; pub mod project_search; pub(crate) mod search_bar; pub mod search_status_button; @@ -204,46 +205,3 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { }) }); } - -/// A `PatternItem` is a character, preceded by a backslash, that can be used to -/// modify the search options. -/// For example, using `\c` in a search query will make the search -/// case-insensitive, while `\C` will make it case-sensitive. -enum PatternItem { - CaseSensitiveFalse, - CaseSensitiveTrue, -} - -impl TryFrom<&str> for PatternItem { - type Error = anyhow::Error; - - fn try_from(str: &str) -> Result { - match str { - "\\c" => Ok(Self::CaseSensitiveFalse), - "\\C" => Ok(Self::CaseSensitiveTrue), - _ => anyhow::bail!("Invalid pattern item: {}", str), - } - } -} - -impl PatternItem { - /// Representation of the pattern item as a single character, without the - /// backslash. - fn character(&self) -> char { - match self { - Self::CaseSensitiveFalse => 'c', - Self::CaseSensitiveTrue => 'C', - } - } - - fn search_option(&self) -> (SearchOptions, bool) { - match self { - Self::CaseSensitiveFalse => (SearchOptions::CASE_SENSITIVE, false), - Self::CaseSensitiveTrue => (SearchOptions::CASE_SENSITIVE, true), - } - } - - fn all_variants() -> &'static [Self] { - &[Self::CaseSensitiveFalse, Self::CaseSensitiveTrue] - } -} From d28e52d9e7fdd2748faf4cd1364acc47246d4d8d Mon Sep 17 00:00:00 2001 From: dinocosta Date: Wed, 20 Aug 2025 10:06:08 +0100 Subject: [PATCH 13/15] feat: add pattern items to project search Update the `search::project_search::ProjectSearchView` implementation in order to allow users to leverage pattern items in the search query. Add a very basic `test_pattern_items` test to `search::project_search` in order to test how the `apply_pattern_items` method affects the `ProjectSearchView.search_options` value depending on the query editor's text. Unfortunately I wasn't having much luck adding a text similar to the one for `BufferSearchBar` where we actually simulate the user's input and keystrokes, so definitely something that can be improved upon. --- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 163 ++++++++++++++++++++++++++-- 2 files changed, 155 insertions(+), 14 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ac9d48df46..99ab54601f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1570,7 +1570,7 @@ mod tests { }); let search_bar = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(*window, cx).as_mut(); + let cx = VisualTestContext::from_window(*window, cx).into_mut(); (editor.unwrap(), search_bar, cx) } @@ -2957,7 +2957,7 @@ mod tests { assert_eq!( search_bar.search_options, SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when \\C pattern item is present, even if preceeded by \\c" + "Should have case sensitivity enabled when \\C pattern item is present, even if preceded by \\c" ); }); @@ -2968,7 +2968,7 @@ mod tests { assert_eq!( search_bar.search_options, SearchOptions::REGEX, - "Should have no case sensitivity enabled when \\c pattern item is present, even if preceeded by \\C" + "Should have no case sensitivity enabled when \\c pattern item is present, even if preceded by \\C" ); }); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588af..c042f4d861 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3,6 +3,7 @@ use crate::{ SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, + pattern_items::PatternItems, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; @@ -210,6 +211,7 @@ pub struct ProjectSearchView { replacement_editor: Entity, results_editor: Entity, search_options: SearchOptions, + pattern_items: PatternItems, panels_with_errors: HashMap, active_match_index: Option, search_id: usize, @@ -775,15 +777,17 @@ impl ProjectSearchView { // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes subscriptions.push( cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event - && EditorSettings::get_global(cx).use_smartcase_search - { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + if let EditorEvent::Edited { .. } = event { + this.apply_pattern_items(&this.search_query_text_raw(cx)); + + if EditorSettings::get_global(cx).use_smartcase_search { + let query = this.search_query_text(cx); + if !query.is_empty() + && this.search_options.contains(SearchOptions::CASE_SENSITIVE) + != contains_uppercase(&query) + { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + } } } cx.emit(ViewEvent::EditorEvent(event.clone())) @@ -880,6 +884,7 @@ impl ProjectSearchView { query_editor, results_editor, search_options: options, + pattern_items: Default::default(), panels_with_errors: HashMap::default(), active_match_index: None, included_files_editor, @@ -1133,13 +1138,19 @@ impl ProjectSearchView { } } - pub fn search_query_text(&self, cx: &App) -> String { + /// Returns the search query text with pattern items. + fn search_query_text_raw(&self, cx: &App) -> String { self.query_editor.read(cx).text(cx) } + /// Returns the search query text without pattern items. + pub fn search_query_text(&self, cx: &App) -> String { + PatternItems::clean_query(&self.search_query_text_raw(cx)) + } + 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 { @@ -1560,6 +1571,21 @@ impl ProjectSearchView { }) } } + + // Determines which pattern items are present in the search query and + // updates the search options accordingly, only if the regex search option + // is enabled. + fn apply_pattern_items(&mut self, query: &str) { + if self.search_options.contains(SearchOptions::REGEX) { + // Determine what the search options were before the pattern items + // were applied, so we can reapply them and determine which ones + // actually have an effect on the search options, which are the ones + // we need to keep track of. + let search_options = self.pattern_items.revert(self.search_options); + self.pattern_items = PatternItems::from_search_options(search_options, &query); + self.search_options = self.pattern_items.apply(search_options); + } + } } fn buffer_search_query( @@ -4126,6 +4152,121 @@ pub mod tests { }); } + #[gpui::test] + async fn test_pattern_items(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/dir"), + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + + let project_search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); + let project_search_settings = ProjectSearchSettings { + filters_enabled: false, + search_options: SearchOptions::REGEX, + }; + let project_search_view = cx.add_window(|window, cx| { + ProjectSearchView::new( + workspace.downgrade(), + project_search.clone(), + window, + cx, + Some(project_search_settings), + ) + }); + + project_search_view + .update(cx, |view, window, cx| { + // Verify that `test\\c` does not change search options, as + // `SearchOptions::CASE_SENSITIVE` is disabled. + view.query_editor.update(cx, |editor, cx| { + editor.set_text("test\\c", window, cx); + }); + + assert_eq!(view.search_query_text_raw(cx), "test\\c"); + assert_eq!(view.search_query_text(cx), "test"); + + view.apply_pattern_items(&view.search_query_text_raw(cx)); + + assert_eq!( + view.search_options, + SearchOptions::REGEX, + "Case sensitivity should remain disabled if only pattern item is '\\c'" + ); + + // Verify that `test\\c\\C\\c` does not change search options, + // as even though `\\C` should enabled + // `SearchOptions::CASE_SENSITIVE`, the last `\\c` should + // disable it. + view.query_editor.update(cx, |editor, cx| { + editor.set_text("test\\c\\C\\c", window, cx); + }); + + assert_eq!(view.search_query_text_raw(cx), "test\\c\\C\\c"); + assert_eq!(view.search_query_text(cx), "test"); + + view.apply_pattern_items(&view.search_query_text_raw(cx)); + + assert_eq!( + view.search_options, + SearchOptions::REGEX, + "Case sensitivity should be disabled if '\\c' follows '\\C'" + ); + + // Verify that a single `\\C` pattern item in the search query + // enables the `SearchOptions::CASE_SENSITIVE` option. + view.query_editor.update(cx, |editor, cx| { + editor.set_text("test\\C", window, cx); + }); + + assert_eq!(view.search_query_text_raw(cx), "test\\C"); + assert_eq!(view.search_query_text(cx), "test"); + + view.apply_pattern_items(&view.search_query_text_raw(cx)); + + assert_eq!( + view.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Case sensitivity should be enabled when '\\C' is used" + ); + + // Since `SearchOptions::CASE_SENSITIVE` is now enabled, we can + // check that a pattern item preceded by a backslash, for + // example, `\\\\c`, does not affect the search option and is + // not even recognized as a valid pattern item. + // We need to clear the `pattern_items` before testing this, as + // calling `apply_pattern_items` will revert it, so the + // `CASE_SENSITIVE` option would be turned off. + view.pattern_items = PatternItems::default(); + view.query_editor.update(cx, |editor, cx| { + editor.set_text("test\\\\c", window, cx); + }); + + assert_eq!(view.search_query_text_raw(cx), "test\\\\c"); + assert_eq!(view.search_query_text(cx), "test\\\\c"); + + view.apply_pattern_items(&view.search_query_text_raw(cx)); + + assert_eq!( + view.search_options, + SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, + "Case sensitivity should remain enabled when pattern item is preceded by a backslash" + ); + }) + .unwrap(); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); From c69f4f49f0ab237ebe511e716c0eae861ecfdc66 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 26 Aug 2025 17:12:29 +0100 Subject: [PATCH 14/15] refactor: implement case sensitive pattern items in search query In order to simplify the implementation of pattern items in search queries, this commit updates the `project::search::SearchQuery::regex` function so as to support both `\\c` and `\\C` in the provided query. This means that we no longer need to have both `BufferSearchBar` and `ProjectSearchView` handling pattern items, so the `search::pattern_items` module can now safely be removed. It's worth noting that, since these are now handled at the `SearchQuery` level, this removes the updates to the UI regarding search options, that were being triggered by the pattern items being processed and applied to the search options. Co-authored-by: Conrad Irwin --- crates/project/src/search.rs | 132 +++++++++++++++++++- crates/search/src/buffer_search.rs | 180 +--------------------------- crates/search/src/project_search.rs | 160 ++----------------------- 3 files changed, 142 insertions(+), 330 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index ee216a9976..f2c6091e0c 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 99ab54601f..5985a62bb5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -4,7 +4,6 @@ use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - pattern_items::PatternItems, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; @@ -112,10 +111,6 @@ pub struct BufferSearchBar { pending_search: Option>, search_options: SearchOptions, default_options: SearchOptions, - /// Pattern items that have been applied from the query to the search - /// options. Tracking these allows us to revert the search options to their - /// original state as pattern items are removed from the query. - pattern_items: PatternItems, configured_options: SearchOptions, query_error: Option, dismissed: bool, @@ -660,7 +655,6 @@ impl BufferSearchBar { default_options: search_options, configured_options: search_options, search_options, - pattern_items: Default::default(), pending_search: None, query_error: None, dismissed: true, @@ -816,14 +810,9 @@ impl BufferSearchBar { }); } - /// Returns the raw query string without any of the pattern items removed. - pub fn raw_query(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - /// Returns the sanitized query string with pattern items removed. pub fn query(&self, cx: &App) -> String { - PatternItems::clean_query(&self.raw_query(cx)) + self.query_editor.read(cx).text(cx) } pub fn replacement(&self, cx: &mut App) -> String { @@ -1055,7 +1044,6 @@ impl BufferSearchBar { editor::EditorEvent::Blurred => self.query_editor_focused = false, editor::EditorEvent::Edited { .. } => { self.smartcase(window, cx); - self.apply_pattern_items(cx); self.clear_matches(window, cx); let search = self.update_matches(false, window, cx); @@ -1465,22 +1453,6 @@ impl BufferSearchBar { } } - // Determines which pattern items are present in the search query and - // updates the search options accordingly, only if the regex search option - // is enabled. - fn apply_pattern_items(&mut self, cx: &mut Context) { - if self.search_options.contains(SearchOptions::REGEX) { - // Determine what the search options were before the pattern items - // were applied, so we can reapply them and determine which ones - // actually have an effect on the search options, which are the ones - // we need to keep track of. - let query = self.raw_query(cx); - let search_options = self.pattern_items.revert(self.search_options); - self.pattern_items = PatternItems::from_search_options(search_options, &query); - self.set_search_options(self.pattern_items.apply(search_options), cx); - } - } - fn adjust_query_regex_language(&self, cx: &mut App) { let enable = self.search_options.contains(SearchOptions::REGEX); let query_buffer = self @@ -2867,156 +2839,6 @@ mod tests { }); } - #[gpui::test] - async fn test_pattern_items(cx: &mut TestAppContext) { - let (_editor, search_bar, cx) = init_test(cx); - - update_search_settings( - SearchSettings { - button: true, - whole_word: false, - case_sensitive: false, - include_ignored: false, - regex: false, - }, - cx, - ); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.show(window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::NONE, - "Should have no search options enabled by default" - ); - - cx.focus_view(&search_bar.query_editor, window); - }); - - cx.simulate_input("test\\C"); - - search_bar.update_in(cx, |search_bar, _, _| { - assert_eq!( - search_bar.search_options, - SearchOptions::NONE, - "Should not apply pattern items if regex not enabled" - ); - }); - - cx.simulate_keystrokes("backspace backspace"); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); - }); - - cx.simulate_input("\\C"); - - search_bar.update_in(cx, |search_bar, _, _| { - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when \\C pattern item is present and regex is enabled" - ); - }); - - // Remove `\\C` from the query to check if the search option is - // correctly reverted to its default state. - cx.simulate_keystrokes("backspace backspace"); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.raw_query(cx), "test"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should have case sensitivity disabled when \\C pattern item is removed" - ); - - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled by default" - ); - }); - cx.run_until_parked(); - - cx.simulate_input("\\c"); - cx.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\c"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should have no case sensitivity enabled when \\c pattern item is present" - ); - }); - - cx.simulate_input("\\C"); - cx.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\c\\C"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when \\C pattern item is present, even if preceded by \\c" - ); - }); - - cx.simulate_input("\\c"); - cx.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\c\\C\\c"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should have no case sensitivity enabled when \\c pattern item is present, even if preceded by \\C" - ); - }); - - cx.simulate_keystrokes("backspace backspace"); - - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\c\\C"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when suffix \\c pattern item is removed" - ); - }); - - cx.simulate_keystrokes("backspace backspace"); - - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\c"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should have case sensitivity disabled when suffix \\C pattern item is removed and a \\c pattern item is still present" - ); - }); - - cx.simulate_keystrokes("backspace backspace"); - - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should have case sensitivity enabled when all pattern items are removed and original search options are restored" - ); - }); - - cx.simulate_input("\\\\c"); - cx.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.raw_query(cx), "test\\\\c"); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Should still have case sensitivity enabled when pattern item is preceded by another \\" - ); - }); - } - fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c042f4d861..0c0f871fb5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3,7 +3,6 @@ use crate::{ SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - pattern_items::PatternItems, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; @@ -211,7 +210,6 @@ pub struct ProjectSearchView { replacement_editor: Entity, results_editor: Entity, search_options: SearchOptions, - pattern_items: PatternItems, panels_with_errors: HashMap, active_match_index: Option, search_id: usize, @@ -777,17 +775,15 @@ impl ProjectSearchView { // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes subscriptions.push( cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event { - this.apply_pattern_items(&this.search_query_text_raw(cx)); - - if EditorSettings::get_global(cx).use_smartcase_search { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - } + if let EditorEvent::Edited { .. } = event + && EditorSettings::get_global(cx).use_smartcase_search + { + let query = this.search_query_text(cx); + if !query.is_empty() + && this.search_options.contains(SearchOptions::CASE_SENSITIVE) + != contains_uppercase(&query) + { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); } } cx.emit(ViewEvent::EditorEvent(event.clone())) @@ -884,7 +880,6 @@ impl ProjectSearchView { query_editor, results_editor, search_options: options, - pattern_items: Default::default(), panels_with_errors: HashMap::default(), active_match_index: None, included_files_editor, @@ -1138,14 +1133,9 @@ impl ProjectSearchView { } } - /// Returns the search query text with pattern items. - fn search_query_text_raw(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - /// Returns the search query text without pattern items. pub fn search_query_text(&self, cx: &App) -> String { - PatternItems::clean_query(&self.search_query_text_raw(cx)) + self.query_editor.read(cx).text(cx) } fn build_search_query(&mut self, cx: &mut Context) -> Option { @@ -1571,21 +1561,6 @@ impl ProjectSearchView { }) } } - - // Determines which pattern items are present in the search query and - // updates the search options accordingly, only if the regex search option - // is enabled. - fn apply_pattern_items(&mut self, query: &str) { - if self.search_options.contains(SearchOptions::REGEX) { - // Determine what the search options were before the pattern items - // were applied, so we can reapply them and determine which ones - // actually have an effect on the search options, which are the ones - // we need to keep track of. - let search_options = self.pattern_items.revert(self.search_options); - self.pattern_items = PatternItems::from_search_options(search_options, &query); - self.search_options = self.pattern_items.apply(search_options); - } - } } fn buffer_search_query( @@ -4152,121 +4127,6 @@ pub mod tests { }); } - #[gpui::test] - async fn test_pattern_items(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let project_search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); - let project_search_settings = ProjectSearchSettings { - filters_enabled: false, - search_options: SearchOptions::REGEX, - }; - let project_search_view = cx.add_window(|window, cx| { - ProjectSearchView::new( - workspace.downgrade(), - project_search.clone(), - window, - cx, - Some(project_search_settings), - ) - }); - - project_search_view - .update(cx, |view, window, cx| { - // Verify that `test\\c` does not change search options, as - // `SearchOptions::CASE_SENSITIVE` is disabled. - view.query_editor.update(cx, |editor, cx| { - editor.set_text("test\\c", window, cx); - }); - - assert_eq!(view.search_query_text_raw(cx), "test\\c"); - assert_eq!(view.search_query_text(cx), "test"); - - view.apply_pattern_items(&view.search_query_text_raw(cx)); - - assert_eq!( - view.search_options, - SearchOptions::REGEX, - "Case sensitivity should remain disabled if only pattern item is '\\c'" - ); - - // Verify that `test\\c\\C\\c` does not change search options, - // as even though `\\C` should enabled - // `SearchOptions::CASE_SENSITIVE`, the last `\\c` should - // disable it. - view.query_editor.update(cx, |editor, cx| { - editor.set_text("test\\c\\C\\c", window, cx); - }); - - assert_eq!(view.search_query_text_raw(cx), "test\\c\\C\\c"); - assert_eq!(view.search_query_text(cx), "test"); - - view.apply_pattern_items(&view.search_query_text_raw(cx)); - - assert_eq!( - view.search_options, - SearchOptions::REGEX, - "Case sensitivity should be disabled if '\\c' follows '\\C'" - ); - - // Verify that a single `\\C` pattern item in the search query - // enables the `SearchOptions::CASE_SENSITIVE` option. - view.query_editor.update(cx, |editor, cx| { - editor.set_text("test\\C", window, cx); - }); - - assert_eq!(view.search_query_text_raw(cx), "test\\C"); - assert_eq!(view.search_query_text(cx), "test"); - - view.apply_pattern_items(&view.search_query_text_raw(cx)); - - assert_eq!( - view.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Case sensitivity should be enabled when '\\C' is used" - ); - - // Since `SearchOptions::CASE_SENSITIVE` is now enabled, we can - // check that a pattern item preceded by a backslash, for - // example, `\\\\c`, does not affect the search option and is - // not even recognized as a valid pattern item. - // We need to clear the `pattern_items` before testing this, as - // calling `apply_pattern_items` will revert it, so the - // `CASE_SENSITIVE` option would be turned off. - view.pattern_items = PatternItems::default(); - view.query_editor.update(cx, |editor, cx| { - editor.set_text("test\\\\c", window, cx); - }); - - assert_eq!(view.search_query_text_raw(cx), "test\\\\c"); - assert_eq!(view.search_query_text(cx), "test\\\\c"); - - view.apply_pattern_items(&view.search_query_text_raw(cx)); - - assert_eq!( - view.search_options, - SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE, - "Case sensitivity should remain enabled when pattern item is preceded by a backslash" - ); - }) - .unwrap(); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); From 9c35cd6a87248ffd0d8caa59bed46549a95da1b9 Mon Sep 17 00:00:00 2001 From: dinocosta Date: Tue, 26 Aug 2025 19:14:37 +0100 Subject: [PATCH 15/15] chore(diagnostics): remove pattern_items module With the previous commit, both `BufferSearchBar` and `ProjectSearchView` have been updated to no longer rely on `PatternItems`, seeing as `SearchQuery::regex` is now responsible for handling pattern items. This commit removes the `search::pattern_items` module, since there's no longer other modules needing its functionality. --- Cargo.lock | 1 - crates/search/Cargo.toml | 1 - crates/search/src/pattern_items.rs | 254 ----------------------------- crates/search/src/search.rs | 1 - 4 files changed, 257 deletions(-) delete mode 100644 crates/search/src/pattern_items.rs diff --git a/Cargo.lock b/Cargo.lock index 5017c42b6f..42649b137f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14568,7 +14568,6 @@ dependencies = [ "client", "collections", "editor", - "fancy-regex 0.14.0", "futures 0.3.31", "gpui", "language", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 20a69694dd..613f229d4d 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -42,7 +42,6 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true -fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/pattern_items.rs b/crates/search/src/pattern_items.rs deleted file mode 100644 index fa4cbeac06..0000000000 --- a/crates/search/src/pattern_items.rs +++ /dev/null @@ -1,254 +0,0 @@ -use crate::SearchOptions; -use anyhow; -use fancy_regex::Regex; -use std::sync::LazyLock; - -/// A `PatternItem` is a character, preceded by a backslash, that can be used to -/// modify the search options. -/// For example, using `\c` in a search query will make the search -/// case-insensitive, while `\C` will make it case-sensitive. -#[derive(Clone, Debug, PartialEq)] -enum PatternItem { - CaseSensitiveFalse, - CaseSensitiveTrue, -} - -/// Regex for matching pattern items in a search query. -pub static PATTERN_ITEMS_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(&format!( - r"(?() - )) - .expect("Failed to compile pattern items regex") -}); - -impl TryFrom<&str> for PatternItem { - type Error = anyhow::Error; - - fn try_from(str: &str) -> Result { - match str { - "\\c" => Ok(Self::CaseSensitiveFalse), - "\\C" => Ok(Self::CaseSensitiveTrue), - _ => anyhow::bail!("Invalid pattern item: {}", str), - } - } -} - -impl PatternItem { - /// Representation of the pattern item as a single character, without the - /// backslash. - pub fn character(&self) -> char { - match self { - Self::CaseSensitiveFalse => 'c', - Self::CaseSensitiveTrue => 'C', - } - } - - pub fn search_option(&self) -> (SearchOptions, bool) { - match self { - Self::CaseSensitiveFalse => (SearchOptions::CASE_SENSITIVE, false), - Self::CaseSensitiveTrue => (SearchOptions::CASE_SENSITIVE, true), - } - } - - fn all_variants() -> &'static [Self] { - &[Self::CaseSensitiveFalse, Self::CaseSensitiveTrue] - } -} - -#[derive(Default)] -pub struct PatternItems { - items: Vec, -} - -impl PatternItems { - /// Builds the list of pattern items that, from the provided search options - /// and query, do actually affect the search options. - /// For example, if search options is `SearchOptions::CASE_SENSITIVE`, and - /// the query only contains `\C`, then the pattern item will not have an - /// effect on the sarch options. - pub fn from_search_options(search_options: SearchOptions, query: &str) -> Self { - let mut search_options = search_options; - let mut pattern_items: Vec = Vec::new(); - - Self::extract_from_query(query) - .iter() - .for_each(|pattern_item| { - let (search_option, value) = pattern_item.search_option(); - - if search_options.contains(search_option) != value { - search_options.toggle(search_option); - pattern_items.push(pattern_item.clone()); - } - }); - - Self { - items: pattern_items, - } - } - - /// Replaces all pattern items in the provided string with an empty string. - pub fn clean_query(str: &str) -> String { - PATTERN_ITEMS_REGEX.replace_all(str, "").into_owned() - } - - /// Calculates what the provided search options looked liked before the - /// pattern items were applied. - pub fn revert(&self, search_options: SearchOptions) -> SearchOptions { - let mut result = search_options; - - self.items - .iter() - .rev() - .map(PatternItem::search_option) - .for_each(|(search_option, value)| result.set(search_option, !value)); - - result - } - - /// Returns the search options after applying the pattern items. - pub fn apply(&self, search_options: SearchOptions) -> SearchOptions { - let mut search_options = search_options; - - self.items - .iter() - .map(PatternItem::search_option) - .for_each(|(search_option, value)| search_options.set(search_option, value)); - - search_options - } - - /// Extracts all pattern items from the provided string. - fn extract_from_query(str: &str) -> Vec { - PATTERN_ITEMS_REGEX - .captures_iter(str) - .filter_map(|capture| capture.ok()?.get(1)) - .filter_map(|capture| PatternItem::try_from(capture.as_str()).ok()) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_clean_query() { - let query = "Main\\c\\C"; - let cleaned_query = PatternItems::clean_query(query); - assert_eq!(cleaned_query, "Main"); - } - - #[test] - fn test_apply() { - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - assert_eq!(pattern_items.apply(search_options), search_options); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - assert_eq!(pattern_items.apply(search_options), SearchOptions::NONE); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - assert_eq!(pattern_items.apply(search_options), search_options); - - let search_options = SearchOptions::NONE; - let query = "Main\\c\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - assert_eq!( - pattern_items.apply(search_options), - SearchOptions::CASE_SENSITIVE - ); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c\\C\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - assert_eq!(pattern_items.apply(search_options), SearchOptions::NONE); - } - - #[test] - fn test_revert() { - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - let updated_search_options = pattern_items.apply(search_options); - assert_eq!(updated_search_options, SearchOptions::NONE); - assert_eq!(pattern_items.revert(updated_search_options), search_options); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - let updated_search_options = pattern_items.apply(search_options); - assert_eq!(updated_search_options, SearchOptions::NONE); - assert_eq!(pattern_items.revert(updated_search_options), search_options); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - let updated_search_options = pattern_items.apply(search_options); - assert_eq!(updated_search_options, search_options); - assert_eq!(pattern_items.revert(updated_search_options), search_options); - - let search_options = SearchOptions::NONE; - let query = "Main\\c\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - let updated_search_options = pattern_items.apply(search_options); - assert_eq!(updated_search_options, SearchOptions::CASE_SENSITIVE); - assert_eq!(pattern_items.revert(updated_search_options), search_options); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c\\C\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - let updated_search_options = pattern_items.apply(search_options); - assert_eq!(updated_search_options, SearchOptions::NONE); - assert_eq!(pattern_items.revert(updated_search_options), search_options); - } - - #[test] - fn test_extract_from_query() { - let query = "Main\\c\\C\\c"; - let pattern_items = PatternItems::extract_from_query(query); - assert_eq!( - pattern_items, - vec![ - PatternItem::CaseSensitiveFalse, - PatternItem::CaseSensitiveTrue, - PatternItem::CaseSensitiveFalse - ] - ); - } - - #[test] - fn test_from_search_options() { - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - - assert_eq!(pattern_items.items, vec![]); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\c"; - let pattern_items = PatternItems::from_search_options(search_options, query); - - assert_eq!(pattern_items.items, vec![PatternItem::CaseSensitiveFalse]); - - let search_options = SearchOptions::CASE_SENSITIVE; - let query = "Main\\C\\c\\C"; - let pattern_items = PatternItems::from_search_options(search_options, query); - - assert_eq!( - pattern_items.items, - vec![ - PatternItem::CaseSensitiveFalse, - PatternItem::CaseSensitiveTrue - ] - ); - } -} diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 4d216cca00..65e59fd5de 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -14,7 +14,6 @@ pub use search_status_button::SEARCH_ICON; use crate::project_search::ProjectSearchBar; pub mod buffer_search; -pub mod pattern_items; pub mod project_search; pub(crate) mod search_bar; pub mod search_status_button;