search: Escape seeded buffer search query in regex mode (#57748)

## What

When `seed_search_query_from_cursor` is enabled and regex mode is
active, the selected text is used verbatim as the regex pattern. This
means selecting text like `z.d` seeds the regex `z.d`, where `.` acts as
a wildcard — matching `zed`, `zXd`, etc. — rather than only the literal
string the user selected.

Fixes #57253.

## Why it was broken

`search_suggested()` in `BufferSearchBar` retrieves the raw selection
text from `query_suggestion()` and passes it directly to `search()`.
There was no escaping step, even when `default_options` had
`SearchOptions::REGEX` set.

## Fix

Run the seeded suggestion through `regex::escape()` before passing it to
`search()` when regex mode is enabled. This mirrors the identical
pattern already used in `crates/vim/src/normal/search.rs:485`.

```rust
// Before
let search = self
    .query_suggestion(seed_query_override, window, cx)
    .map(|suggestion| {
        self.search(&suggestion, Some(self.default_options), true, window, cx)
    });

// After
let search = self
    .query_suggestion(seed_query_override, window, cx)
    .map(|suggestion| {
        let suggestion = if self.default_options.contains(SearchOptions::REGEX) {
            regex::escape(&suggestion)
        } else {
            suggestion
        };
        self.search(&suggestion, Some(self.default_options), true, window, cx)
    });
```

## Test

Added `test_seeded_query_is_escaped_in_regex_mode` in
`buffer_search.rs`. It creates a buffer with `"z.d\nzed\n"`, enables
regex mode, selects `z.d`, seeds the search, and asserts:
- The query text becomes `z\.d` (escaped), not `z.d`
- Exactly 1 match is highlighted (the literal `z.d`), not 2

The `regex` crate was already a workspace dependency and is already used
in the `search` crate transitively; this adds it explicitly to
`crates/search/Cargo.toml`.

Release Notes:

- Fixed regex search seeded from cursor/selection matching more than the
selected text when the selection contained regex special characters
(e.g. `.`, `*`, `(`)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-07-24 15:45:41 +00:00
1 parent ca267a68a3
commit 3d7c8e732e
1 file changed
+49
+49
View File
@@ -1043,6 +1043,11 @@ impl BufferSearchBar {
let search = self
.query_suggestion(seed_query_override, window, cx)
.map(|suggestion| {
let suggestion = if self.default_options.contains(SearchOptions::REGEX) {
regex::escape(&suggestion)
} else {
suggestion
};
self.search(&suggestion, Some(self.default_options), true, window, cx)
});
@@ -4167,6 +4172,50 @@ mod tests {
);
}
#[gpui::test]
async fn test_seeded_query_is_escaped_in_regex_mode(cx: &mut TestAppContext) {
init_globals(cx);
let buffer = cx.new(|cx| Buffer::local("z.d\nzed\n", 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 search_bar = BufferSearchBar::new(None, window, cx);
search_bar.set_active_pane_item(Some(&editor), window, cx);
search_bar.show(window, cx);
search_bar
});
// Select "z.d" on the first line.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(0, 0)..Point::new(0, 3)])
});
});
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
search_bar.search_suggested(Some(SeedQuerySetting::Selection), window, cx);
});
cx.run_until_parked();
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(
search_bar.query(cx),
r"z\.d",
"seeded regex query must be escaped"
);
});
editor.update(cx, |editor, cx| {
assert_eq!(
editor.search_background_highlights(cx).len(),
1,
"only the literal 'z.d' should match, not 'zed'"
);
});
}
fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {