editor: Add active match highlight for buffer and project search (#44098)

Smit Barmase and DarkMatter-999 created

Closes #28617

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/b1c2880c-5744-4bed-a687-5c5e7aa7fef5"
/>

Release Notes:

- Improved visibility of the currently active match when browsing
results in buffer or project search.

---------

Co-authored-by: DarkMatter-999 <darkmatter999official@gmail.com>

Change summary

assets/themes/ayu/ayu.json                        |   3 
assets/themes/gruvbox/gruvbox.json                |   6 
assets/themes/one/one.json                        |   2 
crates/agent_ui/src/text_thread_editor.rs         |   6 
crates/debugger_tools/src/dap_log.rs              |   6 
crates/debugger_ui/src/session/running/console.rs |   3 
crates/editor/src/editor.rs                       |  29 
crates/editor/src/editor_tests.rs                 |  10 
crates/editor/src/hover_popover.rs                |   2 
crates/editor/src/items.rs                        |   9 
crates/language_tools/src/lsp_log_view.rs         |   6 
crates/language_tools/src/syntax_tree_view.rs     |   2 
crates/search/src/buffer_search.rs                |  25 +
crates/search/src/project_search.rs               | 326 +++++++++++-----
crates/settings/src/settings_content/theme.rs     |   3 
crates/terminal_view/src/terminal_view.rs         |   1 
crates/theme/src/default_colors.rs                |   2 
crates/theme/src/fallback_themes.rs               |   1 
crates/theme/src/schema.rs                        |  15 
crates/theme/src/styles/colors.rs                 |   3 
crates/vim/src/normal/yank.rs                     |   2 
crates/vim/src/replace.rs                         |   2 
crates/workspace/src/searchable.rs                |  19 
23 files changed, 338 insertions(+), 145 deletions(-)

Detailed changes

assets/themes/ayu/ayu.json 🔗

@@ -45,6 +45,7 @@
         "tab.inactive_background": "#1f2127ff",
         "tab.active_background": "#0d1016ff",
         "search.match_background": "#5ac2fe66",
+        "search.active_match_background": "#ea570166",
         "panel.background": "#1f2127ff",
         "panel.focused_border": "#5ac1feff",
         "pane.focused_border": null,
@@ -436,6 +437,7 @@
         "tab.inactive_background": "#ececedff",
         "tab.active_background": "#fcfcfcff",
         "search.match_background": "#3b9ee566",
+        "search.active_match_background": "#f88b3666",
         "panel.background": "#ececedff",
         "panel.focused_border": "#3b9ee5ff",
         "pane.focused_border": null,
@@ -827,6 +829,7 @@
         "tab.inactive_background": "#353944ff",
         "tab.active_background": "#242835ff",
         "search.match_background": "#73cffe66",
+        "search.active_match_background": "#fd722b66",
         "panel.background": "#353944ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/gruvbox/gruvbox.json 🔗

@@ -46,6 +46,7 @@
         "tab.inactive_background": "#3a3735ff",
         "tab.active_background": "#282828ff",
         "search.match_background": "#83a59866",
+        "search.active_match_background": "#c09f3f66",
         "panel.background": "#3a3735ff",
         "panel.focused_border": "#83a598ff",
         "pane.focused_border": null,
@@ -452,6 +453,7 @@
         "tab.inactive_background": "#393634ff",
         "tab.active_background": "#1d2021ff",
         "search.match_background": "#83a59866",
+        "search.active_match_background": "#c9653666",
         "panel.background": "#393634ff",
         "panel.focused_border": "#83a598ff",
         "pane.focused_border": null,
@@ -858,6 +860,7 @@
         "tab.inactive_background": "#3b3735ff",
         "tab.active_background": "#32302fff",
         "search.match_background": "#83a59866",
+        "search.active_match_background": "#aea85166",
         "panel.background": "#3b3735ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1264,6 +1267,7 @@
         "tab.inactive_background": "#ecddb4ff",
         "tab.active_background": "#fbf1c7ff",
         "search.match_background": "#0b667866",
+        "search.active_match_background": "#ba2d1166",
         "panel.background": "#ecddb4ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1670,6 +1674,7 @@
         "tab.inactive_background": "#ecddb5ff",
         "tab.active_background": "#f9f5d7ff",
         "search.match_background": "#0b667866",
+        "search.active_match_background": "#dc351466",
         "panel.background": "#ecddb5ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -2076,6 +2081,7 @@
         "tab.inactive_background": "#ecdcb3ff",
         "tab.active_background": "#f2e5bcff",
         "search.match_background": "#0b667866",
+        "search.active_match_background": "#d7331466",
         "panel.background": "#ecdcb3ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/one/one.json 🔗

@@ -45,6 +45,7 @@
         "tab.inactive_background": "#2f343eff",
         "tab.active_background": "#282c33ff",
         "search.match_background": "#74ade866",
+        "search.active_match_background": "#e8af7466",
         "panel.background": "#2f343eff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -448,6 +449,7 @@
         "tab.inactive_background": "#ebebecff",
         "tab.active_background": "#fafafaff",
         "search.match_background": "#5c79e266",
+        "search.active_match_background": "#d0a92366",
         "panel.background": "#ebebecff",
         "panel.focused_border": null,
         "pane.focused_border": null,

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -2622,11 +2622,13 @@ impl SearchableItem for TextThreadEditor {
     fn update_matches(
         &mut self,
         matches: &[Self::Match],
+        active_match_index: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.editor
-            .update(cx, |editor, cx| editor.update_matches(matches, window, cx));
+        self.editor.update(cx, |editor, cx| {
+            editor.update_matches(matches, active_match_index, window, cx)
+        });
     }
 
     fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

crates/debugger_tools/src/dap_log.rs 🔗

@@ -1017,11 +1017,13 @@ impl SearchableItem for DapLogView {
     fn update_matches(
         &mut self,
         matches: &[Self::Match],
+        active_match_index: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.editor
-            .update(cx, |e, cx| e.update_matches(matches, window, cx))
+        self.editor.update(cx, |e, cx| {
+            e.update_matches(matches, active_match_index, window, cx)
+        })
     }
 
     fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -252,10 +252,11 @@ impl Console {
                         let start_offset = range.start;
                         let range = buffer.anchor_after(MultiBufferOffset(range.start))
                             ..buffer.anchor_before(MultiBufferOffset(range.end));
+                        let color_fn = color_fetcher(color);
                         console.highlight_background_key::<ConsoleAnsiHighlight>(
                             start_offset,
                             &[range],
-                            color_fetcher(color),
+                            move |_, theme| color_fn(theme),
                             cx,
                         );
                     }

crates/editor/src/editor.rs 🔗

@@ -726,7 +726,10 @@ impl EditorActionId {
 // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
-type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
+type BackgroundHighlight = (
+    Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
+    Arc<[Range<Anchor>]>,
+);
 type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
 
 #[derive(Default)]
@@ -6610,7 +6613,7 @@ impl Editor {
             editor.update(cx, |editor, cx| {
                 editor.highlight_background::<Self>(
                     &ranges_to_highlight,
-                    |theme| theme.colors().editor_highlighted_line_background,
+                    |_, theme| theme.colors().editor_highlighted_line_background,
                     cx,
                 );
             });
@@ -7012,12 +7015,12 @@ impl Editor {
 
                     this.highlight_background::<DocumentHighlightRead>(
                         &read_ranges,
-                        |theme| theme.colors().editor_document_highlight_read_background,
+                        |_, theme| theme.colors().editor_document_highlight_read_background,
                         cx,
                     );
                     this.highlight_background::<DocumentHighlightWrite>(
                         &write_ranges,
-                        |theme| theme.colors().editor_document_highlight_write_background,
+                        |_, theme| theme.colors().editor_document_highlight_write_background,
                         cx,
                     );
                     cx.notify();
@@ -7125,7 +7128,7 @@ impl Editor {
                     if !match_ranges.is_empty() {
                         editor.highlight_background::<SelectedTextHighlight>(
                             &match_ranges,
-                            |theme| theme.colors().editor_document_highlight_bracket_background,
+                            |_, theme| theme.colors().editor_document_highlight_bracket_background,
                             cx,
                         )
                     }
@@ -17519,7 +17522,7 @@ impl Editor {
                 }
                 editor.highlight_background::<Self>(
                     &ranges,
-                    |theme| theme.colors().editor_highlighted_line_background,
+                    |_, theme| theme.colors().editor_highlighted_line_background,
                     cx,
                 );
             }
@@ -20989,7 +20992,7 @@ impl Editor {
     pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
         self.highlight_background::<SearchWithinRange>(
             ranges,
-            |colors| colors.colors().editor_document_highlight_read_background,
+            |_, colors| colors.colors().editor_document_highlight_read_background,
             cx,
         )
     }
@@ -21005,12 +21008,12 @@ impl Editor {
     pub fn highlight_background<T: 'static>(
         &mut self,
         ranges: &[Range<Anchor>],
-        color_fetcher: fn(&Theme) -> Hsla,
+        color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
         cx: &mut Context<Self>,
     ) {
         self.background_highlights.insert(
             HighlightKey::Type(TypeId::of::<T>()),
-            (color_fetcher, Arc::from(ranges)),
+            (Arc::new(color_fetcher), Arc::from(ranges)),
         );
         self.scrollbar_marker_state.dirty = true;
         cx.notify();
@@ -21020,12 +21023,12 @@ impl Editor {
         &mut self,
         key: usize,
         ranges: &[Range<Anchor>],
-        color_fetcher: fn(&Theme) -> Hsla,
+        color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
         cx: &mut Context<Self>,
     ) {
         self.background_highlights.insert(
             HighlightKey::TypePlus(TypeId::of::<T>(), key),
-            (color_fetcher, Arc::from(ranges)),
+            (Arc::new(color_fetcher), Arc::from(ranges)),
         );
         self.scrollbar_marker_state.dirty = true;
         cx.notify();
@@ -21250,7 +21253,6 @@ impl Editor {
     ) -> Vec<(Range<DisplayPoint>, Hsla)> {
         let mut results = Vec::new();
         for (color_fetcher, ranges) in self.background_highlights.values() {
-            let color = color_fetcher(theme);
             let start_ix = match ranges.binary_search_by(|probe| {
                 let cmp = probe
                     .end
@@ -21263,7 +21265,7 @@ impl Editor {
             }) {
                 Ok(i) | Err(i) => i,
             };
-            for range in &ranges[start_ix..] {
+            for (index, range) in ranges[start_ix..].iter().enumerate() {
                 if range
                     .start
                     .cmp(&search_range.end, &display_snapshot.buffer_snapshot())
@@ -21272,6 +21274,7 @@ impl Editor {
                     break;
                 }
 
+                let color = color_fetcher(&(start_ix + index), theme);
                 let start = range.start.to_display_point(display_snapshot);
                 let end = range.end.to_display_point(display_snapshot);
                 results.push((start..end, color))

crates/editor/src/editor_tests.rs 🔗

@@ -16978,7 +16978,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
                 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
                 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
             ],
-            |_| Hsla::red(),
+            |_, _| Hsla::red(),
             cx,
         );
         editor.highlight_background::<Type2>(
@@ -16988,7 +16988,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
                 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
                 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
             ],
-            |_| Hsla::green(),
+            |_, _| Hsla::green(),
             cx,
         );
 
@@ -23973,7 +23973,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
         let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
         editor.highlight_background::<DocumentHighlightRead>(
             &[highlight_range],
-            |theme| theme.colors().editor_document_highlight_read_background,
+            |_, theme| theme.colors().editor_document_highlight_read_background,
             cx,
         );
     });
@@ -24051,7 +24051,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
         let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
         editor.highlight_background::<DocumentHighlightRead>(
             &[highlight_range],
-            |theme| theme.colors().editor_document_highlight_read_background,
+            |_, theme| theme.colors().editor_document_highlight_read_background,
             cx,
         );
     });
@@ -27299,7 +27299,7 @@ let result = variable * 2;",
 
         editor.highlight_background::<DocumentHighlightRead>(
             &anchor_ranges,
-            |theme| theme.colors().editor_document_highlight_read_background,
+            |_, theme| theme.colors().editor_document_highlight_read_background,
             cx,
         );
     });

crates/editor/src/hover_popover.rs 🔗

@@ -518,7 +518,7 @@ fn show_hover(
                     // Highlight the selected symbol using a background highlight
                     editor.highlight_background::<HoverState>(
                         &hover_highlights,
-                        |theme| theme.colors().element_hover, // todo update theme
+                        |_, theme| theme.colors().element_hover, // todo update theme
                         cx,
                     );
                 }

crates/editor/src/items.rs 🔗

@@ -1487,6 +1487,7 @@ impl SearchableItem for Editor {
     fn update_matches(
         &mut self,
         matches: &[Range<Anchor>],
+        active_match_index: Option<usize>,
         _: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -1497,7 +1498,13 @@ impl SearchableItem for Editor {
         let updated = existing_range != Some(matches);
         self.highlight_background::<BufferSearchHighlights>(
             matches,
-            |theme| theme.colors().search_match_background,
+            move |index, theme| {
+                if active_match_index == Some(*index) {
+                    theme.colors().search_active_match_background
+                } else {
+                    theme.colors().search_match_background
+                }
+            },
             cx,
         );
         if updated {

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -805,11 +805,13 @@ impl SearchableItem for LspLogView {
     fn update_matches(
         &mut self,
         matches: &[Self::Match],
+        active_match_index: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.editor
-            .update(cx, |e, cx| e.update_matches(matches, window, cx))
+        self.editor.update(cx, |e, cx| {
+            e.update_matches(matches, active_match_index, window, cx)
+        })
     }
 
     fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -459,7 +459,7 @@ impl SyntaxTreeView {
                                         editor.clear_background_highlights::<Self>(cx);
                                         editor.highlight_background::<Self>(
                                             &[range],
-                                            |theme| {
+                                            |_, theme| {
                                                 theme
                                                     .colors()
                                                     .editor_document_highlight_write_background

crates/search/src/buffer_search.rs 🔗

@@ -1031,7 +1031,7 @@ impl BufferSearchBar {
             let new_match_index = searchable_item
                 .match_index_for_direction(matches, index, direction, count, window, cx);
 
-            searchable_item.update_matches(matches, window, cx);
+            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
             searchable_item.activate_match(new_match_index, matches, window, cx);
         }
     }
@@ -1045,7 +1045,7 @@ impl BufferSearchBar {
             if matches.is_empty() {
                 return;
             }
-            searchable_item.update_matches(matches, window, cx);
+            searchable_item.update_matches(matches, Some(0), window, cx);
             searchable_item.activate_match(0, matches, window, cx);
         }
     }
@@ -1060,7 +1060,7 @@ impl BufferSearchBar {
                 return;
             }
             let new_match_index = matches.len() - 1;
-            searchable_item.update_matches(matches, window, cx);
+            searchable_item.update_matches(matches, Some(new_match_index), window, cx);
             searchable_item.activate_match(new_match_index, matches, window, cx);
         }
     }
@@ -1300,7 +1300,12 @@ impl BufferSearchBar {
                                 if matches.is_empty() {
                                     active_searchable_item.clear_matches(window, cx);
                                 } else {
-                                    active_searchable_item.update_matches(matches, window, cx);
+                                    active_searchable_item.update_matches(
+                                        matches,
+                                        this.active_match_index,
+                                        window,
+                                        cx,
+                                    );
                                 }
                                 let _ = done_tx.send(());
                             }
@@ -1335,6 +1340,18 @@ impl BufferSearchBar {
             });
         if new_index != self.active_match_index {
             self.active_match_index = new_index;
+            if !self.dismissed {
+                if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                    if let Some(matches) = self
+                        .searchable_items_with_matches
+                        .get(&searchable_item.downgrade())
+                    {
+                        if !matches.is_empty() {
+                            searchable_item.update_matches(matches, new_index, window, cx);
+                        }
+                    }
+                }
+            }
             cx.notify();
         }
     }

crates/search/src/project_search.rs 🔗

@@ -1444,6 +1444,7 @@ impl ProjectSearchView {
                     s.select_ranges([range_to_select])
                 });
             });
+            self.highlight_matches(&match_ranges, Some(new_index), cx);
         }
     }
 
@@ -1518,11 +1519,6 @@ impl ProjectSearchView {
                     });
                     editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
                 }
