Convert code folding to be in terms of buffer points instead of display points

Mikayla Maki and max created

Co-authored-by: max <max@zed.dev>

Change summary

crates/editor/src/display_map.rs          | 76 ++++++++++++++++--------
crates/editor/src/display_map/fold_map.rs | 20 +++--
crates/editor/src/display_map/tab_map.rs  |  2 
crates/editor/src/editor.rs               | 27 ++++----
crates/editor/src/element.rs              |  1 
5 files changed, 77 insertions(+), 49 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -4,7 +4,7 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
-use block_map::{BlockMap, BlockPoint};
+pub use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
@@ -249,7 +249,7 @@ pub struct DisplaySnapshot {
     folds_snapshot: fold_map::FoldSnapshot,
     tabs_snapshot: tab_map::TabSnapshot,
     wraps_snapshot: wrap_map::WrapSnapshot,
-    blocks_snapshot: block_map::BlockSnapshot,
+    pub blocks_snapshot: block_map::BlockSnapshot,
     text_highlights: TextHighlights,
     clip_at_line_ends: bool,
 }
@@ -544,11 +544,8 @@ impl DisplaySnapshot {
         self.folds_snapshot.intersects_fold(offset)
     }
 
-    pub fn is_line_folded(&self, display_row: u32) -> bool {
-        let block_point = BlockPoint(Point::new(display_row, 0));
-        let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
-        let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
-        self.folds_snapshot.is_line_folded(tab_point.row())
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        self.folds_snapshot.is_line_folded(buffer_row)
     }
 
     pub fn is_block_line(&self, display_row: u32) -> bool {
@@ -594,6 +591,27 @@ impl DisplaySnapshot {
         (indent, is_blank)
     }
 
+    pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
+        let (buffer, range) = self
+            .buffer_snapshot
+            .buffer_line_for_row(buffer_row)
+            .unwrap();
+        let chars = buffer.chars_at(Point::new(range.start.row, 0));
+
+        let mut is_blank = false;
+        let mut indent_size = 0;
+        for c in chars {
+            // TODO: Handle tab expansion here
+            if c == ' ' {
+                indent_size += 1;
+            } else {
+                is_blank = c == '\n';
+                break;
+            }
+        }
+        (indent_size, is_blank)
+    }
+
     pub fn line_len(&self, row: u32) -> u32 {
         self.blocks_snapshot.line_len(row)
     }
@@ -602,49 +620,55 @@ impl DisplaySnapshot {
         self.blocks_snapshot.longest_row()
     }
 
-    pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
-        if self.is_foldable(display_row) {
+    pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
+        if self.is_foldable(buffer_row) {
             Some(FoldStatus::Foldable)
-        } else if self.is_line_folded(display_row) {
+        } else if self.is_line_folded(buffer_row) {
             Some(FoldStatus::Folded)
         } else {
             None
         }
     }
 
