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);