-                editor.highlight_background::<Self>(
-                    &match_ranges,
-                    |theme| theme.colors().search_match_background,
-                    cx,
-                );
             });
             if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) {
                 self.focus_results_editor(window, cx);
@@ -1535,18 +1531,41 @@ impl ProjectSearchView {
 
     fn update_match_index(&mut self, cx: &mut Context<Self>) {
         let results_editor = self.results_editor.read(cx);
+        let match_ranges = self.entity.read(cx).match_ranges.clone();
         let new_index = active_match_index(
             Direction::Next,
-            &self.entity.read(cx).match_ranges,
+            &match_ranges,
             &results_editor.selections.newest_anchor().head(),
             &results_editor.buffer().read(cx).snapshot(cx),
         );
+        self.highlight_matches(&match_ranges, new_index, cx);
         if self.active_match_index != new_index {
             self.active_match_index = new_index;
             cx.notify();
         }
     }
 
+    fn highlight_matches(
+        &self,
+        match_ranges: &[Range<Anchor>],
+        active_index: Option<usize>,
+        cx: &mut Context<Self>,
+    ) {
+        self.results_editor.update(cx, |editor, cx| {
+            editor.highlight_background::<Self>(
+                match_ranges,
+                move |index, theme| {
+                    if active_index == Some(*index) {
+                        theme.colors().search_active_match_background
+                    } else {
+                        theme.colors().search_match_background
+                    }
+                },
+                cx,
+            );
+        });
+    }
+
     pub fn has_matches(&self) -> bool {
         self.active_match_index.is_some()
     }
@@ -2456,7 +2475,9 @@ pub mod tests {
     use pretty_assertions::assert_eq;
     use project::FakeFs;
     use serde_json::json;
-    use settings::{InlayHintSettingsContent, SettingsStore};
+    use settings::{
+        InlayHintSettingsContent, SettingsStore, ThemeColorsContent, ThemeStyleContent,
+    };
     use util::{path, paths::PathStyle, rel_path::rel_path};
     use util_macros::perf;
     use workspace::DeploySearch;
@@ -2464,8 +2485,105 @@ pub mod tests {
     #[perf]
     #[gpui::test]
     async fn test_project_search(cx: &mut TestAppContext) {
+        fn dp(row: u32, col: u32) -> DisplayPoint {
+            DisplayPoint::new(DisplayRow(row), col)
+        }
+
+        fn assert_active_match_index(
+            search_view: &WindowHandle<ProjectSearchView>,
+            cx: &mut TestAppContext,
+            expected_index: usize,
+        ) {
+            search_view
+                .update(cx, |search_view, _window, _cx| {
+                    assert_eq!(search_view.active_match_index, Some(expected_index));
+                })
+                .unwrap();
+        }
+
+        fn assert_selection_range(
+            search_view: &WindowHandle<ProjectSearchView>,
+            cx: &mut TestAppContext,
+            expected_range: Range<DisplayPoint>,
+        ) {
+            search_view
+                .update(cx, |search_view, _window, cx| {
+                    assert_eq!(
+                        search_view.results_editor.update(cx, |editor, cx| editor
+                            .selections
+                            .display_ranges(&editor.display_snapshot(cx))),
+                        [expected_range]
+                    );
+                })
+                .unwrap();
+        }
+
+        fn assert_highlights(
+            search_view: &WindowHandle<ProjectSearchView>,
+            cx: &mut TestAppContext,
+            expected_highlights: Vec<(Range<DisplayPoint>, &str)>,
+        ) {
+            search_view
+                .update(cx, |search_view, window, cx| {
+                    let match_bg = cx.theme().colors().search_match_background;
+                    let active_match_bg = cx.theme().colors().search_active_match_background;
+                    let selection_bg = cx
+                        .theme()
+                        .colors()
+                        .editor_document_highlight_bracket_background;
+
+                    let highlights: Vec<_> = expected_highlights
+                        .into_iter()
+                        .map(|(range, color_type)| {
+                            let color = match color_type {
+                                "active" => active_match_bg,
+                                "match" => match_bg,
+                                "selection" => selection_bg,
+                                _ => panic!("Unknown color type"),
+                            };
+                            (range, color)
+                        })
+                        .collect();
+
+                    assert_eq!(
+                        search_view.results_editor.update(cx, |editor, cx| editor
+                            .all_text_background_highlights(window, cx)),
+                        highlights.as_slice()
+                    );
+                })
+                .unwrap();
+        }
+
+        fn select_match(
+            search_view: &WindowHandle<ProjectSearchView>,
+            cx: &mut TestAppContext,
+            direction: Direction,
+        ) {
+            search_view
+                .update(cx, |search_view, window, cx| {
+                    search_view.select_match(direction, window, cx);
+                })
+                .unwrap();
+        }
+
         init_test(cx);
 
+        // Override active search match color since the fallback theme uses the same color
+        // for normal search match and active one, which can make this test less robust.
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |settings, cx| {
+                settings.update_user_settings(cx, |settings| {
+                    settings.theme.experimental_theme_overrides = Some(ThemeStyleContent {
+                        colors: ThemeColorsContent {
+                            search_active_match_background: Some("#ff0000ff".to_string()),
+                            ..Default::default()
+                        },
+                        ..Default::default()
+                    });
+                });
+            });
+        });
+
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
             path!("/dir"),
@@ -2486,113 +2604,113 @@ pub mod tests {
         });
 
         perform_search(search_view, "TWO", cx);
-        search_view.update(cx, |search_view, window, cx| {
-            assert_eq!(
-                search_view
-                    .results_editor
-                    .update(cx, |editor, cx| editor.display_text(cx)),
-                "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
-            );
-            let match_background_color = cx.theme().colors().search_match_background;
-            let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background;
-            assert_eq!(
-                search_view
-                    .results_editor
-                    .update(cx, |editor, cx| editor.all_text_background_highlights(window, cx)),
-                &[
-                    (
-                        DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35),
-                        match_background_color
-                    ),
-                    (
-                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
-                        selection_background_color
-                    ),
-                    (
-                        DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
-                        match_background_color
-                    ),
-                    (
-                        DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
-                        match_background_color
-                    ),
-
-                ]
-            );
-            assert_eq!(search_view.active_match_index, Some(0));
-            assert_eq!(
-                search_view
-                    .results_editor
-                    .update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))),
-                [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
-            );
-
-            search_view.select_match(Direction::Next, window, cx);
-        }).unwrap();
+        cx.run_until_parked();
 
         search_view
