Highlight active row(s) in the gutter

Antonio Scandurra created

Change summary

zed/assets/themes/dark.toml |  1 
zed/src/editor.rs           | 49 +++++++++++++++++++++--------
zed/src/editor/element.rs   | 65 +++++++++++++++++++++++++-------------
3 files changed, 78 insertions(+), 37 deletions(-)

Detailed changes

zed/assets/themes/dark.toml 🔗

@@ -14,7 +14,6 @@ modal_match_border = 0x000000
 modal_match_text = 0xcccccc
 modal_match_text_highlight = 0x18a3ff
 
-
 [editor]
 background = 0x1c1d1e
 gutter_background = 0x1c1d1e

zed/src/editor.rs 🔗

@@ -5,6 +5,7 @@ pub mod movement;
 
 use crate::{
     settings::{Settings, StyleId, Theme},
+    time::ReplicaId,
     util::{post_inc, Bias},
     workspace,
     worktree::{File, Worktree},
@@ -26,6 +27,7 @@ use smallvec::SmallVec;
 use smol::Timer;
 use std::{
     cmp::{self, Ordering},
+    collections::HashSet,
     fmt::Write,
     iter::FromIterator,
     mem,
@@ -461,6 +463,10 @@ impl Editor {
         }
     }
 
+    pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
+        self.buffer.read(cx).replica_id()
+    }
+
     pub fn buffer(&self) -> &ModelHandle<Buffer> {
         &self.buffer
     }
@@ -2350,27 +2356,28 @@ impl Snapshot {
 
     pub fn layout_line_numbers(
         &self,
-        viewport_height: f32,
+        rows: Range<u32>,
+        active_rows: &HashSet<u32>,
         font_cache: &FontCache,
         layout_cache: &TextLayoutCache,
         theme: &Theme,
     ) -> Result<Vec<Option<text_layout::Line>>> {
         let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
 
-        let start_row = self.scroll_position().y() as usize;
-        let end_row = cmp::min(
-            self.display_snapshot.max_point().row() as usize,
-            start_row + (viewport_height / self.line_height(font_cache)).ceil() as usize,
-        );
-        let line_count = end_row - start_row + 1;
-
-        let mut layouts = Vec::with_capacity(line_count);
+        let mut layouts = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
-        for (buffer_row, soft_wrapped) in self
+        for (ix, (buffer_row, soft_wrapped)) in self
             .display_snapshot
-            .buffer_rows(start_row as u32)
-            .take(line_count)
+            .buffer_rows(rows.start)
+            .take((rows.end - rows.start) as usize)
+            .enumerate()
         {
+            let display_row = rows.start + ix as u32;
+            let color = if active_rows.contains(&display_row) {
+                theme.editor.line_number_active.0
+            } else {
+                theme.editor.line_number.0
+            };
             if soft_wrapped {
                 layouts.push(None);
             } else {
@@ -2379,7 +2386,7 @@ impl Snapshot {
                 layouts.push(Some(layout_cache.layout_str(
                     &line_number,
                     self.font_size,
-                    &[(line_number.len(), font_id, theme.editor.line_number.0)],
+                    &[(line_number.len(), font_id, color)],
                 )));
             }
         }
@@ -2482,6 +2489,14 @@ impl Snapshot {
             )],
         ))
     }
+
+    pub fn prev_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
+        self.display_snapshot.prev_row_boundary(point)
+    }
+
+    pub fn next_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
+        self.display_snapshot.next_row_boundary(point)
+    }
 }
 
 fn compute_scroll_position(
@@ -2792,7 +2807,13 @@ mod tests {
         let layouts = editor.update(cx, |editor, cx| {
             editor
                 .snapshot(cx)
-                .layout_line_numbers(1000.0, &font_cache, &layout_cache, &settings.borrow().theme)
+                .layout_line_numbers(
+                    0..6,
+                    &Default::default(),
+                    &font_cache,
+                    &layout_cache,
+                    &settings.borrow().theme,
+                )
                 .unwrap()
         });
         assert_eq!(layouts.len(), 6);

zed/src/editor/element.rs 🔗

@@ -14,7 +14,7 @@ use gpui::{
 };
 use json::json;
 use smallvec::SmallVec;
-use std::{cmp::Ordering, ops::Range};
+use std::{cmp::Ordering, collections::HashSet, ops::Range};
 use std::{
     cmp::{self},
     collections::HashMap,
@@ -387,6 +387,46 @@ impl Element for EditorElement {
             (autoscroll_horizontally, snapshot)
         });
 
+        let scroll_position = snapshot.scroll_position();
+        let start_row = scroll_position.y() as u32;
+        let scroll_top = scroll_position.y() * line_height;
+        let end_row = ((scroll_top + size.y()) / line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
+
+        let mut selections = HashMap::new();
+        let mut active_rows = HashSet::new();
+        self.update_view(cx.app, |view, cx| {
+            let replica_id = view.replica_id(cx);
+            for selection_set_id in view.active_selection_sets(cx).collect::<Vec<_>>() {
+                let mut set = Vec::new();
+                for selection in view.selections_in_range(
+                    selection_set_id,
+                    DisplayPoint::new(start_row, 0)..DisplayPoint::new(end_row, 0),
+                    cx,
+                ) {
+                    set.push(selection.clone());
+                    if selection_set_id.replica_id == replica_id {
+                        let mut selection_start;
+                        let mut selection_end;
+                        if selection.start < selection.end {
+                            selection_start = selection.start;
+                            selection_end = selection.end;
+                        } else {
+                            selection_start = selection.end;
+                            selection_end = selection.start;
+                        };
+                        selection_start = snapshot.prev_row_boundary(selection_start).0;
+                        selection_end = snapshot.next_row_boundary(selection_end).0;
+                        active_rows.extend(
+                            cmp::max(selection_start.row(), start_row)
+                                ..=cmp::min(selection_end.row(), end_row),
+                        );
+                    }
+                }
+
+                selections.insert(selection_set_id.replica_id, set);
+            }
+        });
+
         let line_number_layouts = if snapshot.gutter_visible {
             let settings = self
                 .view
@@ -396,7 +436,8 @@ impl Element for EditorElement {
                 .settings
                 .borrow();
             match snapshot.layout_line_numbers(
-                size.y(),
+                start_row..end_row,
+                &active_rows,
                 cx.font_cache,
                 cx.text_layout_cache,
                 &settings.theme,
@@ -411,11 +452,6 @@ impl Element for EditorElement {
             Vec::new()
         };
 
-        let scroll_position = snapshot.scroll_position();
-        let start_row = scroll_position.y() as u32;
-        let scroll_top = scroll_position.y() * line_height;
-        let end_row = ((scroll_top + size.y()) / line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
-
         let mut max_visible_line_width = 0.0;
         let line_layouts = match snapshot.layout_lines(start_row..end_row, font_cache, layout_cache)
         {
@@ -434,21 +470,6 @@ impl Element for EditorElement {
             }
         };
 
-        let mut selections = HashMap::new();
-        self.update_view(cx.app, |view, cx| {
-            for selection_set_id in view.active_selection_sets(cx).collect::<Vec<_>>() {
-                selections.insert(
-                    selection_set_id.replica_id,
-                    view.selections_in_range(
-                        selection_set_id,
-                        DisplayPoint::new(start_row, 0)..DisplayPoint::new(end_row, 0),
-                        cx,
-                    )
-                    .collect(),
-                );
-            }
-        });
-
         let mut layout = LayoutState {
             size,
             gutter_size,