search: Treat non-word char as whole-char when searching (#19152)

when search somethings like `clone(`, with search options `match case
sensitively` and `match whole words` in zed code base, only `clone(cx)`
hit match, `clone()` will not hit math.

Release Notes:

- Improved buffer search for queries ending with non-letter characters
This commit is contained in:
CharlesChen0823 2024-11-28 17:06:48 +08:00 committed by GitHub
parent 3ac119ac4e
commit cacec06db6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 5 deletions

View file

@ -3,14 +3,14 @@ use anyhow::Result;
use client::proto; use client::proto;
use fancy_regex::{Captures, Regex, RegexBuilder}; use fancy_regex::{Captures, Regex, RegexBuilder};
use gpui::Model; use gpui::Model;
use language::{Buffer, BufferSnapshot}; use language::{Buffer, BufferSnapshot, CharKind};
use smol::future::yield_now; use smol::future::yield_now;
use std::{ use std::{
borrow::Cow, borrow::Cow,
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
ops::Range, ops::Range,
path::Path, path::Path,
sync::{Arc, OnceLock}, sync::{Arc, LazyLock, OnceLock},
}; };
use text::Anchor; use text::Anchor;
use util::paths::PathMatcher; use util::paths::PathMatcher;
@ -76,6 +76,12 @@ pub enum SearchQuery {
}, },
} }
static WORD_MATCH_TEST: LazyLock<Regex> = LazyLock::new(|| {
RegexBuilder::new(r"\B")
.build()
.expect("Failed to create WORD_MATCH_TEST")
});
impl SearchQuery { impl SearchQuery {
pub fn text( pub fn text(
query: impl ToString, query: impl ToString,
@ -119,9 +125,17 @@ impl SearchQuery {
let initial_query = Arc::from(query.as_str()); let initial_query = Arc::from(query.as_str());
if whole_word { if whole_word {
let mut word_query = String::new(); 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(&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 query = word_query
} }
@ -313,7 +327,9 @@ impl SearchQuery {
let end_kind = let end_kind =
classifier.kind(rope.reversed_chars_at(mat.end()).next().unwrap()); classifier.kind(rope.reversed_chars_at(mat.end()).next().unwrap());
let next_kind = rope.chars_at(mat.end()).next().map(|c| classifier.kind(c)); 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; continue;
} }
} }

View file

@ -1866,6 +1866,86 @@ mod tests {
.unwrap(); .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] #[gpui::test]
async fn test_search_query_history(cx: &mut TestAppContext) { async fn test_search_query_history(cx: &mut TestAppContext) {
init_globals(cx); init_globals(cx);