-            .update(cx, |search_view, window, cx| {
-                assert_eq!(search_view.active_match_index, Some(1));
+            .update(cx, |search_view, _window, cx| {
                 assert_eq!(
-                    search_view.results_editor.update(cx, |editor, cx| editor
-                        .selections
-                        .display_ranges(&editor.display_snapshot(cx))),
-                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
+                    search_view
+                        .results_editor
+                        .update(cx, |editor, cx| editor.display_text(cx)),
+                    "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
                 );
-                search_view.select_match(Direction::Next, window, cx);
             })
             .unwrap();
 
-        search_view
-            .update(cx, |search_view, window, cx| {
-                assert_eq!(search_view.active_match_index, Some(2));
-                assert_eq!(
-                    search_view.results_editor.update(cx, |editor, cx| editor
-                        .selections
-                        .display_ranges(&editor.display_snapshot(cx))),
-                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
-                );
-                search_view.select_match(Direction::Next, window, cx);
-            })
-            .unwrap();
+        assert_active_match_index(&search_view, cx, 0);
+        assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "active"),
+                (dp(2, 37)..dp(2, 40), "selection"),
+                (dp(2, 37)..dp(2, 40), "match"),
+                (dp(5, 6)..dp(5, 9), "match"),
+                // TODO: we should be getting selection highlight here after project search
+                // but for some reason we are not getting it here
+            ],
+        );
+        select_match(&search_view, cx, Direction::Next);
+        cx.run_until_parked();
 
