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 <conrad.irwin@gmail.com>
This commit is contained in:
dinocosta 2025-08-26 17:12:29 +01:00
parent d28e52d9e7
commit c69f4f49f0
3 changed files with 142 additions and 330 deletions

View file

@ -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<Editor>,
results_editor: Entity<Editor>,
search_options: SearchOptions,
pattern_items: PatternItems,
panels_with_errors: HashMap<InputPanel, String>,
active_match_index: Option<usize>,
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<Self>) -> Option<SearchQuery> {
@ -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);