Merge pull request #61 from zed-industries/ropes-2

Max Brunsfeld created

Store buffer's visible and deleted text using ropes

Change summary

zed/src/editor/buffer/mod.rs           | 490 ++++++++++++++-------------
zed/src/editor/buffer/rope.rs          | 477 +++++++++++++++++++++++++++
zed/src/editor/buffer/text.rs          | 461 --------------------------
zed/src/editor/display_map/fold_map.rs |  16 
zed/src/editor/display_map/mod.rs      |   2 
zed/src/sum_tree/mod.rs                |  40 ++
zed/src/worktree.rs                    |  15 
7 files changed, 794 insertions(+), 707 deletions(-)

Detailed changes

zed/src/editor/buffer/mod.rs 🔗

@@ -1,18 +1,18 @@
 mod anchor;
 mod point;
+pub mod rope;
 mod selection;
-mod text;
 
 pub use anchor::*;
 pub use point::*;
+pub use rope::{Rope, TextSummary};
 use seahash::SeaHasher;
 pub use selection::*;
 use similar::{ChangeTag, TextDiff};
-pub use text::*;
 
 use crate::{
     operation_queue::{self, OperationQueue},
-    sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree},
+    sum_tree::{self, FilterCursor, SeekBias, SumTree},
     time::{self, ReplicaId},
     util::RandomCharIter,
     worktree::FileHandle,
