Align block text with the anchor's column

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/display_map/block_map.rs | 180 ++++++++++++++++++++---
crates/editor/src/display_map/wrap_map.rs  |   4 
2 files changed, 158 insertions(+), 26 deletions(-)

Detailed changes

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

@@ -8,7 +8,7 @@ use std::{
     collections::HashSet,
     fmt::Debug,
     iter,
-    ops::Range,
+    ops::{Deref, Range},
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
@@ -74,7 +74,13 @@ pub enum BlockDisposition {
 #[derive(Clone, Debug)]
 struct Transform {
     summary: TransformSummary,
-    block: Option<Arc<Block>>,
+    block: Option<AlignedBlock>,
+}
+
+#[derive(Clone, Debug)]
+struct AlignedBlock {
+    block: Arc<Block>,
+    column: u32,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -99,6 +105,8 @@ struct BlockChunks<'a> {
     chunks: rope::Chunks<'a>,
     runs: iter::Peekable<vec::IntoIter<(usize, HighlightStyle)>>,
     chunk: Option<&'a str>,
+    remaining_padding: u32,
+    padding_column: u32,
     run_start: usize,
     offset: usize,
 }
@@ -271,6 +279,7 @@ impl BlockMap {
                     .iter()
                     .map(|block| {
                         let mut position = block.position.to_point(buffer);
+                        let column = wrap_snapshot.from_point(position, Bias::Left).column();
                         match block.disposition {
                             BlockDisposition::Above => position.column = 0,
                             BlockDisposition::Below => {
@@ -278,21 +287,22 @@ impl BlockMap {
                             }
                         }
                         let position = wrap_snapshot.from_point(position, Bias::Left);
-                        (position.row(), block)
+                        (position.row(), column, block)
                     }),
             );
-            blocks_in_edit.sort_unstable_by_key(|(row, block)| (*row, block.disposition, block.id));
+            blocks_in_edit
+                .sort_unstable_by_key(|(row, _, block)| (*row, block.disposition, block.id));
 
             // For each of these blocks, insert a new isomorphic transform preceding the block,
             // and then insert the block itself.
-            for (block_row, block) in blocks_in_edit.iter().copied() {
+            for (block_row, column, block) in blocks_in_edit.iter().copied() {
                 let insertion_row = match block.disposition {
                     BlockDisposition::Above => block_row,
                     BlockDisposition::Below => block_row + 1,
                 };
                 let extent_before_block = insertion_row - new_transforms.summary().input_rows;
                 push_isomorphic(&mut new_transforms, extent_before_block);
-                new_transforms.push(Transform::block(block.clone()), &());
+                new_transforms.push(Transform::block(block.clone(), column), &());
             }
 
             old_end = WrapRow(old_end.0.min(old_row_count));
@@ -345,7 +355,7 @@ impl BlockPoint {
     }
 }
 
-impl std::ops::Deref for BlockPoint {
+impl Deref for BlockPoint {
     type Target = Point;
 
     fn deref(&self) -> &Self::Target {
@@ -555,7 +565,11 @@ impl BlockSnapshot {
             let (output_start, input_start) = cursor.start();
             let overshoot = row - output_start.0;
             if let Some(block) = &transform.block {
-                block.text.line_len(overshoot)
+                let mut len = block.text.line_len(overshoot);
+                if len > 0 {
+                    len += block.column;
+                }
+                len
             } else {
                 self.wrap_snapshot.line_len(input_start.0 + overshoot)
             }
@@ -665,16 +679,16 @@ impl Transform {
         }
     }
 
-    fn block(block: Arc<Block>) -> Self {
+    fn block(block: Arc<Block>, column: u32) -> Self {
         let text_summary = block.text.summary();
         Self {
             summary: TransformSummary {
                 input_rows: 0,
                 output_rows: text_summary.lines.row + 1,
                 longest_row_in_block: text_summary.longest_row,
-                longest_row_in_block_chars: text_summary.longest_row_chars,
+                longest_row_in_block_chars: column + text_summary.longest_row_chars,
             },
-            block: Some(block),
+            block: Some(AlignedBlock { block, column }),
         }
     }
 
@@ -759,7 +773,7 @@ impl<'a> Iterator for Chunks<'a> {
 }
 
 impl<'a> BlockChunks<'a> {
-    fn new(block: &'a Block, rows: Range<u32>, cx: Option<&'a AppContext>) -> Self {
+    fn new(block: &'a AlignedBlock, rows: Range<u32>, cx: Option<&'a AppContext>) -> Self {
         let offset_range = block.text.point_to_offset(Point::new(rows.start, 0))
             ..block.text.point_to_offset(Point::new(rows.end, 0));
 
@@ -785,6 +799,8 @@ impl<'a> BlockChunks<'a> {
         Self {
             chunk: None,
             run_start,
+            padding_column: block.column,
+            remaining_padding: block.column,
             chunks: block.text.chunks_in_range(offset_range.clone()),
             runs,
             offset: offset_range.start,
@@ -801,7 +817,27 @@ impl<'a> Iterator for BlockChunks<'a> {
         }
 
         let chunk = self.chunk?;
-        let mut chunk_len = chunk.len();
+
+        if chunk.starts_with('\n') {
+            self.remaining_padding = 0;
+        }
+
+        if self.remaining_padding > 0 {
+            const PADDING: &'static str = "                ";
+            let padding_len = self.remaining_padding.min(PADDING.len() as u32);
+            self.remaining_padding -= padding_len;
+            return Some(Chunk {
+                text: &PADDING[..padding_len as usize],
+                ..Default::default()
+            });
+        }
+
+        let mut chunk_len = if let Some(ix) = chunk.find('\n') {
+            ix + 1
+        } else {
+            chunk.len()
+        };
+
         let mut highlight_style = None;
         if let Some((run_len, style)) = self.runs.peek() {
             highlight_style = Some(style.clone());
@@ -815,6 +851,11 @@ impl<'a> Iterator for BlockChunks<'a> {
 
         self.offset += chunk_len;
         let (chunk, suffix) = chunk.split_at(chunk_len);
+
+        if chunk.ends_with('\n') {
+            self.remaining_padding = self.padding_column;
+        }
+
         self.chunk = if suffix.is_empty() {
             None
         } else {
@@ -892,6 +933,14 @@ impl BlockDisposition {
     }
 }
 
+impl Deref for AlignedBlock {
+    type Target = Block;
+
+    fn deref(&self) -> &Self::Target {
+        self.block.as_ref()
+    }
+}
+
 impl Debug for Block {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("Block")
@@ -926,6 +975,7 @@ mod tests {
     use super::*;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
     use buffer::RandomCharIter;
+    use gpui::color::Color;
     use language::Buffer;
     use rand::prelude::*;
     use std::env;
@@ -944,6 +994,75 @@ mod tests {
         assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
     }
 
+    #[gpui::test]
+    fn test_block_chunks(cx: &mut gpui::MutableAppContext) {
+        let red = Color::red();
+        let blue = Color::blue();
+        let clear = Color::default();
+
+        let block = AlignedBlock {
+            column: 5,
+            block: Arc::new(Block {
+                id: BlockId(0),
+                position: Anchor::min(),
+                text: "one!\ntwo three\nfour".into(),
+                build_runs: Some(Arc::new(move |_| {
+                    vec![(3, red.into()), (6, Default::default()), (5, blue.into())]
+                })),
+                disposition: BlockDisposition::Above,
+            }),
+        };
+
+        assert_eq!(
+            colored_chunks(&block, 0..3, cx),
+            &[
+                ("     ", clear),
+                ("one", red),
+                ("!\n", clear),
+                ("     ", clear),
+                ("two ", clear),
+                ("three", blue),
+                ("\n", clear),
+                ("     ", clear),
+                ("four", clear)
+            ]
+        );
+        assert_eq!(
+            colored_chunks(&block, 0..1, cx),
+            &[
+                ("     ", clear), //
+                ("one", red),
+                ("!\n", clear),
+            ]
+        );
+        assert_eq!(
+            colored_chunks(&block, 1..3, cx),
+            &[
+                ("     ", clear),
+                ("two ", clear),
+                ("three", blue),
+                ("\n", clear),
+                ("     ", clear),
+                ("four", clear)
+            ]
+        );
+
+        fn colored_chunks<'a>(
+            block: &'a AlignedBlock,
+            row_range: Range<u32>,
+            cx: &'a AppContext,
+        ) -> Vec<(&'a str, Color)> {
+            BlockChunks::new(block, row_range, Some(cx))
+                .map(|c| {
+                    (
+                        c.text,
+                        c.highlight_style.map_or(Color::default(), |s| s.color),
+                    )
+                })
+                .collect()
+        }
+    }
+
     #[gpui::test]
     fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
@@ -988,7 +1107,7 @@ mod tests {
         let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
         assert_eq!(
             snapshot.text(),
-            "aaa\nBLOCK 1\nBLOCK 2\nbbb\nccc\nddd\nBLOCK 3"
+            "aaa\nBLOCK 1\n  BLOCK 2\nbbb\nccc\nddd\n  BLOCK 3"
         );
         assert_eq!(
             snapshot.to_block_point(WrapPoint::new(0, 3)),
@@ -1080,7 +1199,7 @@ mod tests {
         let mut snapshot = block_map.read(wraps_snapshot, wrap_edits, cx);
         assert_eq!(
             snapshot.text(),
-            "aaa\nBLOCK 1\nb!!!\nBLOCK 2\nbb\nccc\nddd\nBLOCK 3"
+            "aaa\nBLOCK 1\nb!!!\n BLOCK 2\nbb\nccc\nddd\n  BLOCK 3"
         );
     }
 
@@ -1124,7 +1243,7 @@ mod tests {
         let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
         assert_eq!(
             snapshot.text(),
-            "one two \nthree\n<BLOCK 1\nfour five \nsix\n>BLOCK 2\nseven \neight"
+            "one two \nthree\n  <BLOCK 1\nfour five \nsix\n >BLOCK 2\nseven \neight"
         );
     }
 
@@ -1267,6 +1386,7 @@ mod tests {
                 .cloned()
                 .map(|(id, block)| {
                     let mut position = block.position.to_point(buffer);
+                    let column = wraps_snapshot.from_point(position, Bias::Left).column();
                     match block.disposition {
                         BlockDisposition::Above => {
                             position.column = 0;
@@ -1279,7 +1399,7 @@ mod tests {
                     (
                         id,
                         BlockProperties {
-                            position: row,
+                            position: BlockPoint::new(row, column),
                             text: block.text,
                             build_runs: block.build_runs.clone(),
                             disposition: block.disposition,
@@ -1288,7 +1408,7 @@ mod tests {
                 })
                 .collect::<Vec<_>>();
             sorted_blocks
-                .sort_unstable_by_key(|(id, block)| (block.position, block.disposition, *id));
+                .sort_unstable_by_key(|(id, block)| (block.position.row, block.disposition, *id));
             let mut sorted_blocks = sorted_blocks.into_iter().peekable();
 
             let mut expected_buffer_rows = Vec::new();
@@ -1305,11 +1425,15 @@ mod tests {
                     .row;
 
                 while let Some((_, block)) = sorted_blocks.peek() {
-                    if block.position == row && block.disposition == BlockDisposition::Above {
+                    if block.position.row == row && block.disposition == BlockDisposition::Above {
                         let text = block.text.to_string();
-                        expected_text.push_str(&text);
-                        expected_text.push('\n');
-                        for _ in text.split('\n') {
+                        let padding = " ".repeat(block.position.column as usize);
+                        for line in text.split('\n') {
+                            if !line.is_empty() {
+                                expected_text.push_str(&padding);
+                                expected_text.push_str(line);
+                            }
+                            expected_text.push('\n');
                             expected_buffer_rows.push(None);
                         }
                         sorted_blocks.next();
@@ -1323,11 +1447,15 @@ mod tests {
                 expected_text.push_str(input_line);
 
                 while let Some((_, block)) = sorted_blocks.peek() {
-                    if block.position == row && block.disposition == BlockDisposition::Below {
+                    if block.position.row == row && block.disposition == BlockDisposition::Below {
                         let text = block.text.to_string();
-                        expected_text.push('\n');
-                        expected_text.push_str(&text);
-                        for _ in text.split('\n') {
+                        let padding = " ".repeat(block.position.column as usize);
+                        for line in text.split('\n') {
+                            expected_text.push('\n');
+                            if !line.is_empty() {
+                                expected_text.push_str(&padding);
+                                expected_text.push_str(line);
+                            }
                             expected_buffer_rows.push(None);
                         }
                         sorted_blocks.next();