Added DisplayRow abstraction to make folding code more readable

Mikayla Maki created

Change summary

crates/editor/src/display_map.rs | 119 ++++++++++++++++++++++++---------
crates/editor/src/editor.rs      |   6 
crates/vim/src/normal.rs         |   8 +
3 files changed, 95 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -576,10 +576,10 @@ impl DisplaySnapshot {
         result
     }
 
-    pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
+    pub fn line_indent(&self, display_row: DisplayRow) -> (u32, bool) {
         let mut indent = 0;
         let mut is_blank = true;
-        for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for (c, _) in self.chars_at(display_row.start(self)) {
             if c == ' ' {
                 indent += 1;
             } else {
@@ -599,7 +599,8 @@ impl DisplaySnapshot {
     }
 
     pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
-        if self.is_line_foldable(display_row) {
+        let display_row_typed = DisplayRow::new(display_row);
+        if self.is_foldable(display_row_typed) {
             Some(FoldStatus::Foldable)
         } else if self.is_line_folded(display_row) {
             Some(FoldStatus::Folded)
@@ -608,44 +609,43 @@ impl DisplaySnapshot {
         }
     }
 
-    pub fn is_line_foldable(self: &Self, display_row: u32) -> bool {
-        let max_point = self.max_point();
-        if display_row >= max_point.row() {
-            false
-        } else {
-            let (start_indent, is_blank) = self.line_indent(display_row);
+    pub fn is_foldable(self: &Self, row: DisplayRow) -> bool {
+        if let Some(next_lines) = row.next_rows(self) {
+            let (start_indent, is_blank) = self.line_indent(row);
             if is_blank {
-                false
-            } else {
-                for display_row in display_row + 1..=max_point.row() {
-                    let (indent, is_blank) = self.line_indent(display_row);
-                    if !is_blank {
-                        return indent > start_indent;
-                    }
+                return false;
+            }
+
+            for display_row in next_lines {
+                let (indent, is_blank) = self.line_indent(display_row);
+                if !is_blank {
+                    return indent > start_indent;
                 }
-                false
             }
         }
+        return false;
     }
 
-    pub fn foldable_range_for_line(self: &Self, start_row: u32) -> Option<Range<Point>> {
-        if self.is_line_foldable(start_row) && !self.is_line_folded(start_row) {
+    pub fn foldable_range(self: &Self, row: DisplayRow) -> Option<Range<Point>> {
+        let start = row.end(&self);
+
+        if self.is_foldable(row) && !self.is_line_folded(start.row()) {
+            let (start_indent, _) = self.line_indent(row);
             let max_point = self.max_point();
-            let (start_indent, _) = self.line_indent(start_row);
-            let start = DisplayPoint::new(start_row, self.line_len(start_row));
             let mut end = None;
-            for row in start_row + 1..=max_point.row() {
+
+            for row in row.next_rows(self).unwrap() {
                 let (indent, is_blank) = self.line_indent(row);
                 if !is_blank && indent <= start_indent {
-                    end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
+                    end = row.previous_row(self);
                     break;
                 }
             }
 
-            let end = end.unwrap_or(max_point);
+            let end = end.map(|end_row| end_row.end(self)).unwrap_or(max_point);
             Some(start.to_point(self)..end.to_point(self))
         } else {
-            return None;
+            None
         }
     }
 
@@ -665,19 +665,74 @@ pub struct DisplayPoint(BlockPoint);
 #[repr(transparent)]
 pub struct DisplayRow(pub u32);
 
+// TODO: Move display_map check into new, and then remove it from everywhere else
 impl DisplayRow {
     pub fn new(display_row: u32) -> Self {
         DisplayRow(display_row)
     }
 
-    pub fn to_span(self, display_map: &DisplaySnapshot) -> Range<DisplayPoint> {
-        let row_start = DisplayPoint::new(self.0, 0);
-        let row_end = DisplayPoint::new(
-            self.0,
-            display_map.buffer_snapshot.line_len(row_start.row()),
-        );
+    pub fn to_line_span(&self, display_map: &DisplaySnapshot) -> Range<DisplayPoint> {
+        self.start(display_map)..self.end(&display_map)
+    }
+
+    pub fn previous_row(&self, _display_map: &DisplaySnapshot) -> Option<DisplayRow> {
+        self.0.checked_sub(1).map(|prev_row| DisplayRow(prev_row))
+    }
+
+    pub fn next_row(&self, display_map: &DisplaySnapshot) -> Option<DisplayRow> {
+        if self.0 + 1 > display_map.max_point().row() {
+            None
+        } else {
+            Some(DisplayRow(self.0 + 1))
+        }
+    }
+
+    // TODO: Remove and use next_row
+    fn next_unchecked(&self) -> Option<DisplayRow> {
+        Some(DisplayRow(self.0 + 1))
+    }
+
+    pub fn next_rows(
+        &self,
+        display_map: &DisplaySnapshot,
+    ) -> Option<impl Iterator<Item = DisplayRow>> {
+        self.next_row(display_map)
+            .and_then(|next_row| next_row.span_to(display_map.max_point()))
+    }
+
+    pub fn span_to<I: Into<DisplayRow>>(
+        &self,
+        end_row: I,
+    ) -> Option<impl Iterator<Item = DisplayRow>> {
+        let end_row = end_row.into();
+        if self.0 <= end_row.0 {
+            let start = *self;
+            let mut current = None;
+
+            Some(std::iter::from_fn(move || {
+                if current == None {
+                    current = Some(start);
+                } else {
+                    current = current.unwrap().next_unchecked();
+                }
+                if current.unwrap().0 > end_row.0 {
+                    None
+                } else {
+                    current
+                }
+            }))
+        } else {
+            None
+        }
+    }
+
+    // Force users to think about the display map when using this type
+    pub fn start(&self, _display_map: &DisplaySnapshot) -> DisplayPoint {
+        DisplayPoint::new(self.0, 0)
+    }
 
-        row_start..row_end
+    pub fn end(&self, display_map: &DisplaySnapshot) -> DisplayPoint {
+        DisplayPoint::new(self.0, display_map.line_len(self.0))
     }
 }
 

crates/editor/src/editor.rs 🔗

@@ -5715,7 +5715,7 @@ impl Editor {
             let buffer_start_row = range.start.to_point(&display_map).row;
 
             for row in (0..=range.end.row()).rev() {
-                if let Some(fold_range) = display_map.foldable_range_for_line(row) {
+                if let Some(fold_range) = display_map.foldable_range(DisplayRow::new(row)) {
                     if fold_range.end.row >= buffer_start_row {
                         fold_ranges.push(fold_range);
                         if row <= range.start.row() {
@@ -5734,7 +5734,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        if let Some(fold_range) = display_map.foldable_range_for_line(display_row.0) {
+        if let Some(fold_range) = display_map.foldable_range(display_row) {
             self.fold_ranges(std::iter::once(fold_range), true, cx);
         }
     }
@@ -5762,7 +5762,7 @@ impl Editor {
 
         let display_range = fold_at
             .display_row
-            .to_span(&display_map)
+            .to_line_span(&display_map)
             .map_endpoints(|endpoint| endpoint.to_point(&display_map));
 
         self.unfold_ranges(std::iter::once(display_range), true, true, cx)

crates/vim/src/normal.rs 🔗

@@ -12,7 +12,7 @@ use crate::{
 };
 use collections::{HashMap, HashSet};
 use editor::{
-    display_map::ToDisplayPoint,
+    display_map::{DisplayRow, ToDisplayPoint},
     scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
     Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
 };
@@ -195,7 +195,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                     .map(|selection| selection.start.row())
                     .collect();
                 let edits = selection_start_rows.into_iter().map(|row| {
-                    let (indent, _) = map.line_indent(row);
+                    let (indent, _) = map.line_indent(DisplayRow::new(row));
                     let start_of_line = map
                         .clip_point(DisplayPoint::new(row, 0), Bias::Left)
                         .to_point(&map);
@@ -216,6 +216,8 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
     });
 }
 
+// TODO: FIGURE OUT WHY PANIC WHEN CLICKING ON FOLDS
+
 fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.switch_mode(Mode::Insert, false, cx);
@@ -227,7 +229,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                     .map(|selection| selection.end.row())
                     .collect();
                 let edits = selection_end_rows.into_iter().map(|row| {
-                    let (indent, _) = map.line_indent(row);
+                    let (indent, _) = map.line_indent(DisplayRow::new(row));
                     let end_of_line = map
                         .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
                         .to_point(&map);