Add highlighted_ranges API to editor

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/editor.rs  | 137 ++++++++++++++++++++++++
crates/editor/src/element.rs | 207 +++++++++++++++++++++++++------------
2 files changed, 273 insertions(+), 71 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -9,12 +9,13 @@ mod test;
 
 use aho_corasick::AhoCorasick;
 use clock::ReplicaId;
-use collections::{HashMap, HashSet};
+use collections::{BTreeMap, HashMap, HashSet};
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
 use gpui::{
     action,
+    color::Color,
     elements::*,
     fonts::TextStyle,
     geometry::vector::{vec2f, Vector2F},
@@ -37,7 +38,8 @@ use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
 use std::{
-    cmp,
+    any::TypeId,
+    cmp::{self, Ordering},
     iter::{self, FromIterator},
     mem,
     ops::{Deref, Range, RangeInclusive, Sub},
@@ -382,6 +384,7 @@ pub struct Editor {
     vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
+    highlighted_ranges: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
 }
 
@@ -522,6 +525,7 @@ impl Editor {
             vertical_scroll_margin: 3.0,
             placeholder_text: None,
             highlighted_rows: None,
+            highlighted_ranges: Default::default(),
             nav_history: None,
         };
         let selection = Selection {
@@ -3721,6 +3725,58 @@ impl Editor {
         self.highlighted_rows.clone()
     }
 
+    pub fn highlight_ranges<T: 'static>(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        color: Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.highlighted_ranges
+            .insert(TypeId::of::<T>(), (color, ranges));
+        cx.notify();
+    }
+
+    pub fn clear_highlighted_ranges<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        self.highlighted_ranges.remove(&TypeId::of::<T>());
+        cx.notify();
+    }
+
+    pub fn highlighted_ranges_in_range(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+    ) -> Vec<(Color, Range<DisplayPoint>)> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        for (color, ranges) in self.highlighted_ranges.values() {
+            let start_ix = match ranges.binary_search_by(|probe| {
+                let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap();
+                if cmp.is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Less
+                }
+            }) {
+                Ok(i) | Err(i) => i,
+            };
+            for range in &ranges[start_ix..] {
+                if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() {
+                    break;
+                }
+                let start = range
+                    .start
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                let end = range
+                    .end
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                results.push((*color, start..end))
+            }
+        }
+        results
+    }
+
     fn next_blink_epoch(&mut self) -> usize {
         self.blink_epoch += 1;
         self.blink_epoch
@@ -6555,6 +6611,83 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
+        let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+        let settings = EditorSettings::test(&cx);
+        let (_, editor) = cx.add_window(Default::default(), |cx| {
+            build_editor(buffer.clone(), settings, cx)
+        });
+
+        editor.update(cx, |editor, cx| {
+            struct Type1;
+            struct Type2;
+
+            let buffer = buffer.read(cx).snapshot(cx);
+
+            let anchor_range = |range: Range<Point>| {
+                buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
+            };
+
+            editor.highlight_ranges::<Type1>(
+                vec![
+                    anchor_range(Point::new(2, 1)..Point::new(2, 3)),
+                    anchor_range(Point::new(4, 2)..Point::new(4, 4)),
+                    anchor_range(Point::new(6, 3)..Point::new(6, 5)),
+                    anchor_range(Point::new(8, 4)..Point::new(8, 6)),
+                ],
+                Color::red(),
+                cx,
+            );
+            editor.highlight_ranges::<Type2>(
+                vec![
+                    anchor_range(Point::new(3, 2)..Point::new(3, 5)),
+                    anchor_range(Point::new(5, 3)..Point::new(5, 6)),
+                    anchor_range(Point::new(7, 4)..Point::new(7, 7)),
+                    anchor_range(Point::new(9, 5)..Point::new(9, 8)),
+                ],
+                Color::green(),
+                cx,
+            );
+
+            let snapshot = editor.snapshot(cx);
+            assert_eq!(
+                editor.highlighted_ranges_in_range(
+                    anchor_range(Point::new(3, 4)..Point::new(7, 4)),
+                    &snapshot,
+                ),
+                &[
+                    (
+                        Color::red(),
+                        DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
+                    ),
+                    (
+                        Color::red(),
+                        DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                    ),
+                    (
+                        Color::green(),
+                        DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5)
+                    ),
+                    (
+                        Color::green(),
+                        DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6)
+                    ),
+                ]
+            );
+            assert_eq!(
+                editor.highlighted_ranges_in_range(
+                    anchor_range(Point::new(5, 6)..Point::new(6, 4)),
+                    &snapshot,
+                ),
+                &[(
+                    Color::red(),
+                    DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                ),]
+            );
+        });
+    }
+
     fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point

crates/editor/src/element.rs 🔗

