From 3d7c8e732e9dcf893642b7364cf705355fbe8735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20G=C3=B3mez?= Date: Fri, 24 Jul 2026 17:45:41 +0200 Subject: [PATCH] search: Escape seeded buffer search query in regex mode (#57748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Co-authored-by: Smit Barmase --- crates/search/src/buffer_search.rs | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index f1f81a4052..a379847295 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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| {