-        search_view
-            .update(cx, |search_view, window, cx| {
-                assert_eq!(search_view.active_match_index, Some(0));
-                assert_eq!(
-                    search_view.results_editor.update(cx, |editor, cx| editor
-                        .selections
-                        .display_ranges(&editor.display_snapshot(cx))),
-                    [DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
-                );
-                search_view.select_match(Direction::Prev, window, cx);
-            })
-            .unwrap();
+        assert_active_match_index(&search_view, cx, 1);
+        assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "selection"),
+                (dp(2, 32)..dp(2, 35), "match"),
+                (dp(2, 37)..dp(2, 40), "active"),
+                (dp(5, 6)..dp(5, 9), "selection"),
+                (dp(5, 6)..dp(5, 9), "match"),
+            ],
+        );
+        select_match(&search_view, cx, Direction::Next);
+        cx.run_until_parked();
 
-        search_view
-            .update(cx, |search_view, window, cx| {
-                assert_eq!(search_view.active_match_index, Some(2));
-                assert_eq!(
-                    search_view.results_editor.update(cx, |editor, cx| editor
-                        .selections
-                        .display_ranges(&editor.display_snapshot(cx))),
-                    [DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
-                );
-                search_view.select_match(Direction::Prev, window, cx);
-            })
-            .unwrap();
+        assert_active_match_index(&search_view, cx, 2);
+        assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "selection"),
+                (dp(2, 32)..dp(2, 35), "match"),
+                (dp(2, 37)..dp(2, 40), "selection"),
+                (dp(2, 37)..dp(2, 40), "match"),
+                (dp(5, 6)..dp(5, 9), "active"),
+            ],
+        );
+        select_match(&search_view, cx, Direction::Next);
+        cx.run_until_parked();
 
