WIP - Add excerpt headers as a built-in feature of BlockMap

Max Brunsfeld and Nathan Sobo created

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

Change summary

crates/editor/src/display_map/block_map.rs | 79 ++++++++++++++++++++---
crates/editor/src/editor.rs                |  1 
crates/editor/src/multi_buffer.rs          | 38 +++++++++++
3 files changed, 105 insertions(+), 13 deletions(-)

Detailed changes

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

@@ -2,12 +2,13 @@ use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
 use crate::{Anchor, ToPoint as _};
 use collections::{HashMap, HashSet};
 use gpui::{AppContext, ElementBox};
-use language::Chunk;
+use language::{BufferSnapshot, Chunk};
 use parking_lot::Mutex;
 use std::{
     cmp::{self, Ordering, Reverse},
     fmt::Debug,
     ops::{Deref, Range},
+    path::Path,
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
@@ -84,13 +85,44 @@ pub enum BlockDisposition {
 #[derive(Clone, Debug)]
 struct Transform {
     summary: TransformSummary,
-    block: Option<AlignedBlock>,
+    block: Option<TransformBlock>,
 }
 
-#[derive(Clone, Debug)]
-pub struct AlignedBlock {
-    block: Arc<Block>,
-    column: u32,
+#[derive(Clone)]
+enum TransformBlock {
+    Custom {
+        block: Arc<Block>,
+        column: u32,
+    },
+    ExcerptHeader {
+        buffer: BufferSnapshot,
+        range: Range<text::Anchor>,
+        path: Option<Arc<Path>>,
+    },
+}
+
+impl TransformBlock {
+    fn disposition(&self) -> BlockDisposition {
+        match self {
+            TransformBlock::Custom { block, column } => block.disposition,
+            TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+        }
+    }
+}
+
+impl Debug for TransformBlock {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Custom { block, column } => f
+                .debug_struct("Custom")
+                .field("block", block)
+                .field("column", column)
+                .finish(),
+            Self::ExcerptHeader { buffer, path, .. } => {
+                f.debug_struct("ExcerptHeader").field("path", path).finish()
+            }
+        }
+    }
 }
 
 #[derive(Clone, Debug, Default)]
@@ -244,12 +276,14 @@ impl BlockMap {
                 Ok(ix) | Err(ix) => last_block_ix + ix,
             };
 
+            let end_anchor;
             let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
+                end_anchor = Anchor::max();
                 self.blocks.len()
             } else {
                 let new_buffer_end =
                     wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
-                let end_anchor = buffer.anchor_before(new_buffer_end);
+                end_anchor = buffer.anchor_before(new_buffer_end);
                 match self.blocks[start_block_ix..].binary_search_by(|probe| {
                     probe
                         .position
@@ -276,25 +310,44 @@ impl BlockMap {
                             }
                         }
                         let position = wrap_snapshot.from_point(position, Bias::Left);
-                        (position.row(), column, block.clone())
+                        (
+                            position.row(),
+                            TransformBlock::Custom {
+                                block: block.clone(),
+                                column,
+                            },
+                        )
+                    }),
+            );
+            blocks_in_edit.extend(
+                buffer
+                    .excerpt_boundaries_in_range(start_anchor..end_anchor)
+                    .map(|excerpt_boundary| {
+                        (
+                            excerpt_boundary.row,
+                            TransformBlock::ExcerptHeader {
+                                buffer: excerpt_boundary.buffer,
+                                range: excerpt_boundary.range,
+                                path: excerpt_boundary.path,
+                            },
+                        )
                     }),
             );
 
             // When multiple blocks are on the same row, newer blocks appear above older
             // blocks. This is arbitrary, but we currently rely on it in ProjectDiagnosticsEditor.
-            blocks_in_edit
-                .sort_by_key(|(row, _, block)| (*row, block.disposition, Reverse(block.id)));
+            blocks_in_edit.sort();
 
             // For each of these blocks, insert a new isomorphic transform preceding the block,
             // and then insert the block itself.
-            for (block_row, column, block) in blocks_in_edit.drain(..) {
-                let insertion_row = match block.disposition {
+            for (block_row, block) in blocks_in_edit.drain(..) {
+                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, column), &());
+                new_transforms.push(Transform::block(block), &());
             }
 
             old_end = WrapRow(old_end.0.min(old_row_count));

crates/editor/src/editor.rs 🔗

@@ -3,6 +3,7 @@ mod element;
 pub mod items;
 pub mod movement;
 mod multi_buffer;
+mod multi_editor;
 
 #[cfg(test)]
 mod test;

crates/editor/src/multi_buffer.rs 🔗

@@ -15,6 +15,7 @@ use std::{
     cmp, fmt, io,
     iter::{self, FromIterator},
     ops::{Range, Sub},
+    path::Path,
     str,
     sync::Arc,
     time::{Duration, Instant},
@@ -101,6 +102,14 @@ pub struct ExcerptProperties<'a, T> {
     pub range: Range<T>,
 }
 
+pub struct ExcerptBoundary {
+    pub row: u32,
+    pub buffer: BufferSnapshot,
+    pub path: Option<Arc<Path>>,
+    pub range: Range<text::Anchor>,
+    pub starts_new_buffer: bool,
+}
+
 #[derive(Clone)]
 struct Excerpt {
     id: ExcerptId,
@@ -1769,6 +1778,24 @@ impl MultiBufferSnapshot {
         start_id != end_id
     }
 
+    pub fn excerpt_boundaries_in_range<'a, T: ToOffset>(
+        &'a self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = ExcerptBoundary> + 'a {
+        let start = range.start.to_offset(self);
+        let end = range.end.to_offset(self);
+        let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
+        cursor.seek(&start, Bias::Right, &());
+
+        let prev_buffer_id = cursor.prev_item().map(|excerpt| excerpt.buffer_id);
+
+        std::iter::from_fn(move || {
+            let excerpt = cursor.item()?;
+            let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
+            todo!()
+        })
+    }
+
     pub fn parse_count(&self) -> usize {
         self.parse_count
     }
@@ -2628,6 +2655,17 @@ mod tests {
         assert!(!snapshot.range_contains_excerpt_boundary(Point::new(4, 0)..Point::new(4, 2)));
         assert!(!snapshot.range_contains_excerpt_boundary(Point::new(4, 2)..Point::new(4, 2)));
 
+        assert_eq!(
+            snapshot
+                .excerpt_boundaries_in_range(Point::new(0, 0)..Point::new(4, 2))
+                .collect::<Vec<_>>(),
+            &[
+                (Some(buffer_1.clone()), true),
+                (Some(buffer_1.clone()), false),
+                (Some(buffer_2.clone()), false),
+            ]
+        );
+
         buffer_1.update(cx, |buffer, cx| {
             buffer.edit(
                 [