Start work on WrapMap::chunks_at

Max Brunsfeld and Nathan Sobo created

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

Change summary

gpui/src/platform/mac/fonts.rs         |  18 +-
zed/src/editor/buffer.rs               |  20 +-
zed/src/editor/display_map/fold_map.rs |  10 
zed/src/editor/display_map/tab_map.rs  |  10 +
zed/src/editor/display_map/wrap_map.rs | 160 +++++++++++++++++++++++++--
zed/src/sum_tree/cursor.rs             |   2 
6 files changed, 175 insertions(+), 45 deletions(-)

Detailed changes

gpui/src/platform/mac/fonts.rs 🔗

@@ -316,6 +316,9 @@ impl FontSystemState {
                     width as f64,
                 ) as usize;
                 ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
+                if ix_converter.utf8_ix >= text.len() {
+                    break;
+                }
                 break_indices.push(ix_converter.utf8_ix as usize);
             }
             break_indices
@@ -485,22 +488,15 @@ mod tests {
         let font_ids = fonts.load_family("Helvetica").unwrap();
         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
 
-        let line = "one two three four five";
+        let line = "one two three four five\n";
         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
-        assert_eq!(
-            wrap_boundaries,
-            &["one two ".len(), "one two three ".len(), line.len()]
-        );
+        assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
 
-        let line = "aaa ααα ✋✋✋ 🎉🎉🎉";
+        let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
         assert_eq!(
             wrap_boundaries,
-            &[
-                "aaa ααα ".len(),
-                "aaa ααα ✋✋✋ ".len(),
-                "aaa ααα ✋✋✋ 🎉🎉🎉".len(),
-            ]
+            &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
         );
     }
 }

zed/src/editor/buffer.rs 🔗

