Make block insertion work in simple cases

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/editor/src/display_map.rs           |   1 
crates/editor/src/display_map/block_map.rs | 259 +++++++++++++++++++++--
2 files changed, 229 insertions(+), 31 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -55,6 +55,7 @@ impl DisplayMap {
         let (wraps_snapshot, _) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
+
         DisplayMapSnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(),
             folds_snapshot,

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

@@ -1,13 +1,26 @@
 use super::wrap_map::{self, Edit as WrapEdit, Snapshot as WrapSnapshot, WrapPoint};
-use buffer::{rope, Anchor, Bias, Point, Rope, ToOffset};
-use gpui::fonts::HighlightStyle;
-use language::HighlightedChunk;
+use buffer::{rope, Anchor, Bias, Edit, Point, Rope, ToOffset, ToPoint as _};
+use gpui::{fonts::HighlightStyle, AppContext, ModelHandle};
+use language::{Buffer, HighlightedChunk};
 use parking_lot::Mutex;
-use std::{cmp, collections::HashSet, iter, ops::Range, slice, sync::Arc};
+use std::{
+    cmp,
+    collections::HashSet,
+    iter,
+    ops::Range,
+    slice,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
 use sum_tree::SumTree;
 
 struct BlockMap {
-    blocks: Vec<(BlockId, Arc<Block>)>,
+    buffer: ModelHandle<Buffer>,
+    next_block_id: AtomicUsize,
+    wrap_snapshot: Mutex<WrapSnapshot>,
+    blocks: Vec<Arc<Block>>,
     transforms: Mutex<SumTree<Transform>>,
 }
 
@@ -24,6 +37,7 @@ struct BlockId(usize);
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct BlockPoint(super::Point);
 
+#[derive(Debug)]
 struct Block {
     id: BlockId,
     position: Anchor,
@@ -44,13 +58,13 @@ where
     disposition: BlockDisposition,
 }
 
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 enum BlockDisposition {
     Above,
     Below,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 struct Transform {
     summary: TransformSummary,
     block: Option<Arc<Block>>,
@@ -86,34 +100,53 @@ struct InputRow(u32);
 struct OutputRow(u32);
 
 impl BlockMap {
-    fn new(wrap_snapshot: WrapSnapshot) -> Self {
+    fn new(buffer: ModelHandle<Buffer>, wrap_snapshot: WrapSnapshot) -> Self {
         Self {
+            buffer,
+            next_block_id: AtomicUsize::new(0),
             blocks: Vec::new(),
             transforms: Mutex::new(SumTree::from_item(
                 Transform::isomorphic(wrap_snapshot.max_point().row() + 1),
                 &(),
             )),
+            wrap_snapshot: Mutex::new(wrap_snapshot),
         }
     }
 
-    fn read(&self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockSnapshot {
-        self.sync(&wrap_snapshot, edits);
+    fn read(
+        &self,
+        wrap_snapshot: WrapSnapshot,
+        edits: Vec<WrapEdit>,
+        cx: &AppContext,
+    ) -> BlockSnapshot {
+        self.apply_edits(&wrap_snapshot, edits, cx);
+        *self.wrap_snapshot.lock() = wrap_snapshot.clone();
         BlockSnapshot {
             wrap_snapshot,
             transforms: self.transforms.lock().clone(),
         }
     }
 
-    fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockMapWriter {
-        self.sync(&wrap_snapshot, edits);
+    fn write(
+        &mut self,
+        wrap_snapshot: WrapSnapshot,
+        edits: Vec<WrapEdit>,
+        cx: &AppContext,
+    ) -> BlockMapWriter {
+        self.apply_edits(&wrap_snapshot, edits, cx);
+        *self.wrap_snapshot.lock() = wrap_snapshot;
         BlockMapWriter(self)
     }
 
-    fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>) {
+    fn apply_edits(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>, cx: &AppContext) {
+        let buffer = self.buffer.read(cx);
         let mut transforms = self.transforms.lock();
         let mut new_transforms = SumTree::new();
         let mut cursor = transforms.cursor::<InputRow>();
         let mut edits = edits.into_iter().peekable();
+        let mut last_block_ix = 0;
+        let mut blocks_in_edit = Vec::new();
+
         while let Some(mut edit) = edits.next() {
             new_transforms.push_tree(
                 cursor.slice(&InputRow(edit.old.start), Bias::Left, &()),
@@ -147,7 +180,45 @@ impl BlockMap {
                 }
             }
 
-            // TODO: process injections
+            let start_anchor = buffer.anchor_before(Point::new(edit.new.start, 0));
+            let end_anchor = buffer.anchor_after(Point::new(edit.new.end, 0));
+            let start_block_ix = match self.blocks[last_block_ix..]
+                .binary_search_by(|probe| probe.position.cmp(&start_anchor, buffer).unwrap())
+            {
+                Ok(ix) | Err(ix) => last_block_ix + ix,
+            };
+            let end_block_ix = match self.blocks[start_block_ix..]
+                .binary_search_by(|probe| probe.position.cmp(&end_anchor, buffer).unwrap())
+            {
+                Ok(ix) | Err(ix) => start_block_ix + ix,
+            };
+            last_block_ix = end_block_ix;
+
+            blocks_in_edit.clear();
+            blocks_in_edit.extend(
+                self.blocks[start_block_ix..end_block_ix]
+                    .iter()
+                    .map(|block| (block.position.to_point(buffer).row, block)),
+            );
+            blocks_in_edit.sort_unstable_by_key(|(row, block)| (*row, block.disposition));
+
+            for (row, block) in blocks_in_edit.iter().copied() {
+                let insertion_row = if block.disposition.is_above() {
+                    row
+                } else {
+                    row + 1
+                };
+
+                let new_transforms_end = new_transforms.summary().input_rows;
+                if new_transforms_end < insertion_row {
+                    new_transforms.push(
+                        Transform::isomorphic(insertion_row - new_transforms_end),
+                        &(),
+                    );
+                }
+
+                new_transforms.push(Transform::block(block.clone()), &());
+            }
 
             let new_transforms_end = new_transforms.summary().input_rows;
             if new_transforms_end < edit.new.end {
@@ -183,14 +254,58 @@ impl BlockPoint {
 
 impl<'a> BlockMapWriter<'a> {
     pub fn insert<P, T>(
-        &self,
+        &mut self,
         blocks: impl IntoIterator<Item = BlockProperties<P, T>>,
+        cx: &AppContext,
     ) -> Vec<BlockId>
     where
         P: ToOffset + Clone,
         T: Into<Rope> + Clone,
     {
-        vec![]
+        let buffer = self.0.buffer.read(cx);
+        let mut ids = Vec::new();
+        let mut edits = Vec::<Edit<u32>>::new();
+
+        for block in blocks {
+            let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
+            ids.push(id);
+
+            let position = buffer.anchor_before(block.position);
+            let row = position.to_point(buffer).row;
+
+            let block_ix = match self
+                .0
+                .blocks
+                .binary_search_by(|probe| probe.position.cmp(&position, buffer).unwrap())
+            {
+                Ok(ix) | Err(ix) => ix,
+            };
+            let mut text = block.text.into();
+            text.push("\n");
+            self.0.blocks.insert(
+                block_ix,
+                Arc::new(Block {
+                    id,
+                    position,
+                    text,
+                    runs: block.runs,
+                    disposition: block.disposition,
+                }),
+            );
+
+            if let Err(edit_ix) = edits.binary_search_by_key(&row, |edit| edit.old.start) {
+                edits.insert(
+                    edit_ix,
+                    Edit {
+                        old: row..(row + 1),
+                        new: row..(row + 1),
+                    },
+                );
+            }
+        }
+
+        self.0.apply_edits(&*self.0.wrap_snapshot.lock(), edits, cx);
+        ids
     }
 
     pub fn remove(&self, ids: HashSet<BlockId>) {
@@ -269,6 +384,16 @@ impl Transform {
         }
     }
 
+    fn block(block: Arc<Block>) -> Self {
+        Self {
+            summary: TransformSummary {
+                input_rows: 0,
+                output_rows: block.text.summary().lines.row,
+            },
+            block: Some(block),
+        }
+    }
+
     fn is_isomorphic(&self) -> bool {
         self.block.is_none()
     }
@@ -283,7 +408,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
         }
 
         if let Some(block_chunks) = self.block_chunks.as_mut() {
-            if let Some(mut block_chunk) = block_chunks.next() {
+            if let Some(block_chunk) = block_chunks.next() {
                 self.output_row += block_chunk.text.matches('\n').count() as u32;
                 return Some(block_chunk);
             } else {
@@ -306,7 +431,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
         }
 
         if self.input_chunk.text.is_empty() {
-            self.input_chunk = self.input_chunks.next().unwrap();
+            self.input_chunk = self.input_chunks.next()?;
         }
 
         let transform_end = self.transforms.end(&()).0 .0;
@@ -314,8 +439,11 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             offset_for_row(self.input_chunk.text, transform_end - self.output_row);
         self.output_row += prefix_rows;
         let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
-
         self.input_chunk.text = suffix;
+        if self.output_row == transform_end {
+            self.transforms.next(&());
+        }
+
         Some(HighlightedChunk {
             text: prefix,
             ..self.input_chunk
@@ -417,20 +545,25 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for OutputRow {
     }
 }
 
+impl BlockDisposition {
+    fn is_above(&self) -> bool {
+        matches!(self, BlockDisposition::Above)
+    }
+}
+
 // Count the number of bytes prior to a target row.
 // If the string doesn't contain the target row, return the total number of rows it does contain.
 // Otherwise return the target row itself.
 fn offset_for_row(s: &str, target_row: u32) -> (u32, usize) {
-    assert!(target_row > 0);
     let mut row = 0;
     let mut offset = 0;
     for (ix, line) in s.split('\n').enumerate() {
         if ix > 0 {
             row += 1;
             offset += 1;
-            if row as u32 >= target_row {
-                break;
-            }
+        }
+        if row as u32 >= target_row {
+            break;
         }
         offset += line.len();
     }
@@ -441,13 +574,78 @@ fn offset_for_row(s: &str, target_row: u32) -> (u32, usize) {
 mod tests {
     use super::*;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
-    use buffer::{RandomCharIter, ToPoint as _};
+    use buffer::RandomCharIter;
     use language::Buffer;
     use rand::prelude::*;
     use std::env;
 
+    #[gpui::test]
+    fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+
+        let text = "aaa\nbbb\nccc\nddd\n";
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
+        let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
+        let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
+        writer.insert(
+            vec![
+                BlockProperties {
+                    position: Point::new(1, 0),
+                    text: "BLOCK 1",
+                    disposition: BlockDisposition::Above,
+                    runs: vec![],
+                },
+                BlockProperties {
+                    position: Point::new(1, 2),
+                    text: "BLOCK 2",
+                    disposition: BlockDisposition::Above,
+                    runs: vec![],
+                },
+                BlockProperties {
+                    position: Point::new(3, 2),
+                    text: "BLOCK 3",
+                    disposition: BlockDisposition::Below,
+                    runs: vec![],
+                },
+            ],
+            cx,
+        );
+
+        let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
+        assert_eq!(
+            snapshot.text(),
+            "aaa\nBLOCK 1\nBLOCK 2\nbbb\nccc\nddd\nBLOCK 3\n"
+        );
+
+        // Insert a line break, separating two block decorations into separate
+        // lines.
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([Point::new(1, 1)..Point::new(1, 1)], "!!!\n", cx)
+        });
+
+        let (folds_snapshot, fold_edits) = fold_map.read(cx);
+        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+        let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+            wrap_map.sync(tabs_snapshot, tab_edits, cx)
+        });
+        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\n"
+        );
+    }
+
     #[gpui::test(iterations = 100)]
-    fn test_random(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+    fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -461,7 +659,6 @@ mod tests {
             .unwrap();
         let font_size = 14.0;
 
-        log::info!("Tab size: {}", tab_size);
         log::info!("Wrap width: {:?}", wrap_width);
 
         let buffer = cx.add_model(|cx| {
@@ -473,7 +670,7 @@ mod tests {
         let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
         let (wrap_map, wraps_snapshot) =
             WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
-        let mut block_map = BlockMap::new(wraps_snapshot);
+        let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot);
         let mut expected_blocks = Vec::new();
 
         for _ in 0..operations {
@@ -519,11 +716,11 @@ mod tests {
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
-                    let block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
 
                     expected_blocks.extend(
                         block_map
-                            .insert(block_properties.clone())
+                            .insert(block_properties.clone(), cx)
                             .into_iter()
                             .zip(block_properties),
                     );
@@ -543,7 +740,7 @@ mod tests {
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
-                    let block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    let block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
 
                     block_map.remove(block_ids_to_remove);
                 }
@@ -557,7 +754,7 @@ mod tests {
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                 wrap_map.sync(tabs_snapshot, tab_edits, cx)
             });
-            let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+            let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits, cx);
             assert_eq!(
                 blocks_snapshot.transforms.summary().input_rows,
                 wraps_snapshot.max_point().row() + 1