@@ -312,64 +312,45 @@ impl EditorElement {
         let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
         let max_glyph_width = layout.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
+        let content_origin = bounds.origin() + layout.text_offset;
 
         cx.scene.push_layer(Some(bounds));
 
-        // Draw selections
-        let corner_radius = 2.5;
-        let mut cursors = SmallVec::<[Cursor; 32]>::new();
-
-        let content_origin = bounds.origin() + layout.text_offset;
+        for (color, range) in &layout.highlighted_ranges {
+            self.paint_highlighted_range(
+                range.clone(),
+                start_row,
+                end_row,
+                *color,
+                0.,
+                layout,
+                content_origin,
+                scroll_top,
+                scroll_left,
+                bounds,
+                cx,
+            );
+        }
 
+        let mut cursors = SmallVec::<[Cursor; 32]>::new();
         for (replica_id, selections) in &layout.selections {
             let style = style.replica_selection_style(*replica_id);
+            let corner_radius = 0.15 * layout.line_height;
 
             for selection in selections {
-                if selection.start != selection.end {
-                    let row_range = if selection.end.column() == 0 {
-                        cmp::max(selection.start.row(), start_row)
-                            ..cmp::min(selection.end.row(), end_row)
-                    } else {
-                        cmp::max(selection.start.row(), start_row)
-                            ..cmp::min(selection.end.row() + 1, end_row)
-                    };
-
-                    let selection = Selection {
-                        color: style.selection,
-                        line_height: layout.line_height,
-                        start_y: content_origin.y() + row_range.start as f32 * layout.line_height
-                            - scroll_top,
-                        lines: row_range
-                            .into_iter()
-                            .map(|row| {
-                                let line_layout = &layout.line_layouts[(row - start_row) as usize];
-                                SelectionLine {
-                                    start_x: if row == selection.start.row() {
-                                        content_origin.x()
-                                            + line_layout
-                                                .x_for_index(selection.start.column() as usize)
-                                            - scroll_left
-                                    } else {
-                                        content_origin.x() - scroll_left
-                                    },
-                                    end_x: if row == selection.end.row() {
-                                        content_origin.x()
-                                            + line_layout
-                                                .x_for_index(selection.end.column() as usize)
-                                            - scroll_left
-                                    } else {
-                                        content_origin.x()
-                                            + line_layout.width()
-                                            + corner_radius * 2.0
-                                            - scroll_left
-                                    },
-                                }
-                            })
-                            .collect(),
-                    };
-
-                    selection.paint(bounds, cx.scene);
-                }
+                self.paint_highlighted_range(
+                    selection.start..selection.end,
+                    start_row,
+                    end_row,
+                    style.selection,
+                    corner_radius,
+                    layout,
+                    content_origin,
+                    scroll_top,
+                    scroll_left,
+                    bounds,
+                    cx,
+                );
 
                 if view.show_local_cursors() || *replica_id != local_replica_id {
                     let cursor_position = selection.head();
@@ -412,6 +393,62 @@ impl EditorElement {
         cx.scene.pop_layer();
     }
 
+    fn paint_highlighted_range(
+        &self,
+        range: Range<DisplayPoint>,
+        start_row: u32,
+        end_row: u32,
+        color: Color,
+        corner_radius: f32,
+        layout: &LayoutState,
+        content_origin: Vector2F,
+        scroll_top: f32,
+        scroll_left: f32,
+        bounds: RectF,
+        cx: &mut PaintContext,
+    ) {
+        if range.start != range.end {
+            let row_range = if range.end.column() == 0 {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+            } else {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+            };
+
+            let selection = HighlightedRange {
+                color,
+                line_height: layout.line_height,
+                corner_radius,
+                start_y: content_origin.y() + row_range.start as f32 * layout.line_height
+                    - scroll_top,
+                lines: row_range
+                    .into_iter()
+                    .map(|row| {
+                        let line_layout = &layout.line_layouts[(row - start_row) as usize];
+                        HighlightedRangeLine {
+                            start_x: if row == range.start.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.start.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() - scroll_left
+                            },
+                            end_x: if row == range.end.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.end.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() + line_layout.width() + corner_radius * 2.0
+                                    - scroll_left
+                            },
+                        }
+                    })
+                    .collect(),
+            };
+
+            selection.paint(bounds, cx.scene);
+        }
+    }
+
     fn paint_blocks(
         &mut self,
         bounds: RectF,
@@ -715,10 +752,16 @@ impl Element for EditorElement {
         let mut selections = HashMap::default();
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
+        let mut highlighted_ranges = Vec::new();
         self.update_view(cx.app, |view, cx| {
-            highlighted_rows = view.highlighted_rows();
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
+            highlighted_rows = view.highlighted_rows();
+            highlighted_ranges = view.highlighted_ranges_in_range(
+                start_anchor.clone()..end_anchor.clone(),
+                &display_map,
+            );
+
             let local_selections = view
                 .local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map);
             for selection in &local_selections {
@@ -837,6 +880,7 @@ impl Element for EditorElement {
                 snapshot,
                 active_rows,
                 highlighted_rows,
+                highlighted_ranges,
                 line_layouts,
                 line_number_layouts,
                 blocks,
@@ -950,6 +994,7 @@ pub struct LayoutState {
     line_height: f32,
     em_width: f32,
     em_advance: f32,
+    highlighted_ranges: Vec<(Color, Range<DisplayPoint>)>,
     selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
     text_offset: Vector2F,
 }
@@ -1036,20 +1081,21 @@ impl Cursor {
 }
 
 #[derive(Debug)]
-struct Selection {
+struct HighlightedRange {
     start_y: f32,
     line_height: f32,
-    lines: Vec<SelectionLine>,
+    lines: Vec<HighlightedRangeLine>,
     color: Color,
+    corner_radius: f32,
 }
 
 #[derive(Debug)]
-struct SelectionLine {
+struct HighlightedRangeLine {
     start_x: f32,
     end_x: f32,
 }
 
-impl Selection {
+impl HighlightedRange {
     fn paint(&self, bounds: RectF, scene: &mut Scene) {
         if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
             self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
@@ -1064,26 +1110,31 @@ impl Selection {
         }
     }
 
-    fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) {
+    fn paint_lines(
+        &self,
+        start_y: f32,
+        lines: &[HighlightedRangeLine],
+        bounds: RectF,
+        scene: &mut Scene,
+    ) {
         if lines.is_empty() {
             return;
         }
 
         let mut path = PathBuilder::new();
-        let corner_radius = 0.15 * self.line_height;
         let first_line = lines.first().unwrap();
         let last_line = lines.last().unwrap();
 
         let first_top_left = vec2f(first_line.start_x, start_y);
         let first_top_right = vec2f(first_line.end_x, start_y);
 
-        let curve_height = vec2f(0., corner_radius);
+        let curve_height = vec2f(0., self.corner_radius);
         let curve_width = |start_x: f32, end_x: f32| {
             let max = (end_x - start_x) / 2.;
-            let width = if max < corner_radius {
+            let width = if max < self.corner_radius {
                 max
             } else {
-                corner_radius
+                self.corner_radius
             };
 
             vec2f(width, 0.)
@@ -1107,26 +1158,38 @@ impl Selection {
                     Ordering::Less => {
                         let curve_width = curve_width(next_top_right.x(), bottom_right.x());
                         path.line_to(bottom_right - curve_height);
-                        path.curve_to(bottom_right - curve_width, bottom_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right - curve_width, bottom_right);
+                        }
                         path.line_to(next_top_right + curve_width);
-                        path.curve_to(next_top_right + curve_height, next_top_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
                     }
                     Ordering::Greater => {
                         let curve_width = curve_width(bottom_right.x(), next_top_right.x());
                         path.line_to(bottom_right - curve_height);
-                        path.curve_to(bottom_right + curve_width, bottom_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right + curve_width, bottom_right);
+                        }
                         path.line_to(next_top_right - curve_width);
-                        path.curve_to(next_top_right + curve_height, next_top_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
                     }
                 }
             } else {
                 let curve_width = curve_width(line.start_x, line.end_x);
                 path.line_to(bottom_right - curve_height);
-                path.curve_to(bottom_right - curve_width, bottom_right);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_right - curve_width, bottom_right);
+                }
 
                 let bottom_left = vec2f(line.start_x, bottom_right.y());
                 path.line_to(bottom_left + curve_width);
-                path.curve_to(bottom_left - curve_height, bottom_left);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_left - curve_height, bottom_left);
+                }
             }
         }
 
@@ -1134,14 +1197,20 @@ impl Selection {
             let curve_width = curve_width(last_line.start_x, first_line.start_x);
             let second_top_left = vec2f(last_line.start_x, start_y + self.line_height);
             path.line_to(second_top_left + curve_height);
-            path.curve_to(second_top_left + curve_width, second_top_left);
+            if self.corner_radius > 0. {
+                path.curve_to(second_top_left + curve_width, second_top_left);
+            }
             let first_bottom_left = vec2f(first_line.start_x, second_top_left.y());
             path.line_to(first_bottom_left - curve_width);
-            path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+            if self.corner_radius > 0. {
+                path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+            }
         }
 
         path.line_to(first_top_left + curve_height);
-        path.curve_to(first_top_left + top_curve_width, first_top_left);
+        if self.corner_radius > 0. {
+            path.curve_to(first_top_left + top_curve_width, first_top_left);
+        }
         path.line_to(first_top_right - top_curve_width);
 
         scene.push_path(path.build(self.color, Some(bounds)));