-    pub fn is_foldable(self: &Self, row: u32) -> bool {
-        let max_point = self.max_point();
-        if row >= max_point.row() {
+    pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
+        let max_row = self.buffer_snapshot.max_buffer_row();
+        if buffer_row >= max_row {
             return false;
         }
 
-        let (start_indent, is_blank) = self.line_indent(row);
+        let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
         if is_blank {
             return false;
         }
 
-        for display_row in next_rows(row, self) {
-            let (indent, is_blank) = self.line_indent(display_row);
-            if !is_blank {
-                return indent > start_indent;
+        for next_row in (buffer_row + 1)..max_row {
+            let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
+            if next_indent_size > indent_size {
+                return true;
+            } else if !next_line_is_blank {
+                break;
             }
         }
 
-        return false;
+        false
     }
 
-    pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
-        let start = DisplayPoint::new(row, self.line_len(row));
+    pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
+        let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
 
-        if self.is_foldable(row) && !self.is_line_folded(start.row()) {
-            let (start_indent, _) = self.line_indent(row);
-            let max_point = self.max_point();
+        if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
+            let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
+            let max_point = self.buffer_snapshot.max_point();
             let mut end = None;
 
-            for row in next_rows(row, self) {
+            for row in (buffer_row + 1)..=max_point.row {
                 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)));
+                    let prev_row = row - 1;
+                    end = Some(Point::new(
+                        prev_row,
+                        self.buffer_snapshot.line_len(prev_row),
+                    ));
                     break;
                 }
             }

crates/editor/src/display_map/fold_map.rs 🔗

@@ -639,14 +639,14 @@ impl FoldSnapshot {
         cursor.item().map_or(false, |t| t.output_text.is_some())
     }
 
-    pub fn is_line_folded(&self, output_row: u32) -> bool {
-        let mut cursor = self.transforms.cursor::<FoldPoint>();
-        cursor.seek(&FoldPoint::new(output_row, 0), Bias::Right, &());
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        let mut cursor = self.transforms.cursor::<Point>();
+        cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
         while let Some(transform) = cursor.item() {
             if transform.output_text.is_some() {
                 return true;
             }
-            if cursor.end(&()).row() == output_row {
+            if cursor.end(&()).row == buffer_row {
                 cursor.next(&())
             } else {
                 break;
@@ -1214,6 +1214,7 @@ pub type FoldEdit = Edit<FoldOffset>;
 mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
+    use collections::HashSet;
     use rand::prelude::*;
     use settings::Settings;
     use std::{cmp::Reverse, env, mem, sync::Arc};
@@ -1593,10 +1594,13 @@ mod tests {
                 fold_row += 1;
             }
 
-            for fold_range in map.merged_fold_ranges() {
-                let fold_point =
-                    snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right);
-                assert!(snapshot.is_line_folded(fold_point.row()));
+            let fold_start_rows = map
+                .merged_fold_ranges()
+                .iter()
+                .map(|range| range.start.to_point(&buffer_snapshot).row)
+                .collect::<HashSet<_>>();
+            for row in 0..=buffer_snapshot.max_buffer_row() {
+                assert_eq!(snapshot.is_line_folded(row), fold_start_rows.contains(&row));
             }
 
             for _ in 0..5 {

crates/editor/src/display_map/tab_map.rs 🔗

@@ -262,7 +262,7 @@ impl TabSnapshot {
             .to_buffer_point(&self.fold_snapshot)
     }
 
-    fn expand_tabs(
+    pub fn expand_tabs(
         chars: impl Iterator<Item = char>,
         column: usize,
         tab_size: NonZeroU32,

crates/editor/src/editor.rs 🔗

@@ -40,6 +40,7 @@ use gpui::{
     keymap_matcher::KeymapContext,
     platform::CursorStyle,
     serde_json::json,
+    text_layout::Line,
     AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
@@ -2707,6 +2708,7 @@ impl Editor {
         &self,
         fold_data: Option<Vec<(u32, FoldStatus)>>,
         active_rows: &BTreeMap<u32, bool>,
+        line_layouts: &Vec<Option<Line>>,
         style: &EditorStyle,
         gutter_hovered: bool,
         line_height: f32,
@@ -2722,9 +2724,11 @@ impl Editor {
                 .iter()
                 .copied()
                 .filter_map(|(fold_location, fold_status)| {
-                    (gutter_hovered
-                        || fold_status == FoldStatus::Folded
-                        || !*active_rows.get(&fold_location).unwrap_or(&true))
+                    let has_line_number = line_layouts[fold_location as usize].is_some();
+                    (has_line_number
+                        && (gutter_hovered
+                            || fold_status == FoldStatus::Folded
+                            || !*active_rows.get(&fold_location).unwrap_or(&true)))
                     .then(|| {
                         (
                             fold_location,
@@ -5759,18 +5763,16 @@ impl Editor {
 
         let selections = self.selections.all::<Point>(cx);
         for selection in selections {
-            let range = selection.display_range(&display_map).sorted();
-            let buffer_start_row = range.start.to_point(&display_map).row;
+            let range = selection.range().sorted();
+            let buffer_start_row = range.start.row;
 
-            for row in (0..=range.end.row()).rev() {
-                let fold_range = display_map.foldable_range(row).map(|range| {
-                    range.start.to_point(&display_map)..range.end.to_point(&display_map)
-                });
+            for row in (0..=range.end.row).rev() {
+                let fold_range = display_map.foldable_range(row);
 
                 if let Some(fold_range) = fold_range {
                     if fold_range.end.row >= buffer_start_row {
                         fold_ranges.push(fold_range);
-                        if row <= range.start.row() {
+                        if row <= range.start.row {
                             break;
                         }
                     }
@@ -5791,10 +5793,7 @@ impl Editor {
                 .selections
                 .all::<Point>(cx)
                 .iter()
-                .any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
-
-            let fold_range =
-                fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
+                .any(|selection| fold_range.overlaps(&selection.range()));
 
             self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
         }

crates/editor/src/element.rs 🔗

@@ -1898,6 +1898,7 @@ impl Element for EditorElement {
             view.render_fold_indicators(
                 folds,
                 &active_rows,
+                &line_number_layouts,
                 &style,
                 view.gutter_hovered,
                 line_height,