Add relative_line_mode

Conrad Irwin and joseph@zed.dev created

Co-Authored-By: joseph@zed.dev

Change summary

assets/settings/default.json         |  1 
crates/editor/src/editor_settings.rs |  2 
crates/editor/src/element.rs         | 81 +++++++++++++++++++++++++++--
3 files changed, 77 insertions(+), 7 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -98,6 +98,7 @@
     // Whether to show selections in the scrollbar.
     "selections": true
   },
+  "relative_line_numbers": false,
   // Inlay hint related settings
   "inlay_hints": {
     // Global switch to toggle hints on and off, switched off by default.

crates/editor/src/editor_settings.rs 🔗

@@ -9,6 +9,7 @@ pub struct EditorSettings {
     pub show_completions_on_input: bool,
     pub use_on_type_format: bool,
     pub scrollbar: Scrollbar,
+    pub relative_line_numbers: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -34,6 +35,7 @@ pub struct EditorSettingsContent {
     pub show_completions_on_input: Option<bool>,
     pub use_on_type_format: Option<bool>,
     pub scrollbar: Option<ScrollbarContent>,
+    pub relative_line_numbers: Option<bool>,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]

crates/editor/src/element.rs 🔗

@@ -1439,10 +1439,47 @@ impl EditorElement {
             .collect()
     }
 
+    fn calculate_relative_line_numbers(
+        &self,
+        rows: &Range<u32>,
+        buffer_rows: &Vec<Option<u32>>,
+        relative_to: Option<u32>,
+    ) -> HashMap<usize, u32> {
+        let mut relative_rows: HashMap<usize, u32> = Default::default();
+        if let Some(relative_to) = relative_to {
+            let head_idx = (relative_to - rows.start) as usize;
+            let mut delta = 1;
+            let mut i = head_idx + 1;
+            while i < buffer_rows.len() {
+                if buffer_rows[i].is_some() {
+                    relative_rows.insert(i, delta);
+                    delta += 1;
+                }
+                i += 1;
+            }
+            delta = 1;
+            i = head_idx;
+            while i > 0 && buffer_rows[i].is_none() {
+                i -= 1;
+            }
+
+            while i > 0 {
+                i -= 1;
+                if buffer_rows[i].is_some() {
+                    relative_rows.insert(i, delta);
+                    delta += 1;
+                }
+            }
+        }
+
+        relative_rows
+    }
+
     fn layout_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
+        newest_selection_head: Option<DisplayPoint>,
         is_singleton: bool,
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
@@ -1455,21 +1492,33 @@ impl EditorElement {
         let mut line_number_layouts = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
-        for (ix, row) in snapshot
+        let is_relative = settings::get::<EditorSettings>(cx).relative_line_numbers;
+        let relative_to = if is_relative {
+            newest_selection_head.map(|head| head.row())
+        } else {
+            None
+        };
+
+        let buffer_rows = snapshot
             .buffer_rows(rows.start)
             .take((rows.end - rows.start) as usize)
-            .enumerate()
-        {
+            .collect::<Vec<_>>();
+
+        let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to);
+
+        for (ix, row) in buffer_rows.iter().enumerate() {
             let display_row = rows.start + ix as u32;
             let (active, color) = if active_rows.contains_key(&display_row) {
                 (true, style.line_number_active)
             } else {
                 (false, style.line_number)
             };
-            if let Some(buffer_row) = row {
+            if let Some(buffer_row) = *row {
                 if include_line_numbers {
                     line_number.clear();
-                    write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+                    let default_number = buffer_row + 1;
+                    let number = relative_rows.get(&ix).unwrap_or(&default_number);
+                    write!(&mut line_number, "{}", number).unwrap();
                     line_number_layouts.push(Some(cx.text_layout_cache().layout_str(
                         &line_number,
                         style.text.font_size,
@@ -2296,6 +2345,7 @@ impl Element<Editor> for EditorElement {
         let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
             start_row..end_row,
             &active_rows,
+            newest_selection_head,
             is_singleton,
             &snapshot,
             cx,
@@ -3054,7 +3104,6 @@ mod tests {
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
-
         let editor = cx
             .add_window(|cx| {
                 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
@@ -3066,10 +3115,28 @@ mod tests {
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);
             element
-                .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx)
+                .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx)
                 .0
         });
         assert_eq!(layouts.len(), 6);
+
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+
+            let rows = 0..6;
+            let buffer_rows = snapshot
+                .buffer_rows(rows.start)
+                .take((rows.end - rows.start) as usize)
+                .collect::<Vec<_>>();
+
+            element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3))
+        });
+        assert_eq!(relative_rows[&0], 3);
+        assert_eq!(relative_rows[&1], 2);
+        assert_eq!(relative_rows[&2], 1);
+        // current line has no relative number
+        assert_eq!(relative_rows[&4], 1);
+        assert_eq!(relative_rows[&5], 2);
     }
 
     #[gpui::test]