@@ -1416,7 +1416,7 @@ impl Buffer {
 
         let mut fragment_start = old_fragments.sum_start().offset();
         for range in ranges {
-            let fragment_end = old_fragments.end(&cx).offset();
+            let fragment_end = old_fragments.sum_end(&cx).offset();
 
             // If the current fragment ends before this range, then jump ahead to the first fragment
             // that extends past the start of this range, reusing any intervening fragments.
@@ -1441,7 +1441,7 @@ impl Buffer {
             }
 
             // If we are at the end of a non-concurrent fragment, advance to the next one.
-            let fragment_end = old_fragments.end(&cx).offset();
+            let fragment_end = old_fragments.sum_end(&cx).offset();
             if fragment_end == range.start && fragment_end > fragment_start {
                 let mut fragment = old_fragments.item().unwrap().clone();
                 fragment.len = fragment_end - fragment_start;
@@ -1495,7 +1495,7 @@ impl Buffer {
             // portions as deleted.
             while fragment_start < range.end {
                 let fragment = old_fragments.item().unwrap();
-                let fragment_end = old_fragments.end(&cx).offset();
+                let fragment_end = old_fragments.sum_end(&cx).offset();
                 let mut intersection = fragment.clone();
                 let intersection_end = cmp::min(range.end, fragment_end);
                 if fragment.was_visible(version, &self.undo_map) {
@@ -1517,7 +1517,7 @@ impl Buffer {
         // If the current fragment has been partially consumed, then consume the rest of it
         // and advance to the next fragment before slicing.
         if fragment_start > old_fragments.sum_start().offset() {
-            let fragment_end = old_fragments.end(&cx).offset();
+            let fragment_end = old_fragments.sum_end(&cx).offset();
             if fragment_end > fragment_start {
                 let mut suffix = old_fragments.item().unwrap().clone();
                 suffix.len = fragment_end - fragment_start;
@@ -1644,7 +1644,7 @@ impl Buffer {
         new_ropes.push_tree(new_fragments.summary().text);
 
         for range in &undo.ranges {
-            let mut end_offset = old_fragments.end(&cx).offset();
+            let mut end_offset = old_fragments.sum_end(&cx).offset();
 
             if end_offset < range.start {
                 let preceding_fragments =
@@ -1668,7 +1668,7 @@ impl Buffer {
                     new_fragments.push(fragment, &None);
 
                     old_fragments.next(&cx);
-                    if end_offset == old_fragments.end(&cx).offset() {
+                    if end_offset == old_fragments.sum_end(&cx).offset() {
                         let unseen_fragments = old_fragments.slice(
                             &VersionedOffset::Offset(end_offset),
                             Bias::Right,
@@ -1677,7 +1677,7 @@ impl Buffer {
                         new_ropes.push_tree(unseen_fragments.summary().text);
                         new_fragments.push_tree(unseen_fragments, &None);
                     }
-                    end_offset = old_fragments.end(&cx).offset();
+                    end_offset = old_fragments.sum_end(&cx).offset();
                 } else {
                     break;
                 }
@@ -1757,7 +1757,7 @@ impl Buffer {
 
         let mut fragment_start = old_fragments.sum_start().visible;
         for range in ranges {
-            let fragment_end = old_fragments.end(&None).visible;
+            let fragment_end = old_fragments.sum_end(&None).visible;
 
             // If the current fragment ends before this range, then jump ahead to the first fragment
             // that extends past the start of this range, reusing any intervening fragments.
@@ -1810,7 +1810,7 @@ impl Buffer {
             // portions as deleted.
             while fragment_start < range.end {
                 let fragment = old_fragments.item().unwrap();
-                let fragment_end = old_fragments.end(&None).visible;
+                let fragment_end = old_fragments.sum_end(&None).visible;
                 let mut intersection = fragment.clone();
                 let intersection_end = cmp::min(range.end, fragment_end);
                 if fragment.visible {
@@ -1835,7 +1835,7 @@ impl Buffer {
         // If the current fragment has been partially consumed, then consume the rest of it
         // and advance to the next fragment before slicing.
         if fragment_start > old_fragments.sum_start().visible {
-            let fragment_end = old_fragments.end(&None).visible;
+            let fragment_end = old_fragments.sum_end(&None).visible;
             if fragment_end > fragment_start {
                 let mut suffix = old_fragments.item().unwrap().clone();
                 suffix.len = fragment_end - fragment_start;

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

@@ -634,7 +634,7 @@ impl Snapshot {
         let overshoot = point - cursor.seek_start();
         OutputPoint(cmp::min(
             cursor.sum_start().0 + overshoot,
-            cursor.end(&()).0,
+            cursor.sum_end(&()).0,
         ))
     }
 
@@ -936,7 +936,7 @@ impl<'a> Iterator for Chunks<'a> {
             self.input_offset += transform.summary.input.bytes;
             self.input_chunks.seek(self.input_offset);
 
-            while self.input_offset >= self.transform_cursor.end(&())
+            while self.input_offset >= self.transform_cursor.sum_end(&())
                 && self.transform_cursor.item().is_some()
             {
                 self.transform_cursor.next(&());
@@ -951,7 +951,7 @@ impl<'a> Iterator for Chunks<'a> {
             chunk = &chunk[offset_in_chunk..];
 
             // Truncate the chunk so that it ends at the next fold.
-            let region_end = self.transform_cursor.end(&()) - self.input_offset;
+            let region_end = self.transform_cursor.sum_end(&()) - self.input_offset;
             if chunk.len() >= region_end {
                 chunk = &chunk[0..region_end];
                 self.transform_cursor.next(&());
@@ -991,7 +991,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             self.input_offset += transform.summary.input.bytes;
             self.input_chunks.seek(self.input_offset);
 
-            while self.input_offset >= self.transform_cursor.end(&())
+            while self.input_offset >= self.transform_cursor.sum_end(&())
                 && self.transform_cursor.item().is_some()
             {
                 self.transform_cursor.next(&());
@@ -1015,7 +1015,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
             chunk = &chunk[offset_in_chunk..];
 
             // Truncate the chunk so that it ends at the next fold.
-            let region_end = self.transform_cursor.end(&()) - self.input_offset;
+            let region_end = self.transform_cursor.sum_end(&()) - self.input_offset;
             if chunk.len() >= region_end {
                 chunk = &chunk[0..region_end];
                 self.transform_cursor.next(&());

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

@@ -7,7 +7,7 @@ use super::fold_map::{
 use crate::{editor::rope, settings::StyleId, util::Bias};
 use std::{
     mem,
-    ops::{AddAssign, Range},
+    ops::{Add, AddAssign, Range},
 };
 
 pub struct TabMap(Mutex<Snapshot>);
@@ -266,6 +266,14 @@ impl AddAssign<Self> for OutputPoint {
     }
 }
 
+impl Add<Self> for OutputPoint {
+    type Output = OutputPoint;
+
+    fn add(self, other: Self) -> Self::Output {
+        Self(self.0 + other.0)
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Edit {
     pub old_lines: Range<OutputPoint>,

zed/src/editor/display_map/wrap_map.rs 🔗

@@ -1,16 +1,19 @@
 use super::tab_map::{
-    Edit as InputEdit, OutputPoint as InputPoint, Snapshot as InputSnapshot, TextSummary,
+    self, Edit as InputEdit, OutputPoint as InputPoint, Snapshot as InputSnapshot, TextSummary,
 };
 use crate::{
     editor::Point,
-    sum_tree::{self, SumTree},
+    sum_tree::{self, Cursor, SumTree},
     util::Bias,
 };
 use gpui::{font_cache::FamilyId, AppContext, FontCache, FontSystem, Task};
 use parking_lot::Mutex;
 use postage::{prelude::Sink, watch};
 use smol::channel;
-use std::{ops::Range, sync::Arc};
+use std::{
+    ops::{AddAssign, Range, Sub},
+    sync::Arc,
+};
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct OutputPoint(super::Point);
@@ -41,6 +44,20 @@ impl OutputPoint {
     }
 }
 
+impl AddAssign<Self> for OutputPoint {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += &rhs.0;
+    }
+}
+
+impl Sub<Self> for OutputPoint {
+    type Output = OutputPoint;
+
+    fn sub(self, other: Self) -> Self::Output {
+        Self(self.0 - other.0)
+    }
+}
+
 #[derive(Clone)]
 pub struct Snapshot {
     transforms: SumTree<Transform>,
@@ -65,6 +82,65 @@ impl Snapshot {
             input,
         }
     }
+
+    pub fn chunks_at(&self, point: OutputPoint) -> Chunks {
+        let mut transforms = self.transforms.cursor();
+        transforms.seek(&point, Bias::Right, &());
+        let input_position =
+            *transforms.sum_start() + InputPoint((point - *transforms.seek_start()).0);
+        let input_chunks = self.input.chunks_at(input_position);
+        Chunks {
+            input_chunks,
+            transforms,
+            input_position,
+            input_chunk: "",
+        }
+    }
+}
+
+pub struct Chunks<'a> {
+    input_chunks: tab_map::Chunks<'a>,
+    input_chunk: &'a str,
+    input_position: InputPoint,
+    transforms: Cursor<'a, Transform, OutputPoint, InputPoint>,
+}
+
+impl<'a> Iterator for Chunks<'a> {
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let transform = self.transforms.item()?;
+        if let Some(display_text) = transform.display_text {
+            self.transforms.next(&());
+            return Some(display_text);
+        }
+
+        if self.input_chunk.is_empty() {
+            self.input_chunk = self.input_chunks.next().unwrap();
+        }
+
+        let mut input_len = 0;
+        let transform_end = self.transforms.sum_end(&());
+        for c in self.input_chunk.chars() {
+            let char_len = c.len_utf8();
+            input_len += char_len;
+            if c == '\n' {
+                *self.input_position.row_mut() += 1;
+                *self.input_position.column_mut() = 0;
+            } else {
+                *self.input_position.column_mut() += char_len as u32;
+            }
+
+            if self.input_position >= transform_end {
+                self.transforms.next(&());
+                break;
+            }
+        }
+
+        let (prefix, suffix) = self.input_chunk.split_at(input_len);
+        self.input_chunk = suffix;
+        Some(prefix)
+    }
 }
 
 struct State {
@@ -149,7 +225,7 @@ impl BackgroundWrapper {
         mut snapshots_tx: watch::Sender<Snapshot>,
     ) {
         let edit = InputEdit {
-            old_lines: Default::default()..Default::default(),
+            old_lines: Default::default()..snapshot.max_point(),
             new_lines: Default::default()..snapshot.max_point(),
         };
         self.sync(snapshot, vec![edit]);
@@ -214,6 +290,8 @@ impl BackgroundWrapper {
                 'outer: for chunk in new_snapshot.chunks_at(InputPoint::new(row, 0)) {
                     for (ix, line_chunk) in chunk.split('\n').enumerate() {
                         if ix > 0 {
+                            line.push('\n');
+
                             let mut prev_boundary_ix = 0;
                             for boundary_ix in self
                                 .font_system
@@ -226,6 +304,15 @@ impl BackgroundWrapper {
                                 prev_boundary_ix = boundary_ix;
                             }
 
+                            if prev_boundary_ix < line.len() {
+                                new_transforms.push(
+                                    Transform::isomorphic(TextSummary::from(
+                                        &line[prev_boundary_ix..],
+                                    )),
+                                    &(),
+                                );
+                            }
+
                             line.clear();
                             row += 1;
                             if row == edit.new_rows.end {
@@ -238,14 +325,17 @@ impl BackgroundWrapper {
                 }
 
                 old_cursor.seek_forward(&InputPoint::new(edit.old_rows.end, 0), Bias::Right, &());
+                if old_cursor.seek_end(&()).row() > edit.old_rows.end {
+                    new_transforms.push(
+                        Transform::isomorphic(self.snapshot.input.text_summary_for_rows(
+                            edit.old_rows.end..old_cursor.seek_end(&()).row(),
+                        )),
+                        &(),
+                    );
+                }
+
                 if let Some(next_edit) = edits.peek() {
                     if next_edit.old_rows.start > old_cursor.seek_end(&()).row() {
-                        new_transforms.push(
-                            Transform::isomorphic(self.snapshot.input.text_summary_for_rows(
-                                edit.old_rows.end..old_cursor.seek_end(&()).row(),
-                            )),
-                            &(),
-                        );
                         old_cursor.next(&());
                         new_transforms.push_tree(
                             old_cursor.slice(
@@ -257,12 +347,6 @@ impl BackgroundWrapper {
                         );
                     }
                 } else {
-                    new_transforms.push(
-                        Transform::isomorphic(self.snapshot.input.text_summary_for_rows(
-                            edit.old_rows.end..old_cursor.seek_end(&()).row(),
-                        )),
-                        &(),
-                    );
                     old_cursor.next(&());
                     new_transforms.push_tree(old_cursor.suffix(&()), &());
                 }
@@ -282,6 +366,10 @@ struct Transform {
 
 impl Transform {
     fn isomorphic(summary: TextSummary) -> Self {
+        if summary.lines.is_zero() {
+            panic!("wtf");
+        }
+
         Self {
             summary: TransformSummary {
                 input: summary.clone(),
@@ -337,6 +425,12 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for InputPoint {
     }
 }
 
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for OutputPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += OutputPoint(summary.output.lines);
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -347,9 +441,40 @@ mod tests {
         },
         util::RandomCharIter,
     };
+    use futures::StreamExt;
     use rand::prelude::*;
     use std::env;
 
+    #[gpui::test]
+    async fn test_simple_wraps(mut cx: gpui::TestAppContext) {
+        let text = "one two three four five six\n";
+        let font_cache = cx.font_cache().clone();
+        let config = Config {
+            wrap_width: 64.,
+            font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
+            font_size: 14.0,
+        };
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
+        let mut wrap_map = cx.read(|cx| {
+            let fold_map = FoldMap::new(buffer.clone(), cx);
+            let (folds_snapshot, edits) = fold_map.read(cx);
+            let tab_map = TabMap::new(folds_snapshot.clone(), 4);
+            let (tabs_snapshot, _) = tab_map.sync(folds_snapshot, edits);
+            WrapMap::new(tabs_snapshot, config, cx)
+        });
+
+        wrap_map.background_snapshots.next().await;
+        let snapshot = wrap_map.background_snapshots.next().await.unwrap();
+
+        assert_eq!(
+            snapshot
+                .chunks_at(OutputPoint(Point::new(0, 3)))
+                .collect::<String>(),
+            " two \nthree four \nfive six\n"
+        );
+    }
+
     #[gpui::test]
     fn test_random_wraps(cx: &mut gpui::MutableAppContext) {
         let iterations = env::var("ITERATIONS")
@@ -370,7 +495,7 @@ mod tests {
             let mut rng = StdRng::seed_from_u64(seed);
 
             let buffer = cx.add_model(|cx| {
-                let len = rng.gen_range(0..10);
+                let len = rng.gen_range(0..32);
                 let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
                 Buffer::new(0, text, cx)
             });
@@ -409,6 +534,7 @@ mod tests {
                     prev_ix = ix;
                 }
             }
+
             dbg!(expected_text);
         }
     }

zed/src/sum_tree/cursor.rs 🔗

@@ -63,7 +63,7 @@ where
         &self.sum_dimension
     }
 
-    pub fn end(&self, cx: &<T::Summary as Summary>::Context) -> U {
+    pub fn sum_end(&self, cx: &<T::Summary as Summary>::Context) -> U {
         if let Some(item_summary) = self.item_summary() {
             let mut end = self.sum_start().clone();
             end.add_summary(item_summary, cx);