editor: Fix selection occurrence highlights during regex buffer search (#52611)

## Context

When using regex buffer search (e.g. `^something`), Zed correctly
navigates only to actual matches. However, navigating to a match selects
the matched text, which triggers the **selection occurrence highlight**
feature. That feature performs a *plain literal* search for the selected
word and highlights all occurrences, including ones that don't satisfy
the regex. The user would see a 4th mid-line occurrence highlighted,
even though `^something` never matched it, this behavior is quite
confusing.

The fix tracks whether the current selection was set by search
navigation via a new `from_search: bool` on `SelectionEffects`. When
`last_selection_from_search` is set and `BufferSearchHighlights` are
active, selection occurrence highlights are suppressed. Making a manual
text selection during an active search clears the flag, restoring normal
occurrence-highlight behavior.

The behavior now matches how VSCode handles this case.

The video below demonstrates the behavior after the fix and shows that
it matches the VSCode behavior now

[Screencast from 2026-03-28
01-33-46.webm](https://github.com/user-attachments/assets/07a005b8-53b1-4abf-93d2-96406f0b6a11)

Closes #52589

## How to Review

Three files changed — read in this order:

1. **`crates/editor/src/editor.rs`** Adds `from_search: bool` to
`SelectionEffects` (with a builder method) and
`last_selection_from_search: bool` to `Editor`.
`selections_did_change()` records the flag from effects.
`prepare_highlight_query_from_selection()` returns `None` early when
both `last_selection_from_search` and active `BufferSearchHighlights`
are set.

2. **`crates/editor/src/items.rs`** `activate_match()` now calls
`.from_search(true)` on its `SelectionEffects` so search-driven
selections are marked at the source.

3. **`crates/search/src/buffer_search.rs`** Regression test
`test_regex_search_does_not_highlight_non_matching_occurrences`:
verifies that after search navigation, `SelectedTextHighlight` is
suppressed and exactly 3 `BufferSearchHighlights` exist; and that after
a manual selection, `SelectedTextHighlight` is restored.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed regex buffer search highlighting non-matching word occurrences
via the selection occurrence highlight feature
This commit is contained in:
saberoueslati authored and GitHub committed 2026-04-13 23:25:28 -07:00
1 parent bf185f7402
commit 1ac669d108
3 files changed
+108 -5

No files matched your search

+23
View File
@@ -1217,6 +1217,7 @@ pub struct Editor {
quick_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
debounced_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
debounced_selection_highlight_complete: bool,
last_selection_from_search: bool,
document_highlights_task: Option<Task<()>>,
linked_editing_range_task: Option<Task<Option<()>>>,
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
@@ -1506,6 +1507,7 @@ pub struct SelectionEffects {
nav_history: Option<bool>,
completions: bool,
scroll: Option<Autoscroll>,
from_search: bool,
}
impl Default for SelectionEffects {
@@ -1514,6 +1516,7 @@ impl Default for SelectionEffects {
nav_history: None,
completions: true,
scroll: Some(Autoscroll::fit()),
from_search: false,
}
}
}
@@ -1545,6 +1548,13 @@ impl SelectionEffects {
..self
}
}
pub fn from_search(self, from_search: bool) -> Self {
Self {
from_search,
..self
}
}
}
struct DeferredSelectionEffectsState {
@@ -2461,6 +2471,7 @@ impl Editor {
quick_selection_highlight_task: None,
debounced_selection_highlight_task: None,
debounced_selection_highlight_complete: false,
last_selection_from_search: false,
document_highlights_task: None,
linked_editing_range_task: None,
pending_rename: None,
@@ -3647,6 +3658,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.last_selection_from_search = effects.from_search;
window.invalidate_character_coordinates();
// Copy selections to primary selection buffer
@@ -7709,6 +7721,17 @@ impl Editor {
if !self.use_selection_highlight || !EditorSettings::get_global(cx).selection_highlight {
return None;
}
// When the current selection was set by search navigation, suppress selection
// occurrence highlights to avoid confusing non-matching occurrences with actual
// search results (e.g. `^something` matches 3 line-start occurrences, but a
// literal highlight would also mark a mid-line "something" that never matched
// the regex). A manual selection made by the user clears this flag, restoring
// the normal occurrence-highlight behavior.
if self.last_selection_from_search
&& self.has_background_highlights(HighlightKey::BufferSearchHighlights)
{
return None;
}
if self.selections.count() != 1 || self.selections.line_mode() {
return None;
}
+8 -3
View File
@@ -1689,9 +1689,14 @@ impl SearchableItem for Editor {
} else {
Autoscroll::fit()
};
self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
s.select_ranges([range]);
})
self.change_selections(
SelectionEffects::scroll(autoscroll).from_search(true),
window,
cx,
|s| {
s.select_ranges([range]);
},
)
}
fn select_matches(
+77 -2
View File
@@ -1900,11 +1900,12 @@ impl BufferSearchBar {
#[cfg(test)]
mod tests {
use std::ops::Range;
use std::{ops::Range, time::Duration};
use super::*;
use editor::{
DisplayPoint, Editor, MultiBuffer, PathKey, SearchSettings, SelectionEffects,
DisplayPoint, Editor, HighlightKey, MultiBuffer, PathKey,
SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT, SearchSettings, SelectionEffects,
display_map::DisplayRow, test::editor_test_context::EditorTestContext,
};
use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
@@ -3788,6 +3789,80 @@ mod tests {
editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
}
#[gpui::test]
async fn test_regex_search_does_not_highlight_non_matching_occurrences(
cx: &mut TestAppContext,
) {
init_globals(cx);
let buffer = cx.new(|cx| {
Buffer::local(
"something is at the top\nsomething is behind something\nsomething is at the bottom\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
});
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
});
search_bar
.update_in(cx, |search_bar, window, cx| {
search_bar.search("^something", None, true, window, cx)
})
.await
.unwrap();
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.select_next_match(&SelectNextMatch, window, cx);
});
// Advance past the debounce so the selection occurrence highlight would
// have fired if it were not suppressed by the active buffer search.
cx.executor()
.advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
cx.run_until_parked();
editor.update(cx, |editor, cx| {
assert!(
!editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
"selection occurrence highlights must be suppressed during buffer search"
);
assert_eq!(
editor.search_background_highlights(cx).len(),
3,
"expected exactly 3 search highlights (one per line start)"
);
});
// Manually select "something" — this should restore occurrence highlights
// because it clears the search-navigation flag.
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, 9)])
});
});
cx.executor()
.advance_clock(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(1));
cx.run_until_parked();
editor.update(cx, |editor, _cx| {
assert!(
editor.has_background_highlights(HighlightKey::SelectedTextHighlight),
"selection occurrence highlights must be restored after a manual selection"
);
});
}
fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {