diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 6a2d5032e413b..0708f25410d96 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -3,14 +3,14 @@ use anyhow::Result; use client::proto; use fancy_regex::{Captures, Regex, RegexBuilder}; use gpui::Model; -use language::{Buffer, BufferSnapshot}; +use language::{Buffer, BufferSnapshot, CharKind}; use smol::future::yield_now; use std::{ borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::Path, - sync::{Arc, OnceLock}, + sync::{Arc, LazyLock, OnceLock}, }; use text::Anchor; use util::paths::PathMatcher; @@ -76,6 +76,12 @@ pub enum SearchQuery { }, } +static WORD_MATCH_TEST: LazyLock = LazyLock::new(|| { + RegexBuilder::new(r"\B") + .build() + .expect("Failed to create WORD_MATCH_TEST") +}); + impl SearchQuery { pub fn text( query: impl ToString, @@ -119,9 +125,17 @@ impl SearchQuery { let initial_query = Arc::from(query.as_str()); if whole_word { let mut word_query = String::new(); - word_query.push_str("\\b"); + if let Some(first) = query.get(0..1) { + if WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) { + word_query.push_str("\\b"); + } + } word_query.push_str(&query); - word_query.push_str("\\b"); + if let Some(last) = query.get(query.len() - 1..) { + if WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) { + word_query.push_str("\\b"); + } + } query = word_query } @@ -313,7 +327,9 @@ impl SearchQuery { let end_kind = classifier.kind(rope.reversed_chars_at(mat.end()).next().unwrap()); let next_kind = rope.chars_at(mat.end()).next().map(|c| classifier.kind(c)); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + if (Some(start_kind) == prev_kind && start_kind == CharKind::Word) + || (Some(end_kind) == next_kind && end_kind == CharKind::Word) + { continue; } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 41e5ba28df7ad..b8603b8649942 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1866,6 +1866,86 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) { + init_globals(cx); + let buffer_text = r#" + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) + }); + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + "# + .unindent(); + let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx)); + let cx = cx.add_empty_window(); + + let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.new_view(|cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search( + "edit\\(", + Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), + cx, + ) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + search_bar.update(cx, |_, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 2, + "Should select all `edit(` in the buffer, but got: {all_selections:?}" + ); + }); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search( + "edit(", + Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE), + cx, + ) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + search_bar.update(cx, |_, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 2, + "Should select all `edit(` in the buffer, but got: {all_selections:?}" + ); + }); + } + #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { init_globals(cx);