Allow multibuffer to clip to the ends of excerpts, before trailing newlines

Max Brunsfeld created

Change summary

crates/editor/src/multi_buffer.rs | 145 ++++++++++++++++++++------------
1 file changed, 89 insertions(+), 56 deletions(-)

Detailed changes

crates/editor/src/multi_buffer.rs 🔗

@@ -4,7 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
 use anyhow::Result;
 use clock::ReplicaId;
 use collections::{HashMap, HashSet};
-use gpui::{AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
+use gpui::{AppContext, ElementBox, Entity, ModelContext, ModelHandle, Task};
 use language::{
     Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Selection,
     ToOffset as _, ToPoint as _, TransactionId,
@@ -171,7 +171,7 @@ impl MultiBuffer {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn build_simple(text: &str, cx: &mut MutableAppContext) -> ModelHandle<Self> {
+    pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle<Self> {
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
         cx.add_model(|cx| Self::singleton(buffer, cx))
     }
@@ -180,7 +180,7 @@ impl MultiBuffer {
     pub fn build_random(
         excerpts: usize,
         mut rng: &mut impl rand::Rng,
-        cx: &mut MutableAppContext,
+        cx: &mut gpui::MutableAppContext,
     ) -> ModelHandle<Self> {
         use rand::prelude::*;
         use text::RandomCharIter;
@@ -604,7 +604,6 @@ impl MultiBuffer {
         snapshot.excerpts.update_last(
             |excerpt| {
                 excerpt.has_trailing_newline = true;
-                excerpt.text_summary += TextSummary::from("\n");
                 prev_id = Some(excerpt.id.clone());
             },
             &(),
@@ -852,10 +851,7 @@ impl MultiBufferSnapshot {
         cursor.seek(&offset, Bias::Left, &());
         let mut excerpt_chunks = cursor.item().map(|excerpt| {
             let start_after_header = cursor.start() + excerpt.header_height as usize;
-            let mut end_before_footer = cursor.start() + excerpt.text_summary.bytes;
-            if excerpt.has_trailing_newline {
-                end_before_footer -= 1;
-            }
+            let end_before_footer = cursor.start() + excerpt.text_summary.bytes;
 
             let start = excerpt.range.start.to_offset(&excerpt.buffer);
             let end =
@@ -942,6 +938,12 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let header_end = *cursor.start() + excerpt.header_height as usize;
             if offset < header_end {
+                if bias == Bias::Left {
+                    cursor.prev(&());
+                    if let Some(excerpt) = cursor.item() {
+                        return *cursor.start() + excerpt.text_summary.bytes;
+                    }
+                }
                 header_end
             } else {
                 let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
@@ -966,6 +968,12 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let header_end = *cursor.start() + Point::new(excerpt.header_height as u32, 0);
             if point < header_end {
+                if bias == Bias::Left {
+                    cursor.prev(&());
+                    if let Some(excerpt) = cursor.item() {
+                        return *cursor.start() + excerpt.text_summary.lines;
+                    }
+                }
                 header_end
             } else {
                 let excerpt_start = excerpt.range.start.to_point(&excerpt.buffer);
@@ -990,6 +998,12 @@ impl MultiBufferSnapshot {
         if let Some(excerpt) = cursor.item() {
             let header_end = *cursor.start() + PointUtf16::new(excerpt.header_height as u32, 0);
             if point < header_end {
+                if bias == Bias::Left {
+                    cursor.prev(&());
+                    if let Some(excerpt) = cursor.item() {
+                        return *cursor.start() + excerpt.text_summary.lines_utf16;
+                    }
+                }
                 header_end
             } else {
                 let excerpt_start = excerpt
@@ -1017,10 +1031,8 @@ impl MultiBufferSnapshot {
 
         let mut chunk = &[][..];
         let excerpt_bytes = if let Some(excerpt) = excerpts.item() {
-            let mut excerpt_bytes = excerpt.bytes_in_range(
-                range.start - excerpts.start()
-                    ..cmp::min(range.end - excerpts.start(), excerpt.text_summary.bytes),
-            );
+            let mut excerpt_bytes = excerpt
+                .bytes_in_range(range.start - excerpts.start()..range.end - excerpts.start());
             chunk = excerpt_bytes.next().unwrap_or(&[][..]);
             Some(excerpt_bytes)
         } else {
@@ -1611,15 +1623,6 @@ impl Excerpt {
             text_summary.bytes += header_height as usize;
             text_summary.longest_row += header_height as u32;
         }
-        if has_trailing_newline {
-            text_summary.last_line_chars = 0;
-            text_summary.lines.row += 1;
-            text_summary.lines.column = 0;
-            text_summary.lines_utf16.row += 1;
-            text_summary.lines_utf16.column = 0;
-            text_summary.bytes += 1;
-        }
-
         Excerpt {
             id,
             buffer_id,
@@ -1651,7 +1654,7 @@ impl Excerpt {
     ) -> ExcerptChunks<'a> {
         let content_start = self.range.start.to_offset(&self.buffer);
         let chunks_start = content_start + range.start.saturating_sub(self.header_height as usize);
-        let mut chunks_end = content_start
+        let chunks_end = content_start
             + cmp::min(range.end, self.text_summary.bytes)
                 .saturating_sub(self.header_height as usize);
 
@@ -1659,13 +1662,15 @@ impl Excerpt {
             (self.header_height as usize).saturating_sub(range.start),
             range.len(),
         );
-        let mut footer_height = 0;
-        if self.has_trailing_newline && range.end == self.text_summary.bytes {
-            chunks_end -= 1;
-            if !range.is_empty() {
-                footer_height = 1;
-            }
-        }
+
+        let footer_height = if self.has_trailing_newline
+            && range.start <= self.text_summary.bytes
+            && range.end > self.text_summary.bytes
+        {
+            1
+        } else {
+            0
+        };
 
         let content_chunks = self.buffer.chunks(chunks_start..chunks_end, theme);
 
@@ -1679,7 +1684,7 @@ impl Excerpt {
     fn bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
         let content_start = self.range.start.to_offset(&self.buffer);
         let bytes_start = content_start + range.start.saturating_sub(self.header_height as usize);
-        let mut bytes_end = content_start
+        let bytes_end = content_start
             + cmp::min(range.end, self.text_summary.bytes)
                 .saturating_sub(self.header_height as usize);
 
@@ -1687,13 +1692,15 @@ impl Excerpt {
             (self.header_height as usize).saturating_sub(range.start),
             range.len(),
         );
-        let mut footer_height = 0;
-        if self.has_trailing_newline && range.end == self.text_summary.bytes {
-            bytes_end -= 1;
-            if !range.is_empty() {
-                footer_height = 1;
-            }
-        }
+
+        let footer_height = if self.has_trailing_newline
+            && range.start <= self.text_summary.bytes
+            && range.end > self.text_summary.bytes
+        {
+            1
+        } else {
+            0
+        };
 
         let content_bytes = self.buffer.bytes_in_range(bytes_start..bytes_end);
 
@@ -1740,9 +1747,13 @@ impl sum_tree::Item for Excerpt {
     type Summary = ExcerptSummary;
 
     fn summary(&self) -> Self::Summary {
+        let mut text = self.text_summary.clone();
+        if self.has_trailing_newline {
+            text += TextSummary::from("\n");
+        }
         ExcerptSummary {
             excerpt_id: self.id.clone(),
-            text: self.text_summary.clone(),
+            text,
         }
     }
 }
@@ -1809,11 +1820,7 @@ impl<'a> MultiBufferChunks<'a> {
         self.excerpts.seek(&offset, Bias::Right, &());
         if let Some(excerpt) = self.excerpts.item() {
             self.excerpt_chunks = Some(excerpt.chunks_in_range(
-                self.range.start - self.excerpts.start()
-                    ..cmp::min(
-                        self.range.end - self.excerpts.start(),
-                        excerpt.text_summary.bytes,
-                    ),
+                self.range.start - self.excerpts.start()..self.range.end - self.excerpts.start(),
                 self.theme,
             ));
         } else {
@@ -1834,13 +1841,9 @@ impl<'a> Iterator for MultiBufferChunks<'a> {
         } else {
             self.excerpts.next(&());
             let excerpt = self.excerpts.item()?;
-            self.excerpt_chunks = Some(excerpt.chunks_in_range(
-                0..cmp::min(
-                    self.range.end - self.excerpts.start(),
-                    excerpt.text_summary.bytes,
-                ),
-                self.theme,
-            ));
+            self.excerpt_chunks = Some(
+                excerpt.chunks_in_range(0..self.range.end - self.excerpts.start(), self.theme),
+            );
             self.next()
         }
     }
@@ -1857,12 +1860,8 @@ impl<'a> MultiBufferBytes<'a> {
             } else {
                 self.excerpts.next(&());
                 if let Some(excerpt) = self.excerpts.item() {
-                    let mut excerpt_bytes = excerpt.bytes_in_range(
-                        0..cmp::min(
-                            self.range.end - self.excerpts.start(),
-                            excerpt.text_summary.bytes,
-                        ),
-                    );
+                    let mut excerpt_bytes =
+                        excerpt.bytes_in_range(0..self.range.end - self.excerpts.start());
                     self.chunk = excerpt_bytes.next().unwrap();
                     self.excerpt_bytes = Some(excerpt_bytes);
                 }
@@ -2149,6 +2148,40 @@ mod tests {
                 new: 8..9
             }]
         );
+
+        let multibuffer = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(
+            multibuffer.clip_point(Point::new(0, 0), Bias::Left),
+            Point::new(2, 0)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(0, 0), Bias::Right),
+            Point::new(2, 0)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(1, 0), Bias::Left),
+            Point::new(2, 0)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(1, 0), Bias::Right),
+            Point::new(2, 0)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(8, 0), Bias::Left),
+            Point::new(7, 4)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(8, 0), Bias::Right),
+            Point::new(11, 0)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(9, 0), Bias::Left),
+            Point::new(7, 4)
+        );
+        assert_eq!(
+            multibuffer.clip_point(Point::new(9, 0), Bias::Right),
+            Point::new(11, 0)
+        );
     }
 
     #[gpui::test]