Allow highlighting editor rows from multiple sources concurrently (#9153)

Kirill Bulatov created

Change summary

Cargo.lock                             |  12 +
crates/editor/Cargo.toml               |   2 
crates/editor/src/editor.rs            |  97 ++++++++
crates/editor/src/element.rs           |  48 +++
crates/editor/src/scroll/autoscroll.rs |  12 
crates/go_to_line/Cargo.toml           |   9 
crates/go_to_line/src/go_to_line.rs    | 153 ++++++++++++++
crates/outline/Cargo.toml              |   9 
crates/outline/src/outline.rs          | 293 ++++++++++++++++++++++++++-
9 files changed, 590 insertions(+), 45 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4287,9 +4287,15 @@ version = "0.1.0"
 dependencies = [
  "editor",
  "gpui",
+ "indoc",
+ "language",
  "menu",
+ "project",
+ "serde_json",
  "text",
  "theme",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
  "ui",
  "util",
  "workspace",
@@ -6614,12 +6620,18 @@ dependencies = [
  "editor",
  "fuzzy",
  "gpui",
+ "indoc",
  "language",
+ "menu",
  "ordered-float 2.10.0",
  "picker",
+ "project",
+ "serde_json",
  "settings",
  "smol",
  "theme",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
  "ui",
  "util",
  "workspace",

crates/editor/Cargo.toml 🔗

@@ -41,7 +41,7 @@ futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true
 gpui.workspace = true
-indoc = "1.0.4"
+indoc.workspace = true
 itertools.workspace = true
 language.workspace = true
 lazy_static.workspace = true

crates/editor/src/editor.rs 🔗

@@ -43,7 +43,7 @@ use anyhow::{anyhow, Context as _, Result};
 use blink_manager::BlinkManager;
 use client::{Collaborator, ParticipantIndex};
 use clock::ReplicaId;
-use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use copilot::Copilot;
 use debounced_delay::DebouncedDelay;
@@ -386,7 +386,8 @@ pub struct Editor {
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
-    highlighted_rows: Option<Range<u32>>,
+    highlight_order: usize,
+    highlighted_rows: HashMap<TypeId, Vec<(usize, Range<Anchor>, Hsla)>>,
     background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
     context_menu: RwLock<Option<ContextMenu>>,
@@ -1523,7 +1524,8 @@ impl Editor {
             show_gutter: mode == EditorMode::Full,
             show_wrap_guides: None,
             placeholder_text: None,
-            highlighted_rows: None,
+            highlight_order: 0,
+            highlighted_rows: HashMap::default(),
             background_highlights: Default::default(),
             nav_history: None,
             context_menu: RwLock::new(None),
@@ -8921,12 +8923,93 @@ impl Editor {
         }
     }
 
-    pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
-        self.highlighted_rows = rows;
+    /// Adds or removes (on `None` color) a highlight for the rows corresponding to the anchor range given.
+    /// On matching anchor range, replaces the old highlight; does not clear the other existing highlights.
+    /// If multiple anchor ranges will produce highlights for the same row, the last range added will be used.
+    pub fn highlight_rows<T: 'static>(
+        &mut self,
+        rows: Range<Anchor>,
+        color: Option<Hsla>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        match self.highlighted_rows.entry(TypeId::of::<T>()) {
+            hash_map::Entry::Occupied(o) => {
+                let row_highlights = o.into_mut();
+                let existing_highlight_index =
+                    row_highlights.binary_search_by(|(_, highlight_range, _)| {
+                        highlight_range
+                            .start
+                            .cmp(&rows.start, &multi_buffer_snapshot)
+                            .then(highlight_range.end.cmp(&rows.end, &multi_buffer_snapshot))
+                    });
+                match color {
+                    Some(color) => {
+                        let insert_index = match existing_highlight_index {
+                            Ok(i) => i,
+                            Err(i) => i,
+                        };
+                        row_highlights.insert(
+                            insert_index,
+                            (post_inc(&mut self.highlight_order), rows, color),
+                        );
+                    }
+                    None => {
+                        if let Ok(i) = existing_highlight_index {
+                            row_highlights.remove(i);
+                        }
+                    }
+                }
+            }
+            hash_map::Entry::Vacant(v) => {
+                if let Some(color) = color {
+                    v.insert(vec![(post_inc(&mut self.highlight_order), rows, color)]);
+                }
+            }
+        }
     }
 
-    pub fn highlighted_rows(&self) -> Option<Range<u32>> {
-        self.highlighted_rows.clone()
+    /// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted.
+    pub fn clear_row_highlights<T: 'static>(&mut self) {
+        self.highlighted_rows.remove(&TypeId::of::<T>());
+    }
+
+    /// For a highlight given context type, gets all anchor ranges that will be used for row highlighting.
+    pub fn highlighted_rows<T: 'static>(
+        &self,
+    ) -> Option<impl Iterator<Item = (&Range<Anchor>, &Hsla)>> {
+        Some(
+            self.highlighted_rows
+                .get(&TypeId::of::<T>())?
+                .iter()
+                .map(|(_, range, color)| (range, color)),
+        )
+    }
+
+    // Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
+    // Rerturns a map of display rows that are highlighted and their corresponding highlight color.
+    pub fn highlighted_display_rows(&mut self, cx: &mut WindowContext) -> BTreeMap<u32, Hsla> {
+        let snapshot = self.snapshot(cx);
+        let mut used_highlight_orders = HashMap::default();
+        self.highlighted_rows
+            .iter()
+            .flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
+            .fold(
+                BTreeMap::<u32, Hsla>::new(),
+                |mut unique_rows, (highlight_order, anchor_range, hsla)| {
+                    let start_row = anchor_range.start.to_display_point(&snapshot).row();
+                    let end_row = anchor_range.end.to_display_point(&snapshot).row();
+                    for row in start_row..=end_row {
+                        let used_index =
+                            used_highlight_orders.entry(row).or_insert(*highlight_order);
+                        if highlight_order >= used_index {
+                            *used_index = *highlight_order;
+                            unique_rows.insert(row, *hsla);
+                        }
+                    }
+                    unique_rows
+                },
+            )
     }
 
     pub fn highlight_background<T: 'static>(

crates/editor/src/element.rs 🔗

@@ -665,19 +665,53 @@ impl EditorElement {
                 }
             }
 
-            if let Some(highlighted_rows) = &layout.highlighted_rows {
+            let mut paint_highlight = |highlight_row_start: u32, highlight_row_end: u32, color| {
                 let origin = point(
                     bounds.origin.x,
                     bounds.origin.y
-                        + (layout.position_map.line_height * highlighted_rows.start as f32)
+                        + (layout.position_map.line_height * highlight_row_start as f32)
                         - scroll_top,
                 );
                 let size = size(
                     bounds.size.width,
-                    layout.position_map.line_height * highlighted_rows.len() as f32,
+                    layout.position_map.line_height
+                        * (highlight_row_end + 1 - highlight_row_start) as f32,
                 );
-                let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
-                cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
+                cx.paint_quad(fill(Bounds { origin, size }, color));
+            };
+            let mut last_row = None;
+            let mut highlight_row_start = 0u32;
+            let mut highlight_row_end = 0u32;
+            for (&row, &color) in &layout.highlighted_rows {
+                let paint = last_row.map_or(false, |(last_row, last_color)| {
+                    last_color != color || last_row + 1 < row
+                });
+
+                if paint {
+                    let paint_range_is_unfinished = highlight_row_end == 0;
+                    if paint_range_is_unfinished {
+                        highlight_row_end = row;
+                        last_row = None;
+                    }
+                    paint_highlight(highlight_row_start, highlight_row_end, color);
+                    highlight_row_start = 0;
+                    highlight_row_end = 0;
+                    if !paint_range_is_unfinished {
+                        highlight_row_start = row;
+                        last_row = Some((row, color));
+                    }
+                } else {
+                    if last_row.is_none() {
+                        highlight_row_start = row;
+                    } else {
+                        highlight_row_end = row;
+                    }
+                    last_row = Some((row, color));
+                }
+            }
+            if let Some((row, hsla)) = last_row {
+                highlight_row_end = row;
+                paint_highlight(highlight_row_start, highlight_row_end, hsla);
             }
 
             let scroll_left =
@@ -2064,7 +2098,7 @@ impl EditorElement {
             let mut active_rows = BTreeMap::new();
             let is_singleton = editor.is_singleton(cx);
 
-            let highlighted_rows = editor.highlighted_rows();
+            let highlighted_rows = editor.highlighted_display_rows(cx);
             let highlighted_ranges = editor.background_highlights_in_range(
                 start_anchor..end_anchor,
                 &snapshot.display_snapshot,
@@ -3198,7 +3232,7 @@ pub struct LayoutState {
     visible_anchor_range: Range<Anchor>,
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
-    highlighted_rows: Option<Range<u32>>,
+    highlighted_rows: BTreeMap<u32, Hsla>,
     line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -81,8 +81,8 @@ impl Editor {
 
         let mut target_top;
         let mut target_bottom;
-        if let Some(highlighted_rows) = &self.highlighted_rows {
-            target_top = highlighted_rows.start as f32;
+        if let Some(first_highlighted_row) = &self.highlighted_display_rows(cx).first_entry() {
+            target_top = *first_highlighted_row.key() as f32;
             target_bottom = target_top + 1.;
         } else {
             let selections = self.selections.all::<Point>(cx);
@@ -205,10 +205,7 @@ impl Editor {
         let mut target_left;
         let mut target_right;
 
-        if self.highlighted_rows.is_some() {
-            target_left = px(0.);
-            target_right = px(0.);
-        } else {
+        if self.highlighted_rows.is_empty() {
             target_left = px(f32::INFINITY);
             target_right = px(0.);
             for selection in selections {
@@ -229,6 +226,9 @@ impl Editor {
                     );
                 }
             }
+        } else {
+            target_left = px(0.);
+            target_right = px(0.);
         }
 
         target_right = target_right.min(scroll_width);

crates/go_to_line/Cargo.toml 🔗

@@ -24,3 +24,12 @@ workspace.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+indoc.workspace = true
+language = { workspace = true, features = ["test-support"] }
+menu.workspace = true
+project = { workspace = true, features = ["test-support"] }
+serde_json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-typescript.workspace = true
+workspace = { workspace = true, features = ["test-support"] }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,6 +1,6 @@
 pub mod cursor_position;
 
-use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Editor};
+use editor::{scroll::Autoscroll, Editor};
 use gpui::{
     actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
     FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
@@ -34,6 +34,8 @@ impl FocusableView for GoToLine {
 }
 impl EventEmitter<DismissEvent> for GoToLine {}
 
+enum GoToLineRowHighlights {}
+
 impl GoToLine {
     fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
         let handle = cx.view().downgrade();
@@ -84,7 +86,7 @@ impl GoToLine {
             .update(cx, |_, cx| {
                 let scroll_position = self.prev_scroll_position.take();
                 self.active_editor.update(cx, |editor, cx| {
-                    editor.highlight_rows(None);
+                    editor.clear_row_highlights::<GoToLineRowHighlights>();
                     if let Some(scroll_position) = scroll_position {
                         editor.set_scroll_position(scroll_position, cx);
                     }
@@ -112,9 +114,13 @@ impl GoToLine {
             self.active_editor.update(cx, |active_editor, cx| {
                 let snapshot = active_editor.snapshot(cx).display_snapshot;
                 let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-                let display_point = point.to_display_point(&snapshot);
-                let row = display_point.row();
-                active_editor.highlight_rows(Some(row..row + 1));
+                let anchor = snapshot.buffer_snapshot.anchor_before(point);
+                active_editor.clear_row_highlights::<GoToLineRowHighlights>();
+                active_editor.highlight_rows::<GoToLineRowHighlights>(
+                    anchor..anchor,
+                    Some(cx.theme().colors().editor_highlighted_line_background),
+                    cx,
+                );
                 active_editor.request_autoscroll(Autoscroll::center(), cx);
             });
             cx.notify();
@@ -207,3 +213,140 @@ impl Render for GoToLine {
             )
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use gpui::{TestAppContext, VisualTestContext};
+    use indoc::indoc;
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use workspace::{AppState, Workspace};
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": indoc!{"
+                    struct SingleLine; // display line 0
+                                       // display line 1
+                    struct MultiLine { // display line 2
+                        field_1: i32,  // display line 3
+                        field_2: i32,  // display line 4
+                    }                  // display line 5
+                                       // display line 7
+                    struct Another {   // display line 8
+                        field_1: i32,  // display line 9
+                        field_2: i32,  // display line 10
+                        field_3: i32,  // display line 11
+                        field_4: i32,  // display line 12
+                    }                  // display line 13
+                "}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().update(cx, |project, cx| {
+                project.worktrees().next().unwrap().read(cx).id()
+            })
+        });
+        let _buffer = project
+            .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+            .await
+            .unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "a.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        let go_to_line_view = open_go_to_line_view(&workspace, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "Initially opened go to line modal should not highlight any rows"
+        );
+
+        cx.simulate_input("1");
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            vec![0],
+            "Go to line modal should highlight a row, corresponding to the query"
+        );
+
+        cx.simulate_input("8");
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            vec![13],
+            "If the query is too large, the last row should be highlighted"
+        );
+
+        cx.dispatch_action(menu::Cancel);
+        drop(go_to_line_view);
+        editor.update(cx, |_, _| {});
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "After cancelling and closing the modal, no rows should be highlighted"
+        );
+
+        let go_to_line_view = open_go_to_line_view(&workspace, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "Reopened modal should not highlight any rows"
+        );
+
+        cx.simulate_input("5");
+        assert_eq!(highlighted_display_rows(&editor, cx), vec![4]);
+
+        cx.dispatch_action(menu::Confirm);
+        drop(go_to_line_view);
+        editor.update(cx, |_, _| {});
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "After confirming and closing the modal, no rows should be highlighted"
+        );
+    }
+
+    fn open_go_to_line_view(
+        workspace: &View<Workspace>,
+        cx: &mut VisualTestContext,
+    ) -> View<GoToLine> {
+        cx.dispatch_action(Toggle::default());
+        workspace.update(cx, |workspace, cx| {
+            workspace.active_modal::<GoToLine>(cx).unwrap().clone()
+        })
+    }
+
+    fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
+        editor.update(cx, |editor, cx| {
+            editor.highlighted_display_rows(cx).into_keys().collect()
+        })
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let state = AppState::test(cx);
+            language::init(cx);
+            crate::init(cx);
+            editor::init(cx);
+            workspace::init_settings(cx);
+            Project::init_settings(cx);
+            state
+        })
+    }
+}

crates/outline/Cargo.toml 🔗

@@ -28,3 +28,12 @@ workspace.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+indoc.workspace = true
+language = { workspace = true, features = ["test-support"] }
+menu.workspace = true
+project = { workspace = true, features = ["test-support"] }
+serde_json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-typescript.workspace = true
+workspace = { workspace = true, features = ["test-support"] }

crates/outline/src/outline.rs 🔗

@@ -1,7 +1,4 @@
-use editor::{
-    display_map::ToDisplayPoint, scroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor,
-    EditorMode, ToPoint,
-};
+use editor::{scroll::Autoscroll, Anchor, AnchorRangeExt, Editor, EditorMode};
 use fuzzy::StringMatch;
 use gpui::{
     actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
@@ -121,7 +118,7 @@ impl OutlineViewDelegate {
 
     fn restore_active_editor(&mut self, cx: &mut WindowContext) {
         self.active_editor.update(cx, |editor, cx| {
-            editor.highlight_rows(None);
+            editor.clear_row_highlights::<OutlineRowHighlights>();
             if let Some(scroll_position) = self.prev_scroll_position {
                 editor.set_scroll_position(scroll_position, cx);
             }
@@ -141,19 +138,20 @@ impl OutlineViewDelegate {
             let outline_item = &self.outline.items[selected_match.candidate_id];
 
             self.active_editor.update(cx, |active_editor, cx| {
-                let snapshot = active_editor.snapshot(cx).display_snapshot;
-                let buffer_snapshot = &snapshot.buffer_snapshot;
-                let start = outline_item.range.start.to_point(buffer_snapshot);
-                let end = outline_item.range.end.to_point(buffer_snapshot);
-                let display_rows = start.to_display_point(&snapshot).row()
-                    ..end.to_display_point(&snapshot).row() + 1;
-                active_editor.highlight_rows(Some(display_rows));
+                active_editor.clear_row_highlights::<OutlineRowHighlights>();
+                active_editor.highlight_rows::<OutlineRowHighlights>(
+                    outline_item.range.clone(),
+                    Some(cx.theme().colors().editor_highlighted_line_background),
+                    cx,
+                );
                 active_editor.request_autoscroll(Autoscroll::center(), cx);
             });
         }
     }
 }
 
+enum OutlineRowHighlights {}
+
 impl PickerDelegate for OutlineViewDelegate {
     type ListItem = ListItem;
 
@@ -240,13 +238,13 @@ impl PickerDelegate for OutlineViewDelegate {
         self.prev_scroll_position.take();
 
         self.active_editor.update(cx, |active_editor, cx| {
-            if let Some(rows) = active_editor.highlighted_rows() {
-                let snapshot = active_editor.snapshot(cx).display_snapshot;
-                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
-                active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                    s.select_ranges([position..position])
-                });
-                active_editor.highlight_rows(None);
+            if let Some(rows) = active_editor
+                .highlighted_rows::<OutlineRowHighlights>()
+                .and_then(|highlights| highlights.into_iter().next().map(|(rows, _)| rows.clone()))
+            {
+                active_editor
+                    .change_selections(Some(Autoscroll::center()), cx, |s| s.select_ranges([rows]));
+                active_editor.clear_row_highlights::<OutlineRowHighlights>();
                 active_editor.focus(cx);
             }
         });
@@ -314,3 +312,260 @@ impl PickerDelegate for OutlineViewDelegate {
         )
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use gpui::{TestAppContext, VisualTestContext};
+    use indoc::indoc;
+    use language::{Language, LanguageConfig, LanguageMatcher};
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use workspace::{AppState, Workspace};
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": indoc!{"
+                    struct SingleLine; // display line 0
+                                       // display line 1
+                    struct MultiLine { // display line 2
+                        field_1: i32,  // display line 3
+                        field_2: i32,  // display line 4
+                    }                  // display line 5
+                "}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
+
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().update(cx, |project, cx| {
+                project.worktrees().next().unwrap().read(cx).id()
+            })
+        });
+        let _buffer = project
+            .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+            .await
+            .unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "a.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let ensure_outline_view_contents =
+            |outline_view: &View<Picker<OutlineViewDelegate>>, cx: &mut VisualTestContext| {
+                assert_eq!(query(&outline_view, cx), "");
+                assert_eq!(
+                    outline_names(&outline_view, cx),
+                    vec![
+                        "struct SingleLine",
+                        "struct MultiLine",
+                        "field_1",
+                        "field_2"
+                    ],
+                );
+            };
+
+        let outline_view = open_outline_view(&workspace, cx);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "Initially opened outline view should have no highlights"
+        );
+
+        cx.dispatch_action(menu::SelectNext);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            vec![2, 3, 4, 5],
+            "Second struct's rows should be highlighted"
+        );
+
+        cx.dispatch_action(menu::SelectPrev);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            vec![0],
+            "First struct's row should be highlighted"
+        );
+
+        cx.dispatch_action(menu::Cancel);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "No rows should be highlighted after outline view is cancelled and closed"
+        );
+
+        let outline_view = open_outline_view(&workspace, cx);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "Reopened outline view should have no highlights"
+        );
+
+        cx.dispatch_action(menu::SelectNext);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(highlighted_display_rows(&editor, cx), vec![2, 3, 4, 5]);
+
+        cx.dispatch_action(menu::Confirm);
+        ensure_outline_view_contents(&outline_view, cx);
+        assert_eq!(
+            highlighted_display_rows(&editor, cx),
+            Vec::<u32>::new(),
+            "No rows should be highlighted after outline view is confirmed and closed"
+        );
+    }
+
+    fn open_outline_view(
+        workspace: &View<Workspace>,
+        cx: &mut VisualTestContext,
+    ) -> View<Picker<OutlineViewDelegate>> {
+        cx.dispatch_action(Toggle::default());
+        workspace.update(cx, |workspace, cx| {
+            workspace
+                .active_modal::<OutlineView>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        })
+    }
+
+    fn query(
+        outline_view: &View<Picker<OutlineViewDelegate>>,
+        cx: &mut VisualTestContext,
+    ) -> String {
+        outline_view.update(cx, |outline_view, cx| outline_view.query(cx))
+    }
+
+    fn outline_names(
+        outline_view: &View<Picker<OutlineViewDelegate>>,
+        cx: &mut VisualTestContext,
+    ) -> Vec<String> {
+        outline_view.update(cx, |outline_view, _| {
+            let items = &outline_view.delegate.outline.items;
+            outline_view
+                .delegate
+                .matches
+                .iter()
+                .map(|hit| items[hit.candidate_id].text.clone())
+                .collect::<Vec<_>>()
+        })
+    }
+
+    fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
+        editor.update(cx, |editor, cx| {
+            editor.highlighted_display_rows(cx).into_keys().collect()
+        })
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let state = AppState::test(cx);
+            language::init(cx);
+            crate::init(cx);
+            editor::init(cx);
+            workspace::init_settings(cx);
+            Project::init_settings(cx);
+            state
+        })
+    }
+
+    fn rust_lang() -> Arc<Language> {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Rust".into(),
+                    matcher: LanguageMatcher {
+                        path_suffixes: vec!["rs".to_string()],
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_outline_query(
+                r#"(struct_item
+            (visibility_modifier)? @context
+            "struct" @context
+            name: (_) @name) @item
+
+        (enum_item
+            (visibility_modifier)? @context
+            "enum" @context
+            name: (_) @name) @item
+
+        (enum_variant
+            (visibility_modifier)? @context
+            name: (_) @name) @item
+
+        (impl_item
+            "impl" @context
+            trait: (_)? @name
+            "for"? @context
+            type: (_) @name) @item
+
+        (trait_item
+            (visibility_modifier)? @context
+            "trait" @context
+            name: (_) @name) @item
+
+        (function_item
+            (visibility_modifier)? @context
+            (function_modifiers)? @context
+            "fn" @context
+            name: (_) @name) @item
+
+        (function_signature_item
+            (visibility_modifier)? @context
+            (function_modifiers)? @context
+            "fn" @context
+            name: (_) @name) @item
+
+        (macro_definition
+            . "macro_rules!" @context
+            name: (_) @name) @item
+
+        (mod_item
+            (visibility_modifier)? @context
+            "mod" @context
+            name: (_) @name) @item
+
+        (type_item
+            (visibility_modifier)? @context
+            "type" @context
+            name: (_) @name) @item
+
+        (associated_type
+            "type" @context
+            name: (_) @name) @item
+
+        (const_item
+            (visibility_modifier)? @context
+            "const" @context
+            name: (_) @name) @item
+
+        (field_declaration
+            (visibility_modifier)? @context
+            name: (_) @name) @item
+"#,
+            )
+            .unwrap(),
+        )
+    }
+}