-        search_view
-            .update(cx, |search_view, _, cx| {
-                assert_eq!(search_view.active_match_index, Some(1));
-                assert_eq!(
-                    search_view.results_editor.update(cx, |editor, cx| editor
-                        .selections
-                        .display_ranges(&editor.display_snapshot(cx))),
-                    [DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
-                );
-            })
-            .unwrap();
+        assert_active_match_index(&search_view, cx, 0);
+        assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "active"),
+                (dp(2, 37)..dp(2, 40), "selection"),
+                (dp(2, 37)..dp(2, 40), "match"),
+                (dp(5, 6)..dp(5, 9), "selection"),
+                (dp(5, 6)..dp(5, 9), "match"),
+            ],
+        );
+        select_match(&search_view, cx, Direction::Prev);
+        cx.run_until_parked();
+
+        assert_active_match_index(&search_view, cx, 2);
+        assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "selection"),
+                (dp(2, 32)..dp(2, 35), "match"),
+                (dp(2, 37)..dp(2, 40), "selection"),
+                (dp(2, 37)..dp(2, 40), "match"),
+                (dp(5, 6)..dp(5, 9), "active"),
+            ],
+        );
+        select_match(&search_view, cx, Direction::Prev);
+        cx.run_until_parked();
+
+        assert_active_match_index(&search_view, cx, 1);
+        assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40));
+        assert_highlights(
+            &search_view,
+            cx,
+            vec![
+                (dp(2, 32)..dp(2, 35), "selection"),
+                (dp(2, 32)..dp(2, 35), "match"),
+                (dp(2, 37)..dp(2, 40), "active"),
+                (dp(5, 6)..dp(5, 9), "selection"),
+                (dp(5, 6)..dp(5, 9), "match"),
+            ],
+        );
     }
 
     #[perf]