@@ -25,7 +25,8 @@ use std::{
     cmp,
     hash::BuildHasher,
     iter::{self, Iterator},
-    ops::{AddAssign, Range},
+    mem,
+    ops::Range,
     str,
     sync::Arc,
     time::{Duration, Instant, SystemTime, UNIX_EPOCH},
@@ -58,6 +59,8 @@ type HashSet<T> = std::collections::HashSet<T>;
 
 pub struct Buffer {
     fragments: SumTree<Fragment>,
+    visible_text: Rope,
+    deleted_text: Rope,
     insertion_splits: HashMap<time::Local, SumTree<InsertionSplit>>,
     pub version: time::Global,
     saved_version: time::Global,
@@ -75,10 +78,6 @@ pub struct Buffer {
     lamport_clock: time::Lamport,
 }
 
-pub struct Snapshot {
-    fragments: SumTree<Fragment>,
-}
-
 #[derive(Clone)]
 struct Transaction {
     start: time::Global,
@@ -92,6 +91,7 @@ struct Transaction {
 
 #[derive(Clone)]
 pub struct History {
+    // TODO: Turn this into a String or Rope, maybe.
     pub base_text: Arc<str>,
     ops: HashMap<time::Local, EditOperation>,
     undo_stack: Vec<Transaction>,
@@ -238,18 +238,6 @@ impl UndoMap {
     }
 }
 
-#[derive(Clone)]
-pub struct CharIter<'a> {
-    fragments_cursor: Cursor<'a, Fragment, usize, usize>,
-    fragment_chars: str::Chars<'a>,
-}
-
-#[derive(Clone)]
-pub struct FragmentIter<'a> {
-    cursor: Cursor<'a, Fragment, usize, usize>,
-    started: bool,
-}
-
 struct Edits<'a, F: Fn(&FragmentSummary) -> bool> {
     cursor: FilterCursor<'a, F, Fragment, usize>,
     undos: &'a UndoMap,
@@ -285,15 +273,14 @@ pub struct Insertion {
     id: time::Local,
     parent_id: time::Local,
     offset_in_parent: usize,
-    text: Text,
     lamport_timestamp: time::Lamport,
 }
 
 #[derive(Eq, PartialEq, Clone, Debug)]
 struct Fragment {
     id: FragmentId,
-    insertion: Insertion,
-    text: Text,
+    insertion: Arc<Insertion>,
+    range_in_insertion: Range<usize>,
     deletions: HashSet<time::Local>,
     max_undos: time::Global,
     visible: bool,
@@ -301,15 +288,22 @@ struct Fragment {
 
 #[derive(Eq, PartialEq, Clone, Debug)]
 pub struct FragmentSummary {
-    text_summary: TextSummary,
+    text: FragmentTextSummary,
     max_fragment_id: FragmentId,
     max_version: time::Global,
 }
 
-#[derive(Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
-struct FragmentExtent {
-    chars: usize,
-    lines: Point,
+#[derive(Default, Clone, Debug, PartialEq, Eq)]
+struct FragmentTextSummary {
+    visible: usize,
+    deleted: usize,
+}
+
+impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary {
+    fn add_summary(&mut self, summary: &'a FragmentSummary) {
+        self.visible += summary.text.visible;
+        self.deleted += summary.text.deleted;
+    }
 }
 
 #[derive(Eq, PartialEq, Clone, Debug)]
@@ -348,7 +342,7 @@ pub struct EditOperation {
     end_id: time::Local,
     end_offset: usize,
     version_in_range: time::Global,
-    new_text: Option<Text>,
+    new_text: Option<String>,
 }
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -418,16 +412,17 @@ impl Buffer {
             saved_mtime = UNIX_EPOCH;
         }
 
+        let mut visible_text = Rope::new();
         let mut insertion_splits = HashMap::default();
         let mut fragments = SumTree::new();
 
-        let base_insertion = Insertion {
+        let base_text = Rope::from(history.base_text.as_ref());
+        let base_insertion = Arc::new(Insertion {
             id: time::Local::default(),
             parent_id: time::Local::default(),
             offset_in_parent: 0,
-            text: history.base_text.clone().into(),
             lamport_timestamp: time::Lamport::default(),
-        };
+        });
 
         insertion_splits.insert(
             base_insertion.id,
@@ -440,42 +435,36 @@ impl Buffer {
             ),
         );
         fragments.push(
-            Fragment {
-                id: FragmentId::min_value().clone(),
-                insertion: base_insertion.clone(),
-                text: base_insertion.text.slice(0..0),
-                deletions: Default::default(),
-                max_undos: Default::default(),
-                visible: true,
-            },
+            Fragment::new(
+                FragmentId::min_value().clone(),
+                base_insertion.clone(),
+                0..0,
+            ),
             &(),
         );
 
-        if base_insertion.text.len() > 0 {
+        if base_text.len() > 0 {
             let base_fragment_id =
                 FragmentId::between(&FragmentId::min_value(), &FragmentId::max_value());
+            let range_in_insertion = 0..base_text.len();
 
+            visible_text = base_text.clone();
             insertion_splits.get_mut(&base_insertion.id).unwrap().push(
                 InsertionSplit {
                     fragment_id: base_fragment_id.clone(),
-                    extent: base_insertion.text.len(),
+                    extent: range_in_insertion.end,
                 },
                 &(),
             );
             fragments.push(
-                Fragment {
-                    id: base_fragment_id,
-                    text: base_insertion.text.clone(),
-                    insertion: base_insertion,
-                    deletions: Default::default(),
-                    max_undos: Default::default(),
-                    visible: true,
-                },
+                Fragment::new(base_fragment_id, base_insertion, range_in_insertion.clone()),
                 &(),
             );
         }
 
         Self {
+            visible_text,
+            deleted_text: Rope::new(),
             fragments,
             insertion_splits,
             version: time::Global::new(),
@@ -495,10 +484,8 @@ impl Buffer {
         }
     }
 
-    pub fn snapshot(&self) -> Snapshot {
-        Snapshot {
-            fragments: self.fragments.clone(),
-        }
+    pub fn snapshot(&self) -> Rope {
+        self.visible_text.clone()
     }
 
     pub fn file(&self) -> Option<&FileHandle> {
@@ -609,33 +596,12 @@ impl Buffer {
     }
 
     pub fn text_summary(&self) -> TextSummary {
-        self.fragments.extent::<TextSummary>()
+        self.visible_text.summary()
     }
 
     pub fn text_summary_for_range(&self, range: Range<usize>) -> TextSummary {
-        let mut summary = TextSummary::default();
-
-        let mut cursor = self.fragments.cursor::<usize, usize>();
-        cursor.seek(&range.start, SeekBias::Right, &());
-
-        if let Some(fragment) = cursor.item() {
-            let summary_start = cmp::max(*cursor.start(), range.start) - cursor.start();
-            let summary_end = cmp::min(range.end - cursor.start(), fragment.len());
-            summary += fragment.text.slice(summary_start..summary_end).summary();
-            cursor.next();
-        }
-
-        if range.end > *cursor.start() {
-            summary += cursor.summary::<TextSummary>(&range.end, SeekBias::Right, &());
-
-            if let Some(fragment) = cursor.item() {
-                let summary_start = cmp::max(*cursor.start(), range.start) - cursor.start();
-                let summary_end = cmp::min(range.end - cursor.start(), fragment.len());
-                summary += fragment.text.slice(summary_start..summary_end).summary();
-            }
-        }
-
-        summary
+        // TODO: Use a dedicated ::summarize method in Rope.
+        self.visible_text.slice(range).summary()
     }
 
     pub fn len(&self) -> usize {
@@ -654,37 +620,15 @@ impl Buffer {
     }
 
     pub fn rightmost_point(&self) -> Point {
-        self.fragments.summary().text_summary.rightmost_point
+        self.visible_text.summary().rightmost_point
     }
 
     pub fn rightmost_point_in_range(&self, range: Range<usize>) -> Point {
-        let mut summary = TextSummary::default();
-
-        let mut cursor = self.fragments.cursor::<usize, usize>();
-        cursor.seek(&range.start, SeekBias::Right, &());
-
-        if let Some(fragment) = cursor.item() {
-            let summary_start = cmp::max(*cursor.start(), range.start) - cursor.start();
-            let summary_end = cmp::min(range.end - cursor.start(), fragment.len());
-            summary += fragment.text.slice(summary_start..summary_end).summary();
-            cursor.next();
-        }
-
-        if range.end > *cursor.start() {
-            summary += cursor.summary::<TextSummary>(&range.end, SeekBias::Right, &());
-
-            if let Some(fragment) = cursor.item() {
-                let summary_start = cmp::max(*cursor.start(), range.start) - cursor.start();
-                let summary_end = cmp::min(range.end - cursor.start(), fragment.len());
-                summary += fragment.text.slice(summary_start..summary_end).summary();
-            }
-        }
-
-        summary.rightmost_point
+        self.text_summary_for_range(range).rightmost_point
     }
 
     pub fn max_point(&self) -> Point {
-        self.fragments.extent()
+        self.visible_text.max_point()
     }
 
     pub fn line(&self, row: u32) -> Result<String> {
@@ -707,13 +651,13 @@ impl Buffer {
         Ok(self.chars_at(start)?.take(end - start))
     }
 
-    pub fn chars(&self) -> CharIter {
+    pub fn chars(&self) -> rope::Chars {
         self.chars_at(0).unwrap()
     }
 
-    pub fn chars_at<T: ToOffset>(&self, position: T) -> Result<CharIter> {
+    pub fn chars_at<T: ToOffset>(&self, position: T) -> Result<rope::Chars> {
         let offset = position.to_offset(self)?;
-        Ok(CharIter::new(&self.fragments, offset))
+        Ok(self.visible_text.chars_at(offset))
     }
 
     pub fn selections_changed_since(&self, since: SelectionsVersion) -> bool {
@@ -807,7 +751,7 @@ impl Buffer {
     where
         I: IntoIterator<Item = Range<S>>,
         S: ToOffset,
-        T: Into<Text>,
+        T: Into<String>,
     {
         self.start_transaction_at(None, Instant::now())?;
 
@@ -823,11 +767,12 @@ impl Buffer {
             .map(|range| Ok(range.start.to_offset(self)?..range.end.to_offset(self)?))
             .collect::<Result<Vec<Range<usize>>>>()?;
 
+        let has_new_text = new_text.is_some();
         let ops = self.splice_fragments(
             old_ranges
                 .into_iter()
-                .filter(|old_range| new_text.is_some() || old_range.end > old_range.start),
-            new_text.clone(),
+                .filter(|old_range| has_new_text || old_range.end > old_range.start),
+            new_text.into(),
         );
 
         for op in &ops {
@@ -1022,7 +967,7 @@ impl Buffer {
                         edit.start_offset,
                         edit.end_id,
                         edit.end_offset,
-                        edit.new_text.as_ref().cloned(),
+                        edit.new_text.as_deref(),
                         &edit.version_in_range,
                         edit.id,
                         lamport_timestamp,
@@ -1064,29 +1009,38 @@ impl Buffer {
         start_offset: usize,
         end_id: time::Local,
         end_offset: usize,
-        new_text: Option<Text>,
+        mut new_text: Option<&str>,
         version_in_range: &time::Global,
         local_timestamp: time::Local,
         lamport_timestamp: time::Lamport,
     ) -> Result<()> {
-        let mut new_text = new_text.as_ref().cloned();
         let start_fragment_id = self.resolve_fragment_id(start_id, start_offset)?;
         let end_fragment_id = self.resolve_fragment_id(end_id, end_offset)?;
 
-        let old_fragments = self.fragments.clone();
-        let last_id = old_fragments.extent::<FragmentIdRef>().0.unwrap();
-        let last_id_ref = FragmentIdRef::new(&last_id);
+        let mut old_visible_text = Rope::new();
+        let mut old_deleted_text = Rope::new();
+        let mut old_fragments = SumTree::new();
+        mem::swap(&mut old_visible_text, &mut self.visible_text);
+        mem::swap(&mut old_deleted_text, &mut self.deleted_text);
+        mem::swap(&mut old_fragments, &mut self.fragments);
 
-        let mut cursor = old_fragments.cursor::<FragmentIdRef, ()>();
-        let mut new_fragments =
-            cursor.slice(&FragmentIdRef::new(&start_fragment_id), SeekBias::Left, &());
+        let mut fragments_cursor = old_fragments.cursor::<FragmentIdRef, FragmentTextSummary>();
 
-        if start_offset == cursor.item().unwrap().end_offset() {
-            new_fragments.push(cursor.item().unwrap().clone(), &());
-            cursor.next();
+        let mut new_fragments =
+            fragments_cursor.slice(&FragmentIdRef::new(&start_fragment_id), SeekBias::Left, &());
+        let mut new_ropes =
+            RopeBuilder::new(old_visible_text.cursor(0), old_deleted_text.cursor(0));
+        new_ropes.push_tree(new_fragments.summary().text);
+
+        let start_fragment = fragments_cursor.item().unwrap();
+        if start_offset == start_fragment.range_in_insertion.end {
+            let fragment = fragments_cursor.item().unwrap().clone();
+            new_ropes.push_fragment(&fragment, fragment.visible);
+            new_fragments.push(fragment, &());
+            fragments_cursor.next();
         }
 
-        while let Some(fragment) = cursor.item() {
+        while let Some(fragment) = fragments_cursor.item() {
             if new_text.is_none() && fragment.id > end_fragment_id {
                 break;
             }
@@ -1097,21 +1051,22 @@ impl Buffer {
                 let split_start = if start_fragment_id == fragment.id {
                     start_offset
                 } else {
-                    fragment.start_offset()
+                    fragment.range_in_insertion.start
                 };
                 let split_end = if end_fragment_id == fragment.id {
                     end_offset
                 } else {
-                    fragment.end_offset()
+                    fragment.range_in_insertion.end
                 };
                 let (before_range, within_range, after_range) = self.split_fragment(
-                    cursor.prev_item().as_ref().unwrap(),
+                    fragments_cursor.prev_item().as_ref().unwrap(),
                     &fragment,
                     split_start..split_end,
                 );
-                let insertion = if let Some(new_text) = new_text.take() {
+                let insertion = if let Some(new_text) = new_text {
+                    let prev_fragment = fragments_cursor.prev_item();
                     Some(self.build_fragment_to_insert(
-                        before_range.as_ref().or(cursor.prev_item()).unwrap(),
+                        before_range.as_ref().or(prev_fragment).unwrap(),
                         within_range.as_ref().or(after_range.as_ref()),
                         new_text,
                         local_timestamp,
@@ -1121,62 +1076,78 @@ impl Buffer {
                     None
                 };
                 if let Some(fragment) = before_range {
+                    new_ropes.push_fragment(&fragment, fragment.visible);
                     new_fragments.push(fragment, &());
                 }
                 if let Some(fragment) = insertion {
+                    new_ropes.push_str(new_text.take().unwrap());
                     new_fragments.push(fragment, &());
                 }
                 if let Some(mut fragment) = within_range {
+                    let fragment_was_visible = fragment.visible;
                     if fragment.was_visible(&version_in_range, &self.undo_map) {
                         fragment.deletions.insert(local_timestamp);
-                        fragment.visible = false;
+                        if fragment.visible {
+                            fragment.visible = false;
+                        }
                     }
+
+                    new_ropes.push_fragment(&fragment, fragment_was_visible);
                     new_fragments.push(fragment, &());
                 }
                 if let Some(fragment) = after_range {
+                    new_ropes.push_fragment(&fragment, fragment.visible);
                     new_fragments.push(fragment, &());
                 }
             } else {
                 if new_text.is_some() && lamport_timestamp > fragment.insertion.lamport_timestamp {
-                    new_fragments.push(
-                        self.build_fragment_to_insert(
-                            cursor.prev_item().as_ref().unwrap(),
-                            Some(&fragment),
-                            new_text.take().unwrap(),
-                            local_timestamp,
-                            lamport_timestamp,
-                        ),
-                        &(),
+                    let new_text = new_text.take().unwrap();
+                    let fragment = self.build_fragment_to_insert(
+                        fragments_cursor.prev_item().as_ref().unwrap(),
+                        Some(&fragment),
+                        new_text,
+                        local_timestamp,
+                        lamport_timestamp,
                     );
+                    new_ropes.push_str(new_text);
+                    new_fragments.push(fragment, &());
                 }
 
+                let fragment_was_visible = fragment.visible;
                 if fragment.id < end_fragment_id
                     && fragment.was_visible(&version_in_range, &self.undo_map)
                 {
                     fragment.deletions.insert(local_timestamp);
-                    fragment.visible = false;
+                    if fragment.visible {
+                        fragment.visible = false;
+                    }
                 }
+
+                new_ropes.push_fragment(&fragment, fragment_was_visible);
                 new_fragments.push(fragment, &());
             }
 
-            cursor.next();
+            fragments_cursor.next();
         }
 
         if let Some(new_text) = new_text {
-            new_fragments.push(
-                self.build_fragment_to_insert(
-                    cursor.prev_item().as_ref().unwrap(),
-                    None,
-                    new_text,
-                    local_timestamp,
-                    lamport_timestamp,
-                ),
-                &(),
+            let fragment = self.build_fragment_to_insert(
+                fragments_cursor.prev_item().as_ref().unwrap(),
+                None,
+                new_text,
+                local_timestamp,
+                lamport_timestamp,
             );
+            new_ropes.push_str(new_text);
+            new_fragments.push(fragment, &());
         }
 
-        new_fragments.push_tree(cursor.slice(&last_id_ref, SeekBias::Right, &()), &());
+        let (visible_text, deleted_text) = new_ropes.finish();
+        new_fragments.push_tree(fragments_cursor.suffix(&()), &());
+
         self.fragments = new_fragments;
+        self.visible_text = visible_text;
+        self.deleted_text = deleted_text;
         self.local_clock.observe(local_timestamp);
         self.lamport_clock.observe(lamport_timestamp);
         Ok(())
@@ -1251,58 +1222,83 @@ impl Buffer {
 
     fn apply_undo(&mut self, undo: UndoOperation) -> Result<()> {
         let mut new_fragments;
+        let mut old_visible_text = Rope::new();
+        let mut old_deleted_text = Rope::new();
+        mem::swap(&mut old_visible_text, &mut self.visible_text);
+        mem::swap(&mut old_deleted_text, &mut self.deleted_text);
+        let mut new_ropes =
+            RopeBuilder::new(old_visible_text.cursor(0), old_deleted_text.cursor(0));
 
         self.undo_map.insert(undo);
         let edit = &self.history.ops[&undo.edit_id];
         let start_fragment_id = self.resolve_fragment_id(edit.start_id, edit.start_offset)?;
         let end_fragment_id = self.resolve_fragment_id(edit.end_id, edit.end_offset)?;
-        let mut cursor = self.fragments.cursor::<FragmentIdRef, ()>();
+
+        let mut fragments_cursor = self.fragments.cursor::<FragmentIdRef, ()>();
 
         if edit.start_id == edit.end_id && edit.start_offset == edit.end_offset {
             let splits = &self.insertion_splits[&undo.edit_id];
             let mut insertion_splits = splits.cursor::<(), ()>().map(|s| &s.fragment_id).peekable();
 
             let first_split_id = insertion_splits.next().unwrap();
-            new_fragments = cursor.slice(&FragmentIdRef::new(first_split_id), SeekBias::Left, &());
+            new_fragments =
+                fragments_cursor.slice(&FragmentIdRef::new(first_split_id), SeekBias::Left, &());
+            new_ropes.push_tree(new_fragments.summary().text);
 
             loop {
-                let mut fragment = cursor.item().unwrap().clone();
+                let mut fragment = fragments_cursor.item().unwrap().clone();
+                let was_visible = fragment.visible;
                 fragment.visible = fragment.is_visible(&self.undo_map);
                 fragment.max_undos.observe(undo.id);
-                new_fragments.push(fragment, &());
-                cursor.next();
+
+                new_ropes.push_fragment(&fragment, was_visible);
+                new_fragments.push(fragment.clone(), &());
+
+                fragments_cursor.next();
                 if let Some(split_id) = insertion_splits.next() {
-                    new_fragments.push_tree(
-                        cursor.slice(&FragmentIdRef::new(split_id), SeekBias::Left, &()),
-                        &(),
-                    );
+                    let slice =
+                        fragments_cursor.slice(&FragmentIdRef::new(split_id), SeekBias::Left, &());
+                    new_ropes.push_tree(slice.summary().text);
+                    new_fragments.push_tree(slice, &());
                 } else {
                     break;
                 }
             }
         } else {
-            new_fragments =
-                cursor.slice(&FragmentIdRef::new(&start_fragment_id), SeekBias::Left, &());
-            while let Some(fragment) = cursor.item() {
+            new_fragments = fragments_cursor.slice(
+                &FragmentIdRef::new(&start_fragment_id),
+                SeekBias::Left,
+                &(),
+            );
+            new_ropes.push_tree(new_fragments.summary().text);
+
+            while let Some(fragment) = fragments_cursor.item() {
                 if fragment.id > end_fragment_id {
                     break;
                 } else {
                     let mut fragment = fragment.clone();
+                    let fragment_was_visible = fragment.visible;
                     if edit.version_in_range.observed(fragment.insertion.id)
                         || fragment.insertion.id == undo.edit_id
                     {
                         fragment.visible = fragment.is_visible(&self.undo_map);
                         fragment.max_undos.observe(undo.id);
                     }
+
+                    new_ropes.push_fragment(&fragment, fragment_was_visible);
                     new_fragments.push(fragment, &());
-                    cursor.next();
+                    fragments_cursor.next();
                 }
             }
         }
 
-        new_fragments.push_tree(cursor.suffix(&()), &());
-        drop(cursor);
+        new_fragments.push_tree(fragments_cursor.suffix(&()), &());
+        let (visible_text, deleted_text) = new_ropes.finish();
+        drop(fragments_cursor);
+
         self.fragments = new_fragments;
+        self.visible_text = visible_text;
+        self.deleted_text = deleted_text;
 
         Ok(())
     }
@@ -1372,7 +1368,7 @@ impl Buffer {
             .clone())
     }
 
-    fn splice_fragments<I>(&mut self, mut old_ranges: I, new_text: Option<Text>) -> Vec<Operation>
+    fn splice_fragments<I>(&mut self, mut old_ranges: I, new_text: Option<String>) -> Vec<Operation>
     where
         I: Iterator<Item = Range<usize>>,
     {
@@ -1383,13 +1379,20 @@ impl Buffer {
 
         let mut ops = Vec::with_capacity(old_ranges.size_hint().0);
 
-        let old_fragments = self.fragments.clone();
-        let mut cursor = old_fragments.cursor::<usize, usize>();
-        let mut new_fragments = SumTree::new();
-        new_fragments.push_tree(
-            cursor.slice(&cur_range.as_ref().unwrap().start, SeekBias::Right, &()),
-            &(),
-        );
+        let mut old_fragments = SumTree::new();
+        let mut old_visible_text = Rope::new();
+        let mut old_deleted_text = Rope::new();
+        mem::swap(&mut old_visible_text, &mut self.visible_text);
+        mem::swap(&mut old_deleted_text, &mut self.deleted_text);
+        mem::swap(&mut old_fragments, &mut self.fragments);
+
+        let mut fragments_cursor = old_fragments.cursor::<usize, usize>();
+        let mut new_fragments =
+            fragments_cursor.slice(&cur_range.as_ref().unwrap().start, SeekBias::Right, &());
+
+        let mut new_ropes =
+            RopeBuilder::new(old_visible_text.cursor(0), old_deleted_text.cursor(0));
+        new_ropes.push_tree(new_fragments.summary().text);
 
         let mut start_id = None;
         let mut start_offset = None;
@@ -1400,11 +1403,12 @@ impl Buffer {
         let mut local_timestamp = self.local_clock.tick();
         let mut lamport_timestamp = self.lamport_clock.tick();
 
-        while cur_range.is_some() && cursor.item().is_some() {
-            let mut fragment = cursor.item().unwrap().clone();
-            let fragment_summary = cursor.item_summary().unwrap();
-            let mut fragment_start = *cursor.start();
+        while cur_range.is_some() && fragments_cursor.item().is_some() {
+            let mut fragment = fragments_cursor.item().unwrap().clone();
+            let fragment_summary = fragments_cursor.item_summary().unwrap();
+            let mut fragment_start = *fragments_cursor.start();
             let mut fragment_end = fragment_start + fragment.visible_len();
+            let fragment_was_visible = fragment.visible;
 
             let old_split_tree = self
                 .insertion_splits
@@ -1412,7 +1416,7 @@ impl Buffer {
                 .unwrap();
             let mut splits_cursor = old_split_tree.cursor::<usize, ()>();
             let mut new_split_tree =
-                splits_cursor.slice(&fragment.start_offset(), SeekBias::Right, &());
+                splits_cursor.slice(&fragment.range_in_insertion.start, SeekBias::Right, &());
 
             // Find all splices that start or end within the current fragment. Then, split the
             // fragment and reassemble it in both trees accounting for the deleted and the newly
@@ -1421,14 +1425,17 @@ impl Buffer {
                 let range = cur_range.clone().unwrap();
                 if range.start > fragment_start {
                     let mut prefix = fragment.clone();
-                    prefix.set_end_offset(prefix.start_offset() + (range.start - fragment_start));
+                    prefix.range_in_insertion.end =
+                        prefix.range_in_insertion.start + (range.start - fragment_start);
                     prefix.id =
                         FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id);
-                    fragment.set_start_offset(prefix.end_offset());
+                    fragment.range_in_insertion.start = prefix.range_in_insertion.end;
+
+                    new_ropes.push_fragment(&prefix, prefix.visible);
                     new_fragments.push(prefix.clone(), &());
                     new_split_tree.push(
                         InsertionSplit {
-                            extent: prefix.end_offset() - prefix.start_offset(),
+                            extent: prefix.range_in_insertion.end - prefix.range_in_insertion.start,
                             fragment_id: prefix.id,
                         },
                         &(),
@@ -1438,24 +1445,26 @@ impl Buffer {
 
                 if range.end == fragment_start {
                     end_id = Some(new_fragments.last().unwrap().insertion.id);
-                    end_offset = Some(new_fragments.last().unwrap().end_offset());
+                    end_offset = Some(new_fragments.last().unwrap().range_in_insertion.end);
                 } else if range.end == fragment_end {
                     end_id = Some(fragment.insertion.id);
-                    end_offset = Some(fragment.end_offset());
+                    end_offset = Some(fragment.range_in_insertion.end);
                 }
 
                 if range.start == fragment_start {
                     start_id = Some(new_fragments.last().unwrap().insertion.id);
-                    start_offset = Some(new_fragments.last().unwrap().end_offset());
+                    start_offset = Some(new_fragments.last().unwrap().range_in_insertion.end);
 
                     if let Some(new_text) = new_text.clone() {
                         let new_fragment = self.build_fragment_to_insert(
                             &new_fragments.last().unwrap(),
                             Some(&fragment),
-                            new_text,
+                            &new_text,
                             local_timestamp,
                             lamport_timestamp,
                         );
+
+                        new_ropes.push_str(&new_text);
                         new_fragments.push(new_fragment, &());
                     }
                 }
@@ -1463,26 +1472,29 @@ impl Buffer {
                 if range.end < fragment_end {
                     if range.end > fragment_start {
                         let mut prefix = fragment.clone();
-                        prefix.set_end_offset(prefix.start_offset() + (range.end - fragment_start));
+                        prefix.range_in_insertion.end =
+                            prefix.range_in_insertion.start + (range.end - fragment_start);
                         prefix.id =
                             FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id);
                         version_in_range.observe_all(&fragment_summary.max_version);
-                        if fragment.visible {
+                        if prefix.visible {
                             prefix.deletions.insert(local_timestamp);
                             prefix.visible = false;
                         }
-                        fragment.set_start_offset(prefix.end_offset());
+                        fragment.range_in_insertion.start = prefix.range_in_insertion.end;
+                        new_ropes.push_fragment(&prefix, fragment_was_visible);
                         new_fragments.push(prefix.clone(), &());
                         new_split_tree.push(
                             InsertionSplit {
-                                extent: prefix.end_offset() - prefix.start_offset(),
+                                extent: prefix.range_in_insertion.end
+                                    - prefix.range_in_insertion.start,
                                 fragment_id: prefix.id,
                             },
                             &(),
                         );
                         fragment_start = range.end;
                         end_id = Some(fragment.insertion.id);
-                        end_offset = Some(fragment.start_offset());
+                        end_offset = Some(fragment.range_in_insertion.start);
                     }
                 } else {
                     version_in_range.observe_all(&fragment_summary.max_version);
@@ -1525,7 +1537,7 @@ impl Buffer {
             }
             new_split_tree.push(
                 InsertionSplit {
-                    extent: fragment.end_offset() - fragment.start_offset(),
+                    extent: fragment.range_in_insertion.end - fragment.range_in_insertion.start,
                     fragment_id: fragment.id.clone(),
                 },
                 &(),
@@ -1537,14 +1549,17 @@ impl Buffer {
             );
             self.insertion_splits
                 .insert(fragment.insertion.id, new_split_tree);
+
+            new_ropes.push_fragment(&fragment, fragment_was_visible);
             new_fragments.push(fragment, &());
 
             // Scan forward until we find a fragment that is not fully contained by the current splice.
-            cursor.next();
+            fragments_cursor.next();
             if let Some(range) = cur_range.clone() {
-                while let Some(fragment) = cursor.item() {
-                    let fragment_summary = cursor.item_summary().unwrap();
-                    fragment_start = *cursor.start();
+                while let Some(fragment) = fragments_cursor.item() {
+                    let fragment_summary = fragments_cursor.item_summary().unwrap();
+                    let fragment_was_visible = fragment.visible;
+                    fragment_start = *fragments_cursor.start();
                     fragment_end = fragment_start + fragment.visible_len();
                     if range.start < fragment_start && range.end >= fragment_end {
                         let mut new_fragment = fragment.clone();
@@ -1553,12 +1568,14 @@ impl Buffer {
                             new_fragment.deletions.insert(local_timestamp);
                             new_fragment.visible = false;
                         }
+
+                        new_ropes.push_fragment(&new_fragment, fragment_was_visible);
                         new_fragments.push(new_fragment, &());
-                        cursor.next();
+                        fragments_cursor.next();
 
                         if range.end == fragment_end {
                             end_id = Some(fragment.insertion.id);
-                            end_offset = Some(fragment.end_offset());
+                            end_offset = Some(fragment.range_in_insertion.end);
                             ops.push(Operation::Edit {
                                 edit: EditOperation {
                                     id: local_timestamp,
@@ -1594,10 +1611,13 @@ impl Buffer {
                 // that the cursor is parked at, we should seek to the next splice's start range
                 // and push all the fragments in between into the new tree.
                 if cur_range.as_ref().map_or(false, |r| r.start > fragment_end) {
-                    new_fragments.push_tree(
-                        cursor.slice(&cur_range.as_ref().unwrap().start, SeekBias::Right, &()),
+                    let slice = fragments_cursor.slice(
+                        &cur_range.as_ref().unwrap().start,
+                        SeekBias::Right,
                         &(),
                     );
+                    new_ropes.push_tree(slice.summary().text);
+                    new_fragments.push_tree(slice, &());
                 }
             }
         }
@@ -1611,10 +1631,11 @@ impl Buffer {
                 edit: EditOperation {
                     id: local_timestamp,
                     start_id: last_fragment.insertion.id,
-                    start_offset: last_fragment.end_offset(),
+                    start_offset: last_fragment.range_in_insertion.end,
                     end_id: last_fragment.insertion.id,
-                    end_offset: last_fragment.end_offset(),
+                    end_offset: last_fragment.range_in_insertion.end,
                     version_in_range: time::Global::new(),
+                    // TODO: avoid cloning the String.
                     new_text: new_text.clone(),
                 },
                 lamport_timestamp,
@@ -1624,20 +1645,22 @@ impl Buffer {
                 let new_fragment = self.build_fragment_to_insert(
                     &last_fragment,
                     None,
-                    new_text,
+                    &new_text,
                     local_timestamp,
                     lamport_timestamp,
                 );
+
+                new_ropes.push_str(&new_text);
                 new_fragments.push(new_fragment, &());
             }
-        } else {
-            new_fragments.push_tree(
-                cursor.slice(&old_fragments.extent::<usize>(), SeekBias::Right, &()),
-                &(),
-            );
         }
 
+        new_fragments.push_tree(fragments_cursor.suffix(&()), &());
+        let (visible_text, deleted_text) = new_ropes.finish();
+
         self.fragments = new_fragments;
+        self.visible_text = visible_text;
+        self.deleted_text = deleted_text;
         ops
     }
 
@@ -1647,24 +1670,26 @@ impl Buffer {
         fragment: &Fragment,
         range: Range<usize>,
     ) -> (Option<Fragment>, Option<Fragment>, Option<Fragment>) {
-        debug_assert!(range.start >= fragment.start_offset());
-        debug_assert!(range.start <= fragment.end_offset());
-        debug_assert!(range.end <= fragment.end_offset());
-        debug_assert!(range.end >= fragment.start_offset());
+        debug_assert!(range.start >= fragment.range_in_insertion.start);
+        debug_assert!(range.start <= fragment.range_in_insertion.end);
+        debug_assert!(range.end <= fragment.range_in_insertion.end);
+        debug_assert!(range.end >= fragment.range_in_insertion.start);
 
-        if range.end == fragment.start_offset() {
+        if range.end == fragment.range_in_insertion.start {
             (None, None, Some(fragment.clone()))
-        } else if range.start == fragment.end_offset() {
+        } else if range.start == fragment.range_in_insertion.end {
             (Some(fragment.clone()), None, None)
-        } else if range.start == fragment.start_offset() && range.end == fragment.end_offset() {
+        } else if range.start == fragment.range_in_insertion.start
+            && range.end == fragment.range_in_insertion.end
+        {
             (None, Some(fragment.clone()), None)
         } else {
             let mut prefix = fragment.clone();
 
-            let after_range = if range.end < fragment.end_offset() {
+            let after_range = if range.end < fragment.range_in_insertion.end {
                 let mut suffix = prefix.clone();
-                suffix.set_start_offset(range.end);
-                prefix.set_end_offset(range.end);
+                suffix.range_in_insertion.start = range.end;
+                prefix.range_in_insertion.end = range.end;
                 prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id);
                 Some(suffix)
             } else {
@@ -1673,15 +1698,15 @@ impl Buffer {
 
             let within_range = if range.start != range.end {
                 let mut suffix = prefix.clone();
-                suffix.set_start_offset(range.start);
-                prefix.set_end_offset(range.start);
+                suffix.range_in_insertion.start = range.start;
+                prefix.range_in_insertion.end = range.start;
                 prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id);
                 Some(suffix)
             } else {
                 None
             };
 
-            let before_range = if range.start > fragment.start_offset() {
+            let before_range = if range.start > fragment.range_in_insertion.start {
                 Some(prefix)
             } else {
                 None
@@ -1692,12 +1717,13 @@ impl Buffer {
                 .remove(&fragment.insertion.id)
                 .unwrap();
             let mut cursor = old_split_tree.cursor::<usize, ()>();
-            let mut new_split_tree = cursor.slice(&fragment.start_offset(), SeekBias::Right, &());
+            let mut new_split_tree =
+                cursor.slice(&fragment.range_in_insertion.start, SeekBias::Right, &());
 
             if let Some(ref fragment) = before_range {
                 new_split_tree.push(
                     InsertionSplit {
-                        extent: range.start - fragment.start_offset(),
+                        extent: range.start - fragment.range_in_insertion.start,
                         fragment_id: fragment.id.clone(),
                     },
                     &(),
@@ -1717,7 +1743,7 @@ impl Buffer {
             if let Some(ref fragment) = after_range {
                 new_split_tree.push(
                     InsertionSplit {
-                        extent: fragment.end_offset() - range.end,
+                        extent: fragment.range_in_insertion.end - range.end,
                         fragment_id: fragment.id.clone(),
                     },
                     &(),
@@ -1741,8 +1767,8 @@ impl Buffer {
         &mut self,
         prev_fragment: &Fragment,
         next_fragment: Option<&Fragment>,
-        text: Text,
-        local_timestamp: time::Local,
+        text: &str,
+        insertion_id: time::Local,
         lamport_timestamp: time::Lamport,
     ) -> Fragment {
         let new_fragment_id = FragmentId::between(
@@ -1752,25 +1778,27 @@ impl Buffer {
                 .unwrap_or(&FragmentId::max_value()),
         );
 
+        // TODO: extent could be expressed in bytes, which would save a linear scan.
+        let range_in_insertion = 0..text.chars().count();
         let mut split_tree = SumTree::new();
         split_tree.push(
             InsertionSplit {
-                extent: text.len(),
+                extent: range_in_insertion.len(),
                 fragment_id: new_fragment_id.clone(),
             },
             &(),
         );
-        self.insertion_splits.insert(local_timestamp, split_tree);
+        self.insertion_splits.insert(insertion_id, split_tree);
 
         Fragment::new(
             new_fragment_id,
-            Insertion {
-                id: local_timestamp,
+            Arc::new(Insertion {
+                id: insertion_id,
                 parent_id: prev_fragment.insertion.id,
-                offset_in_parent: prev_fragment.end_offset(),
-                text,
+                offset_in_parent: prev_fragment.range_in_insertion.end,
                 lamport_timestamp,
-            },
+            }),
+            range_in_insertion,
         )
     }
 

zed/src/editor/buffer/rope.rs 🔗

@@ -0,0 +1,477 @@
+use super::Point;
+use crate::sum_tree::{self, SeekBias, SumTree};
+use anyhow::{anyhow, Result};
+use arrayvec::ArrayString;
+use smallvec::SmallVec;
+use std::{cmp, ops::Range, str};
+
+#[cfg(test)]
+const CHUNK_BASE: usize = 2;
+
+#[cfg(not(test))]
+const CHUNK_BASE: usize = 16;
+
+#[derive(Clone, Default, Debug)]
+pub struct Rope {
+    chunks: SumTree<Chunk>,
+}
+
+impl Rope {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn append(&mut self, rope: Rope) {
+        let mut chunks = rope.chunks.cursor::<(), ()>();
+        chunks.next();
+        if let Some(chunk) = chunks.item() {
+            self.push(&chunk.0);
+            chunks.next();
+        }
+
+        self.chunks.push_tree(chunks.suffix(&()), &());
+        self.check_invariants();
+    }
+
+    pub fn push(&mut self, text: &str) {
+        let mut new_chunks = SmallVec::<[_; 16]>::new();
+        let mut new_chunk = ArrayString::new();
+        for ch in text.chars() {
+            if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE {
+                new_chunks.push(Chunk(new_chunk));
+                new_chunk = ArrayString::new();
+            }
+            new_chunk.push(ch);
+        }
+        if !new_chunk.is_empty() {
+            new_chunks.push(Chunk(new_chunk));
+        }
+
+        let mut new_chunks = new_chunks.into_iter();
+        let mut first_new_chunk = new_chunks.next();
+        self.chunks.update_last(
+            |last_chunk| {
+                if let Some(first_new_chunk_ref) = first_new_chunk.as_mut() {
+                    if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE {
+                        last_chunk.0.push_str(&first_new_chunk.take().unwrap().0);
+                    } else {
+                        let mut text = ArrayString::<[_; 4 * CHUNK_BASE]>::new();
+                        text.push_str(&last_chunk.0);
+                        text.push_str(&first_new_chunk_ref.0);
+
+                        let mut midpoint = text.len() / 2;
+                        while !text.is_char_boundary(midpoint) {
+                            midpoint += 1;
+                        }
+                        let (left, right) = text.split_at(midpoint);
+                        last_chunk.0.clear();
+                        last_chunk.0.push_str(left);
+                        first_new_chunk_ref.0.clear();
+                        first_new_chunk_ref.0.push_str(right);
+                    }
+                }
+            },
+            &(),
+        );
+
+        self.chunks
+            .extend(first_new_chunk.into_iter().chain(new_chunks), &());
+        self.check_invariants();
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(test)]
+        {
+            // Ensure all chunks except maybe the last one are not underflowing.
+            let mut chunks = self.chunks.cursor::<(), ()>().peekable();
+            while let Some(chunk) = chunks.next() {
+                if chunks.peek().is_some() {
+                    assert!(chunk.0.len() >= CHUNK_BASE);
+                }
+            }
+        }
+    }
+
+    pub fn slice(&self, range: Range<usize>) -> Rope {
+        self.cursor(range.start).slice(range.end)
+    }
+
+    pub fn summary(&self) -> TextSummary {
+        self.chunks.summary()
+    }
+
+    pub fn len(&self) -> usize {
+        self.chunks.extent()
+    }
+
+    pub fn max_point(&self) -> Point {
+        self.chunks.extent()
+    }
+
+    pub fn cursor(&self, offset: usize) -> Cursor {
+        Cursor::new(self, offset)
+    }
+
+    pub fn chars(&self) -> Chars {
+        self.chars_at(0)
+    }
+
+    pub fn chars_at(&self, start: usize) -> Chars {
+        Chars::new(self, start)
+    }
+
+    pub fn chunks<'a>(&'a self) -> impl Iterator<Item = &'a str> {
+        self.chunks.cursor::<(), ()>().map(|c| c.0.as_str())
+    }
+
+    pub fn to_point(&self, offset: usize) -> Result<Point> {
+        if offset <= self.summary().chars {
+            let mut cursor = self.chunks.cursor::<usize, TextSummary>();
+            cursor.seek(&offset, SeekBias::Left, &());
+            let overshoot = offset - cursor.start().chars;
+            Ok(cursor.start().lines
+                + cursor
+                    .item()
+                    .map_or(Point::zero(), |chunk| chunk.to_point(overshoot)))
+        } else {
+            Err(anyhow!("offset out of bounds"))
+        }
+    }
+
+    pub fn to_offset(&self, point: Point) -> Result<usize> {
+        // TODO: Verify the point actually exists.
+        if point <= self.summary().lines {
+            let mut cursor = self.chunks.cursor::<Point, TextSummary>();
+            cursor.seek(&point, SeekBias::Left, &());
+            let overshoot = point - cursor.start().lines;
+            Ok(cursor.start().chars + cursor.item().map_or(0, |chunk| chunk.to_offset(overshoot)))
+        } else {
+            Err(anyhow!("offset out of bounds"))
+        }
+    }
+}
+
+impl<'a> From<&'a str> for Rope {
+    fn from(text: &'a str) -> Self {
+        let mut rope = Self::new();
+        rope.push(text);
+        rope
+    }
+}
+
+pub struct Cursor<'a> {
+    rope: &'a Rope,
+    chunks: sum_tree::Cursor<'a, Chunk, usize, usize>,
+    offset: usize,
+}
+
+impl<'a> Cursor<'a> {
+    pub fn new(rope: &'a Rope, offset: usize) -> Self {
+        let mut chunks = rope.chunks.cursor();
+        chunks.seek(&offset, SeekBias::Right, &());
+        Self {
+            rope,
+            chunks,
+            offset,
+        }
+    }
+
+    pub fn seek_forward(&mut self, end_offset: usize) {
+        debug_assert!(end_offset >= self.offset);
+
+        self.chunks.seek_forward(&end_offset, SeekBias::Right, &());
+        self.offset = end_offset;
+    }
+
+    pub fn slice(&mut self, end_offset: usize) -> Rope {
+        debug_assert!(end_offset >= self.offset);
+
+        let mut slice = Rope::new();
+        if let Some(start_chunk) = self.chunks.item() {
+            let start_ix = self.offset - self.chunks.start();
+            let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start();
+            slice.push(&start_chunk.0[start_ix..end_ix]);
+        }
+
+        if end_offset > self.chunks.end() {
+            self.chunks.next();
+            slice.append(Rope {
+                chunks: self.chunks.slice(&end_offset, SeekBias::Right, &()),
+            });
+            if let Some(end_chunk) = self.chunks.item() {
+                slice.push(&end_chunk.0[..end_offset - self.chunks.start()]);
+            }
+        }
+
+        self.offset = end_offset;
+        slice
+    }
+
+    pub fn suffix(mut self) -> Rope {
+        self.slice(self.rope.chunks.extent())
+    }
+
+    pub fn offset(&self) -> usize {
+        self.offset
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct Chunk(ArrayString<[u8; 2 * CHUNK_BASE]>);
+
+impl Chunk {
+    fn to_point(&self, target: usize) -> Point {
+        let mut offset = 0;
+        let mut point = Point::new(0, 0);
+        for ch in self.0.chars() {
+            if offset >= target {
+                break;
+            }
+
+            if ch == '\n' {
+                point.row += 1;
+                point.column = 0;
+            } else {
+                point.column += 1;
+            }
+            offset += 1;
+        }
+        point
+    }
+
+    fn to_offset(&self, target: Point) -> usize {
+        let mut offset = 0;
+        let mut point = Point::new(0, 0);
+        for ch in self.0.chars() {
+            if point >= target {
+                break;
+            }
+
+            if ch == '\n' {
+                point.row += 1;
+                point.column = 0;
+            } else {
+                point.column += 1;
+            }
+            offset += 1;
+        }
+        offset
+    }
+}
+
+impl sum_tree::Item for Chunk {
+    type Summary = TextSummary;
+
+    fn summary(&self) -> Self::Summary {
+        let mut chars = 0;
+        let mut bytes = 0;
+        let mut lines = Point::new(0, 0);
+        let mut first_line_len = 0;
+        let mut rightmost_point = Point::new(0, 0);
+        for c in self.0.chars() {
+            chars += 1;
+            bytes += c.len_utf8();
+            if c == '\n' {
+                lines.row += 1;
+                lines.column = 0;
+            } else {
+                lines.column += 1;
+                if lines.row == 0 {
+                    first_line_len = lines.column;
+                }
+                if lines.column > rightmost_point.column {
+                    rightmost_point = lines;
+                }
+            }
+        }
+
+        TextSummary {
+            chars,
+            bytes,
+            lines,
+            first_line_len,
+            rightmost_point,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct TextSummary {
+    pub chars: usize,
+    pub bytes: usize,
+    pub lines: Point,
+    pub first_line_len: u32,
+    pub rightmost_point: Point,
+}
+
+impl sum_tree::Summary for TextSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &Self::Context) {
+        *self += summary;
+    }
+}
+
+impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
+    fn add_assign(&mut self, other: &'a Self) {
+        let joined_line_len = self.lines.column + other.first_line_len;
+        if joined_line_len > self.rightmost_point.column {
+            self.rightmost_point = Point::new(self.lines.row, joined_line_len);
+        }
+        if other.rightmost_point.column > self.rightmost_point.column {
+            self.rightmost_point = self.lines + &other.rightmost_point;
+        }
+
+        if self.lines.row == 0 {
+            self.first_line_len += other.first_line_len;
+        }
+
+        self.chars += other.chars;
+        self.bytes += other.bytes;
+        self.lines += &other.lines;
+    }
+}
+
+impl std::ops::AddAssign<Self> for TextSummary {
+    fn add_assign(&mut self, other: Self) {
+        *self += &other;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TextSummary> for TextSummary {
+    fn add_summary(&mut self, summary: &'a TextSummary) {
+        *self += summary;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
+    fn add_summary(&mut self, summary: &'a TextSummary) {
+        *self += summary.chars;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
+    fn add_summary(&mut self, summary: &'a TextSummary) {
+        *self += &summary.lines;
+    }
+}
+
+pub struct Chars<'a> {
+    cursor: sum_tree::Cursor<'a, Chunk, usize, usize>,
+    chars: str::Chars<'a>,
+}
+
+impl<'a> Chars<'a> {
+    pub fn new(rope: &'a Rope, start: usize) -> Self {
+        let mut cursor = rope.chunks.cursor::<usize, usize>();
+        cursor.slice(&start, SeekBias::Left, &());
+        let chars = if let Some(chunk) = cursor.item() {
+            let ix = start - cursor.start();
+            cursor.next();
+            chunk.0[ix..].chars()
+        } else {
+            "".chars()
+        };
+
+        Self { cursor, chars }
+    }
+}
+
+impl<'a> Iterator for Chars<'a> {
+    type Item = char;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(ch) = self.chars.next() {
+            Some(ch)
+        } else if let Some(chunk) = self.cursor.item() {
+            self.chars = chunk.0.chars();
+            self.cursor.next();
+            Some(self.chars.next().unwrap())
+        } else {
+            None
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::util::RandomCharIter;
+
+    use super::*;
+    use rand::prelude::*;
+    use std::env;
+
+    #[test]
+    fn test_random() {
+        let iterations = env::var("ITERATIONS")
+            .map(|i| i.parse().expect("invalid `ITERATIONS` variable"))
+            .unwrap_or(100);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+        let seed_range = if let Ok(seed) = env::var("SEED") {
+            let seed = seed.parse().expect("invalid `SEED` variable");
+            seed..seed + 1
+        } else {
+            0..iterations
+        };
+
+        for seed in seed_range {
+            dbg!(seed);
+            let mut rng = StdRng::seed_from_u64(seed);
+            let mut expected = String::new();
+            let mut actual = Rope::new();
+            for _ in 0..operations {
+                let end_ix = rng.gen_range(0..=expected.len());
+                let start_ix = rng.gen_range(0..=end_ix);
+                let len = rng.gen_range(0..=20);
+                let new_text: String = RandomCharIter::new(&mut rng).take(len).collect();
+
+                let mut new_actual = Rope::new();
+                let mut cursor = actual.cursor(0);
+                new_actual.append(cursor.slice(start_ix));
+                new_actual.push(&new_text);
+                cursor.seek_forward(end_ix);
+                new_actual.append(cursor.suffix());
+                actual = new_actual;
+
+                let mut new_expected = String::new();
+                new_expected.push_str(&expected[..start_ix]);
+                new_expected.push_str(&new_text);
+                new_expected.push_str(&expected[end_ix..]);
+                expected = new_expected;
+
+                assert_eq!(actual.text(), expected);
+
+                for _ in 0..5 {
+                    let ix = rng.gen_range(0..=expected.len());
+                    assert_eq!(actual.chars_at(ix).collect::<String>(), expected[ix..]);
+                }
+
+                let mut point = Point::new(0, 0);
+                let mut offset = 0;
+                for ch in expected.chars() {
+                    assert_eq!(actual.to_point(offset).unwrap(), point);
+                    assert_eq!(actual.to_offset(point).unwrap(), offset);
+                    if ch == '\n' {
+                        point.row += 1;
+                        point.column = 0
+                    } else {
+                        point.column += 1;
+                    }
+                    offset += 1;
+                }
+            }
+        }
+    }
+
+    impl Rope {
+        fn text(&self) -> String {
+            let mut text = String::new();
+            for chunk in self.chunks.cursor::<(), ()>() {
+                text.push_str(&chunk.0);
+            }
+            text
+        }
+    }
+}

zed/src/editor/buffer/text.rs 🔗

@@ -1,461 +0,0 @@
-use super::Point;
-use crate::sum_tree::{self, SeekBias, SumTree};
-use arrayvec::ArrayVec;
-use std::{
-    cmp,
-    fmt::{self, Debug},
-    ops::{Bound, Index, Range, RangeBounds},
-    sync::Arc,
-};
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum Run {
-    Newline,
-    Chars { len: usize, char_size: u8 },
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
-struct ByteOffset(usize);
-
-impl sum_tree::Item for Run {
-    type Summary = TextSummary;
-
-    fn summary(&self) -> Self::Summary {
-        match *self {
-            Run::Newline => TextSummary {
-                chars: 1,
-                bytes: 1,
-                lines: Point::new(1, 0),
-                first_line_len: 0,
-                rightmost_point: Point::new(0, 0),
-            },
-            Run::Chars { len, char_size } => TextSummary {
-                chars: len,
-                bytes: len * char_size as usize,
-                lines: Point::new(0, len as u32),
-                first_line_len: len as u32,
-                rightmost_point: Point::new(0, len as u32),
-            },
-        }
-    }
-}
-
-impl Run {
-    fn char_size(&self) -> u8 {
-        match self {
-            Run::Newline => 1,
-            Run::Chars { char_size, .. } => *char_size,
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct TextSummary {
-    pub chars: usize,
-    pub bytes: usize,
-    pub lines: Point,
-    pub first_line_len: u32,
-    pub rightmost_point: Point,
-}
-
-impl sum_tree::Summary for TextSummary {
-    type Context = ();
-
-    fn add_summary(&mut self, other: &Self, _: &()) {
-        *self += other;
-    }
-}
-
-impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
-    fn add_assign(&mut self, other: &'a Self) {
-        let joined_line_len = self.lines.column + other.first_line_len;
-        if joined_line_len > self.rightmost_point.column {
-            self.rightmost_point = Point::new(self.lines.row, joined_line_len);
-        }
-        if other.rightmost_point.column > self.rightmost_point.column {
-            self.rightmost_point = self.lines + &other.rightmost_point;
-        }
-
-        if self.lines.row == 0 {
-            self.first_line_len += other.first_line_len;
-        }
-
-        self.chars += other.chars;
-        self.bytes += other.bytes;
-        self.lines += &other.lines;
-    }
-}
-
-impl std::ops::AddAssign<Self> for TextSummary {
-    fn add_assign(&mut self, other: Self) {
-        *self += &other;
-    }
-}
-
-impl<'a> sum_tree::Dimension<'a, TextSummary> for TextSummary {
-    fn add_summary(&mut self, other: &TextSummary) {
-        *self += other;
-    }
-}
-
-impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
-    fn add_summary(&mut self, summary: &TextSummary) {
-        *self += &summary.lines;
-    }
-}
-
-impl<'a> sum_tree::Dimension<'a, TextSummary> for ByteOffset {
-    fn add_summary(&mut self, summary: &TextSummary) {
-        self.0 += summary.bytes
-    }
-}
-
-impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
-    fn add_summary(&mut self, summary: &TextSummary) {
-        *self += summary.chars;
-    }
-}
-
-#[derive(Clone)]
-pub struct Text {
-    text: Arc<str>,
-    runs: SumTree<Run>,
-    range: Range<usize>,
-}
-
-impl From<String> for Text {
-    fn from(text: String) -> Self {
-        Self::from(Arc::from(text))
-    }
-}
-
-impl<'a> From<&'a str> for Text {
-    fn from(text: &'a str) -> Self {
-        Self::from(Arc::from(text))
-    }
-}
-
-impl From<Arc<str>> for Text {
-    fn from(text: Arc<str>) -> Self {
-        let mut runs = Vec::new();
-
-        let mut chars_len = 0;
-        let mut run_char_size = 0;
-        let mut run_chars = 0;
-
-        let mut chars = text.chars();
-        loop {
-            let ch = chars.next();
-            let ch_size = ch.map_or(0, |ch| ch.len_utf8());
-            if run_chars != 0 && (ch.is_none() || ch == Some('\n') || run_char_size != ch_size) {
-                runs.push(Run::Chars {
-                    len: run_chars,
-                    char_size: run_char_size as u8,
-                });
-                run_chars = 0;
-            }
-            run_char_size = ch_size;
-
-            match ch {
-                Some('\n') => runs.push(Run::Newline),
-                Some(_) => run_chars += 1,
-                None => break,
-            }
-            chars_len += 1;
-        }
-
-        let mut tree = SumTree::new();
-        tree.extend(runs, &());
-        Text {
-            text,
-            runs: tree,
-            range: 0..chars_len,
-        }
-    }
-}
-
-impl Debug for Text {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_tuple("Text").field(&self.as_str()).finish()
-    }
-}
-
-impl PartialEq for Text {
-    fn eq(&self, other: &Self) -> bool {
-        self.text == other.text
-    }
-}
-
-impl Eq for Text {}
-
-impl<T: RangeBounds<usize>> Index<T> for Text {
-    type Output = str;
-
-    fn index(&self, range: T) -> &Self::Output {
-        let start = match range.start_bound() {
-            Bound::Included(start) => cmp::min(self.range.start + start, self.range.end),
-            Bound::Excluded(_) => unimplemented!(),
-            Bound::Unbounded => self.range.start,
-        };
-        let end = match range.end_bound() {
-            Bound::Included(end) => cmp::min(self.range.start + end + 1, self.range.end),
-            Bound::Excluded(end) => cmp::min(self.range.start + end, self.range.end),
-            Bound::Unbounded => self.range.end,
-        };
-
-        let byte_start = self.abs_byte_offset_for_offset(start);
-        let byte_end = self.abs_byte_offset_for_offset(end);
-        &self.text[byte_start..byte_end]
-    }
-}
-
-impl Text {
-    pub fn range(&self) -> Range<usize> {
-        self.range.clone()
-    }
-
-    pub fn as_str(&self) -> &str {
-        &self[..]
-    }
-
-    pub fn slice<T: RangeBounds<usize>>(&self, range: T) -> Text {
-        let start = match range.start_bound() {
-            Bound::Included(start) => cmp::min(self.range.start + start, self.range.end),
-            Bound::Excluded(_) => unimplemented!(),
-            Bound::Unbounded => self.range.start,
-        };
-        let end = match range.end_bound() {
-            Bound::Included(end) => cmp::min(self.range.start + end + 1, self.range.end),
-            Bound::Excluded(end) => cmp::min(self.range.start + end, self.range.end),
-            Bound::Unbounded => self.range.end,
-        };
-
-        Text {
-            text: self.text.clone(),
-            runs: self.runs.clone(),
-            range: start..end,
-        }
-    }
-
-    pub fn line_len(&self, row: u32) -> u32 {
-        let mut cursor = self.runs.cursor::<usize, Point>();
-        cursor.seek(&self.range.start, SeekBias::Right, &());
-        let absolute_row = cursor.start().row + row;
-
-        let mut cursor = self.runs.cursor::<Point, usize>();
-        cursor.seek(&Point::new(absolute_row, 0), SeekBias::Right, &());
-        let prefix_len = self.range.start.saturating_sub(*cursor.start());
-        let line_len =
-            cursor.summary::<usize>(&Point::new(absolute_row + 1, 0), SeekBias::Left, &());
-        let suffix_len = cursor.start().saturating_sub(self.range.end);
-
-        line_len
-            .saturating_sub(prefix_len)
-            .saturating_sub(suffix_len) as u32
-    }
-
-    pub fn len(&self) -> usize {
-        self.range.end - self.range.start
-    }
-
-    pub fn lines(&self) -> Point {
-        self.abs_point_for_offset(self.range.end) - &self.abs_point_for_offset(self.range.start)
-    }
-
-    pub fn rightmost_point(&self) -> Point {
-        let lines = self.lines();
-
-        let mut candidates = ArrayVec::<[Point; 3]>::new();
-        candidates.push(lines);
-        if lines.row > 0 {
-            candidates.push(Point::new(0, self.line_len(0)));
-            if lines.row > 1 {
-                let mut cursor = self.runs.cursor::<usize, Point>();
-                cursor.seek(&self.range.start, SeekBias::Right, &());
-                let absolute_start_row = cursor.start().row;
-
-                let mut cursor = self.runs.cursor::<Point, usize>();
-                cursor.seek(&Point::new(absolute_start_row + 1, 0), SeekBias::Right, &());
-                let summary = cursor.summary::<TextSummary>(
-                    &Point::new(absolute_start_row + lines.row, 0),
-                    SeekBias::Left,
-                    &(),
-                );
-
-                candidates.push(Point::new(1, 0) + &summary.rightmost_point);
-            }
-        }
-
-        candidates.into_iter().max_by_key(|p| p.column).unwrap()
-    }
-
-    pub fn point_for_offset(&self, offset: usize) -> Point {
-        self.abs_point_for_offset(self.range.start + offset)
-            - &self.abs_point_for_offset(self.range.start)
-    }
-
-    pub fn offset_for_point(&self, point: Point) -> usize {
-        let mut cursor = self.runs.cursor::<Point, TextSummary>();
-        let abs_point = self.abs_point_for_offset(self.range.start) + &point;
-        cursor.seek(&abs_point, SeekBias::Right, &());
-        let overshoot = abs_point - &cursor.start().lines;
-        let abs_offset = cursor.start().chars + overshoot.column as usize;
-        abs_offset - self.range.start
-    }
-
-    pub fn summary(&self) -> TextSummary {
-        TextSummary {
-            chars: self.range.end - self.range.start,
-            bytes: self.abs_byte_offset_for_offset(self.range.end)
-                - self.abs_byte_offset_for_offset(self.range.start),
-            lines: self.abs_point_for_offset(self.range.end)
-                - &self.abs_point_for_offset(self.range.start),
-            first_line_len: self.line_len(0),
-            rightmost_point: self.rightmost_point(),
-        }
-    }
-
-    fn abs_point_for_offset(&self, offset: usize) -> Point {
-        let mut cursor = self.runs.cursor::<usize, TextSummary>();
-        cursor.seek(&offset, SeekBias::Right, &());
-        let overshoot = (offset - cursor.start().chars) as u32;
-        cursor.start().lines + &Point::new(0, overshoot)
-    }
-
-    fn abs_byte_offset_for_offset(&self, offset: usize) -> usize {
-        let mut cursor = self.runs.cursor::<usize, TextSummary>();
-        cursor.seek(&offset, SeekBias::Right, &());
-        let overshoot = offset - cursor.start().chars;
-        cursor.start().bytes + overshoot * cursor.item().map_or(0, |run| run.char_size()) as usize
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::collections::HashSet;
-    use std::iter::FromIterator;
-
-    #[test]
-    fn test_basic() {
-        let text = Text::from(String::from("ab\ncd€\nfghij\nkl¢m"));
-        assert_eq!(text.len(), 17);
-        assert_eq!(text.as_str(), "ab\ncd€\nfghij\nkl¢m");
-        assert_eq!(text.lines(), Point::new(3, 4));
-        assert_eq!(text.line_len(0), 2);
-        assert_eq!(text.line_len(1), 3);
-        assert_eq!(text.line_len(2), 5);
-        assert_eq!(text.line_len(3), 4);
-        assert_eq!(text.rightmost_point(), Point::new(2, 5));
-
-        let b_to_g = text.slice(1..9);
-        assert_eq!(b_to_g.as_str(), "b\ncd€\nfg");
-        assert_eq!(b_to_g.len(), 8);
-        assert_eq!(b_to_g.lines(), Point::new(2, 2));
-        assert_eq!(b_to_g.line_len(0), 1);
-        assert_eq!(b_to_g.line_len(1), 3);
-        assert_eq!(b_to_g.line_len(2), 2);
-        assert_eq!(b_to_g.line_len(3), 0);
-        assert_eq!(b_to_g.rightmost_point(), Point::new(1, 3));
-
-        let d_to_i = text.slice(4..11);
-        assert_eq!(d_to_i.as_str(), "d€\nfghi");
-        assert_eq!(&d_to_i[1..5], "€\nfg");
-        assert_eq!(d_to_i.len(), 7);
-        assert_eq!(d_to_i.lines(), Point::new(1, 4));
-        assert_eq!(d_to_i.line_len(0), 2);
-        assert_eq!(d_to_i.line_len(1), 4);
-        assert_eq!(d_to_i.line_len(2), 0);
-        assert_eq!(d_to_i.rightmost_point(), Point::new(1, 4));
-
-        let d_to_j = text.slice(4..=11);
-        assert_eq!(d_to_j.as_str(), "d€\nfghij");
-        assert_eq!(&d_to_j[1..], "€\nfghij");
-        assert_eq!(d_to_j.len(), 8);
-    }
-
-    #[test]
-    fn test_random() {
-        use rand::prelude::*;
-
-        for seed in 0..100 {
-            println!("buffer::text seed: {}", seed);
-            let rng = &mut StdRng::seed_from_u64(seed);
-
-            let len = rng.gen_range(0..50);
-            let mut string = String::new();
-            for _ in 0..len {
-                if rng.gen_ratio(1, 5) {
-                    string.push('\n');
-                } else {
-                    string.push(rng.gen());
-                }
-            }
-            let text = Text::from(string.clone());
-
-            for _ in 0..10 {
-                let start = rng.gen_range(0..text.len() + 1);
-                let end = rng.gen_range(start..text.len() + 2);
-
-                let string_slice = string
-                    .chars()
-                    .skip(start)
-                    .take(end - start)
-                    .collect::<String>();
-                let expected_line_endpoints = string_slice
-                    .split('\n')
-                    .enumerate()
-                    .map(|(row, line)| Point::new(row as u32, line.chars().count() as u32))
-                    .collect::<Vec<_>>();
-                let text_slice = text.slice(start..end);
-
-                assert_eq!(text_slice.lines(), lines(&string_slice));
-
-                let mut rightmost_points: HashSet<Point> = HashSet::new();
-                for endpoint in &expected_line_endpoints {
-                    if let Some(rightmost_point) = rightmost_points.iter().next().cloned() {
-                        if endpoint.column > rightmost_point.column {
-                            rightmost_points.clear();
-                        }
-                        if endpoint.column >= rightmost_point.column {
-                            rightmost_points.insert(*endpoint);
-                        }
-                    } else {
-                        rightmost_points.insert(*endpoint);
-                    }
-
-                    assert_eq!(text_slice.line_len(endpoint.row as u32), endpoint.column);
-                }
-
-                assert!(rightmost_points.contains(&text_slice.rightmost_point()));
-
-                for _ in 0..10 {
-                    let offset = rng.gen_range(0..string_slice.chars().count() + 1);
-                    let point = lines(&string_slice.chars().take(offset).collect::<String>());
-                    assert_eq!(text_slice.point_for_offset(offset), point);
-                    assert_eq!(text_slice.offset_for_point(point), offset);
-                    if offset < string_slice.chars().count() {
-                        assert_eq!(
-                            &text_slice[offset..offset + 1],
-                            String::from_iter(string_slice.chars().nth(offset)).as_str()
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    pub fn lines(s: &str) -> Point {
-        let mut row = 0;
-        let mut column = 0;
-        for ch in s.chars() {
-            if ch == '\n' {
-                row += 1;
-                column = 0;
-            } else {
-                column += 1;
-            }
-        }
-        Point::new(row, column)
-    }
-}

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

@@ -1,8 +1,9 @@
 use super::{
-    buffer::{self, AnchorRangeExt},
-    Anchor, Buffer, DisplayPoint, Edit, Point, TextSummary, ToOffset,
+    buffer::{AnchorRangeExt, TextSummary},
+    Anchor, Buffer, DisplayPoint, Edit, Point, ToOffset,
 };
 use crate::{
+    editor::rope,
     sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree},
     time,
 };
@@ -607,7 +608,7 @@ pub struct Chars<'a> {
     cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
     offset: usize,
     buffer: &'a Buffer,
-    buffer_chars: Option<Take<buffer::CharIter<'a>>>,
+    buffer_chars: Option<Take<rope::Chars<'a>>>,
 }
 
 impl<'a> Iterator for Chars<'a> {
@@ -669,8 +670,8 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::editor::buffer::ToPoint;
     use crate::test::sample_text;
-    use buffer::ToPoint;
 
     #[gpui::test]
     fn test_basic_folds(app: &mut gpui::MutableAppContext) {
@@ -916,6 +917,7 @@ mod tests {
                     assert_eq!(line_len, line.chars().count() as u32);
                 }
 
+                let rightmost_point = map.rightmost_point(app.as_ref());
                 let mut display_point = DisplayPoint::new(0, 0);
                 let mut display_offset = DisplayOffset(0);
                 for c in expected_text.chars() {
@@ -941,6 +943,12 @@ mod tests {
                         *display_point.column_mut() += 1;
                     }
                     display_offset.0 += 1;
+                    if display_point.column() > rightmost_point.column() {
+                        panic!(
+                            "invalid rightmost point {:?}, found point {:?}",
+                            rightmost_point, display_point
+                        );
+                    }
                 }
 
                 for _ in 0..5 {

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

@@ -1,6 +1,6 @@
 mod fold_map;
 
-use super::{buffer, Anchor, Buffer, Edit, Point, TextSummary, ToOffset, ToPoint};
+use super::{buffer, Anchor, Buffer, Edit, Point, ToOffset, ToPoint};
 use anyhow::Result;
 pub use fold_map::BufferRows;
 use fold_map::{FoldMap, FoldMapSnapshot};

zed/src/sum_tree/mod.rs 🔗

@@ -101,6 +101,46 @@ impl<T: Item> SumTree<T> {
         self.rightmost_leaf().0.items().last()
     }
 
+    pub fn update_last(&mut self, f: impl FnOnce(&mut T), ctx: &<T::Summary as Summary>::Context) {
+        self.update_last_recursive(f, ctx);
+    }
+
+    fn update_last_recursive(
+        &mut self,
+        f: impl FnOnce(&mut T),
+        ctx: &<T::Summary as Summary>::Context,
+    ) -> Option<T::Summary> {
+        match Arc::make_mut(&mut self.0) {
+            Node::Internal {
+                summary,
+                child_summaries,
+                child_trees,
+                ..
+            } => {
+                let last_summary = child_summaries.last_mut().unwrap();
+                let last_child = child_trees.last_mut().unwrap();
+                *last_summary = last_child.update_last_recursive(f, ctx).unwrap();
+                *summary = sum(child_summaries.iter(), ctx);
+                Some(summary.clone())
+            }
+            Node::Leaf {
+                summary,
+                items,
+                item_summaries,
+            } => {
+                if let Some((item, item_summary)) = items.last_mut().zip(item_summaries.last_mut())
+                {
+                    (f)(item);
+                    *item_summary = item.summary();
+                    *summary = sum(item_summaries.iter(), ctx);
+                    Some(summary.clone())
+                } else {
+                    None
+                }
+            }
+        }
+    }
+
     pub fn extent<'a, D: Dimension<'a, T::Summary>>(&'a self) -> D {
         let mut extent = D::default();
         match self.0.as_ref() {

zed/src/worktree.rs 🔗

@@ -3,7 +3,7 @@ mod fuzzy;
 mod ignore;
 
 use crate::{
-    editor::{History, Snapshot as BufferSnapshot},
+    editor::{History, Rope},
     sum_tree::{self, Cursor, Edit, SeekBias, SumTree},
 };
 use ::ignore::gitignore::Gitignore;
@@ -198,20 +198,15 @@ impl Worktree {
         })
     }
 
-    pub fn save<'a>(
-        &self,
-        path: &Path,
-        content: BufferSnapshot,
-        ctx: &AppContext,
-    ) -> Task<Result<()>> {
+    pub fn save<'a>(&self, path: &Path, content: Rope, ctx: &AppContext) -> Task<Result<()>> {
         let handles = self.handles.clone();
         let path = path.to_path_buf();
         let abs_path = self.absolutize(&path);
         ctx.background_executor().spawn(async move {
-            let buffer_size = content.text_summary().bytes.min(10 * 1024);
+            let buffer_size = content.summary().bytes.min(10 * 1024);
             let file = fs::File::create(&abs_path)?;
             let mut writer = io::BufWriter::with_capacity(buffer_size, &file);
-            for chunk in content.fragments() {
+            for chunk in content.chunks() {
                 writer.write(chunk.as_bytes())?;
             }
             writer.flush()?;
@@ -459,7 +454,7 @@ impl FileHandle {
         self.worktree.read(ctx).load_history(&self.path(), ctx)
     }
 
-    pub fn save<'a>(&self, content: BufferSnapshot, ctx: &AppContext) -> Task<Result<()>> {
+    pub fn save<'a>(&self, content: Rope, ctx: &AppContext) -> Task<Result<()>> {
         let worktree = self.worktree.read(ctx);
         worktree.save(&self.path(), content, ctx)
     }