@@ -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;
}
@@ -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(
@@ -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| {