crates/settings/src/settings_content/theme.rs 🔗

@@ -570,6 +570,9 @@ pub struct ThemeColorsContent {
     #[serde(rename = "search.match_background")]
     pub search_match_background: Option<String>,
 
+    #[serde(rename = "search.active_match_background")]
+    pub search_active_match_background: Option<String>,
+
     #[serde(rename = "panel.background")]
     pub panel_background: Option<String>,
 

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1434,6 +1434,7 @@ impl SearchableItem for TerminalView {
     fn update_matches(
         &mut self,
         matches: &[Self::Match],
+        _active_match_index: Option<usize>,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {

crates/theme/src/default_colors.rs 🔗

@@ -91,6 +91,7 @@ impl ThemeColors {
             tab_inactive_background: neutral().light().step_2(),
             tab_active_background: neutral().light().step_1(),
             search_match_background: neutral().light().step_5(),
+            search_active_match_background: neutral().light().step_7(),
             panel_background: neutral().light().step_2(),
             panel_focused_border: blue().light().step_10(),
             panel_indent_guide: neutral().light_alpha().step_5(),
@@ -228,6 +229,7 @@ impl ThemeColors {
             tab_inactive_background: neutral().dark().step_2(),
             tab_active_background: neutral().dark().step_1(),
             search_match_background: neutral().dark().step_5(),
+            search_active_match_background: neutral().dark().step_3(),
             panel_background: neutral().dark().step_2(),
             panel_focused_border: blue().dark().step_8(),
             panel_indent_guide: neutral().dark_alpha().step_4(),

crates/theme/src/fallback_themes.rs 🔗

@@ -152,6 +152,7 @@ pub(crate) fn zed_default_dark() -> Theme {
                 tab_inactive_background: bg,
                 tab_active_background: editor,
                 search_match_background: bg,
+                search_active_match_background: bg,
 
                 editor_background: editor,
                 editor_gutter_background: editor,

crates/theme/src/schema.rs 🔗

@@ -287,6 +287,15 @@ pub fn theme_colors_refinement(
         .panel_background
         .as_ref()
         .and_then(|color| try_parse_color(color).ok());
+    let search_match_background = this
+        .search_match_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let search_active_match_background = this
+        .search_active_match_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok())
+        .or(search_match_background);
     ThemeColorsRefinement {
         border,
         border_variant: this
@@ -442,10 +451,8 @@ pub fn theme_colors_refinement(
             .tab_active_background
             .as_ref()
             .and_then(|color| try_parse_color(color).ok()),
-        search_match_background: this
-            .search_match_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok()),
+        search_match_background: search_match_background,
+        search_active_match_background: search_active_match_background,
         panel_background,
         panel_focused_border: this
             .panel_focused_border

crates/theme/src/styles/colors.rs 🔗

@@ -128,6 +128,7 @@ pub struct ThemeColors {
     pub tab_inactive_background: Hsla,
     pub tab_active_background: Hsla,
     pub search_match_background: Hsla,
+    pub search_active_match_background: Hsla,
     pub panel_background: Hsla,
     pub panel_focused_border: Hsla,
     pub panel_indent_guide: Hsla,
@@ -352,6 +353,7 @@ pub enum ThemeColorField {
     TabInactiveBackground,
     TabActiveBackground,
     SearchMatchBackground,
+    SearchActiveMatchBackground,
     PanelBackground,
     PanelFocusedBorder,
     PanelIndentGuide,
@@ -467,6 +469,7 @@ impl ThemeColors {
             ThemeColorField::TabInactiveBackground => self.tab_inactive_background,
             ThemeColorField::TabActiveBackground => self.tab_active_background,
             ThemeColorField::SearchMatchBackground => self.search_match_background,
+            ThemeColorField::SearchActiveMatchBackground => self.search_active_match_background,
             ThemeColorField::PanelBackground => self.panel_background,
             ThemeColorField::PanelFocusedBorder => self.panel_focused_border,
             ThemeColorField::PanelIndentGuide => self.panel_indent_guide,

crates/vim/src/normal/yank.rs 🔗

@@ -227,7 +227,7 @@ impl Vim {
 
         editor.highlight_background::<HighlightOnYank>(
             &ranges_to_highlight,
-            |colors| colors.colors().editor_document_highlight_read_background,
+            |_, colors| colors.colors().editor_document_highlight_read_background,
             cx,
         );
         cx.spawn(async move |this, cx| {

crates/vim/src/replace.rs 🔗

@@ -273,7 +273,7 @@ impl Vim {
             let ranges = [new_range];
             editor.highlight_background::<VimExchange>(
                 &ranges,
-                |theme| theme.colors().editor_document_highlight_read_background,
+                |_, theme| theme.colors().editor_document_highlight_read_background,
                 cx,
             );
         }

crates/workspace/src/searchable.rs 🔗

@@ -96,6 +96,7 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
     fn update_matches(
         &mut self,
         matches: &[Self::Match],
+        active_match_index: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     );
@@ -179,7 +180,13 @@ pub trait SearchableItemHandle: ItemHandle {
         handler: Box<dyn Fn(&SearchEvent, &mut Window, &mut App) + Send>,
     ) -> Subscription;
     fn clear_matches(&self, window: &mut Window, cx: &mut App);
-    fn update_matches(&self, matches: &AnyVec<dyn Send>, window: &mut Window, cx: &mut App);
+    fn update_matches(
+        &self,
+        matches: &AnyVec<dyn Send>,
+        active_match_index: Option<usize>,
+        window: &mut Window,
+        cx: &mut App,
+    );
     fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String;
     fn activate_match(
         &self,
@@ -264,10 +271,16 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
     fn clear_matches(&self, window: &mut Window, cx: &mut App) {
         self.update(cx, |this, cx| this.clear_matches(window, cx));
     }
-    fn update_matches(&self, matches: &AnyVec<dyn Send>, window: &mut Window, cx: &mut App) {
+    fn update_matches(
+        &self,
+        matches: &AnyVec<dyn Send>,
+        active_match_index: Option<usize>,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
         let matches = matches.downcast_ref().unwrap();
         self.update(cx, |this, cx| {
-            this.update_matches(matches.as_slice(), window, cx)
+            this.update_matches(matches.as_slice(), active_match_index, window, cx)
         });
     }
     fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String {