Remove Buffer branch APIs

Max Brunsfeld created

Update preview_edits to use a new simpler API, BufferSnapshot::edit,
that applies edits to a buffer snapshot without affecting its original
buffer.

Change summary

crates/editor/src/editor.rs         |  77 -----
crates/editor/src/element.rs        |   2 
crates/editor/src/items.rs          |   5 
crates/language/src/buffer.rs       | 137 ---------
crates/language/src/buffer_tests.rs | 163 -----------
crates/text/src/text.rs             | 443 ++++++++++++++++--------------
6 files changed, 240 insertions(+), 587 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -218,7 +218,7 @@ use workspace::{
     CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, NavigationEntry, OpenInTerminal,
     OpenTerminal, Pane, RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection,
     TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings,
-    item::{ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions},
+    item::{ItemBufferKind, ItemHandle, PreviewTabsSettings},
     notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
     searchable::SearchEvent,
 };
@@ -21115,81 +21115,6 @@ impl Editor {
         })
     }
 
-    pub(crate) fn apply_all_diff_hunks(
-        &mut self,
-        _: &ApplyAllDiffHunks,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
-
-        let buffers = self.buffer.read(cx).all_buffers();
-        for branch_buffer in buffers {
-            branch_buffer.update(cx, |branch_buffer, cx| {
-                branch_buffer.merge_into_base(Vec::new(), cx);
-            });
-        }
-
-        if let Some(project) = self.project.clone() {
-            self.save(
-                SaveOptions {
-                    format: true,
-                    autosave: false,
-                },
-                project,
-                window,
-                cx,
-            )
-            .detach_and_log_err(cx);
-        }
-    }
-
-    pub(crate) fn apply_selected_diff_hunks(
-        &mut self,
-        _: &ApplyDiffHunk,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
-        let snapshot = self.snapshot(window, cx);
-        let hunks = snapshot.hunks_for_ranges(
-            self.selections
-                .all(&snapshot.display_snapshot)
-                .into_iter()
-                .map(|selection| selection.range()),
-        );
-        let mut ranges_by_buffer = HashMap::default();
-        self.transact(window, cx, |editor, _window, cx| {
-            for hunk in hunks {
-                if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
-                    ranges_by_buffer
-                        .entry(buffer.clone())
-                        .or_insert_with(Vec::new)
-                        .push(hunk.buffer_range.to_offset(buffer.read(cx)));
-                }
-            }
-
-            for (buffer, ranges) in ranges_by_buffer {
-                buffer.update(cx, |buffer, cx| {
-                    buffer.merge_into_base(ranges, cx);
-                });
-            }
-        });
-
-        if let Some(project) = self.project.clone() {
-            self.save(
-                SaveOptions {
-                    format: true,
-                    autosave: false,
-                },
-                project,
-                window,
-                cx,
-            )
-            .detach_and_log_err(cx);
-        }
-    }
-
     pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context<Self>) {
         if hovered != self.gutter_hovered {
             self.gutter_hovered = hovered;

crates/editor/src/element.rs 🔗

@@ -641,8 +641,6 @@ impl EditorElement {
         register_action(editor, window, Editor::restore_file);
         register_action(editor, window, Editor::git_restore);
         register_action(editor, window, Editor::restore_and_next);
-        register_action(editor, window, Editor::apply_all_diff_hunks);
-        register_action(editor, window, Editor::apply_selected_diff_hunks);
         register_action(editor, window, Editor::open_active_item_in_terminal);
         register_action(editor, window, Editor::reload_file);
         register_action(editor, window, Editor::spawn_nearest_task);

crates/editor/src/items.rs 🔗

@@ -875,10 +875,7 @@ impl Item for Editor {
         }
 
         let buffers = self.buffer().clone().read(cx).all_buffers();
-        let buffers = buffers
-            .into_iter()
-            .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
-            .collect::<HashSet<_>>();
+        let buffers = buffers.into_iter().collect::<HashSet<_>>();
 
         let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave {
             buffers

crates/language/src/buffer.rs 🔗

@@ -66,7 +66,7 @@ pub use text::{
 use theme::{ActiveTheme as _, SyntaxTheme};
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath};
+use util::{RangeExt, maybe, paths::PathStyle, rel_path::RelPath};
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_python, tree_sitter_rust, tree_sitter_typescript};
@@ -97,7 +97,6 @@ pub type BufferRow = u32;
 /// syntax trees, git status, and diagnostics.
 pub struct Buffer {
     text: TextBuffer,
-    branch_state: Option<BufferBranchState>,
     /// Filesystem state, `None` when there is no path.
     file: Option<Arc<dyn File>>,
     /// The mtime of the file when this buffer was last loaded from
@@ -176,11 +175,6 @@ pub enum ParseStatus {
     Parsing,
 }
 
-struct BufferBranchState {
-    base_buffer: Entity<Buffer>,
-    merged_operations: Vec<Lamport>,
-}
-
 /// An immutable, cheaply cloneable representation of a fixed
 /// state of a buffer.
 pub struct BufferSnapshot {
@@ -1086,7 +1080,7 @@ impl Buffer {
             was_dirty_before_starting_transaction: None,
             has_unsaved_edits: Cell::new((buffer.version(), false)),
             text: buffer,
-            branch_state: None,
+
             file,
             capability,
             syntax_map,
@@ -1242,31 +1236,6 @@ impl Buffer {
         }
     }
 
-    pub fn branch(&mut self, cx: &mut Context<Self>) -> Entity<Self> {
-        let this = cx.entity();
-        cx.new(|cx| {
-            let mut branch = Self {
-                branch_state: Some(BufferBranchState {
-                    base_buffer: this.clone(),
-                    merged_operations: Default::default(),
-                }),
-                language: self.language.clone(),
-                has_conflict: self.has_conflict,
-                has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()),
-                _subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)],
-                ..Self::build(self.text.branch(), self.file.clone(), self.capability())
-            };
-            if let Some(language_registry) = self.language_registry() {
-                branch.set_language_registry(language_registry);
-            }
-
-            // Reparse the branch buffer so that we get syntax highlighting immediately.
-            branch.reparse(cx, true);
-
-            branch
-        })
-    }
-
     #[ztracing::instrument(skip_all)]
     pub fn preview_edits(
         &self,
@@ -1276,7 +1245,7 @@ impl Buffer {
         let registry = self.language_registry();
         let language = self.language().cloned();
         let old_snapshot = self.text.snapshot().clone();
-        let mut branch_buffer = self.text.branch();
+        let mut new_snapshot = old_snapshot.clone();
         let mut syntax_snapshot = self.syntax_map.lock().snapshot();
         cx.background_spawn(async move {
             if !edits.is_empty() {
@@ -1284,109 +1253,21 @@ impl Buffer {
                     syntax_snapshot.reparse(&old_snapshot, registry.clone(), language);
                 }
 
-                branch_buffer.edit(edits.iter().cloned());
-                let snapshot = branch_buffer.snapshot();
-                syntax_snapshot.interpolate(&snapshot);
+                new_snapshot.edit(edits.iter().cloned());
+                syntax_snapshot.interpolate(&new_snapshot);
 
                 if let Some(language) = language {
-                    syntax_snapshot.reparse(&snapshot, registry, language);
+                    syntax_snapshot.reparse(&new_snapshot, registry, language);
                 }
             }
             EditPreview {
                 old_snapshot,
-                applied_edits_snapshot: branch_buffer.into_snapshot(),
+                applied_edits_snapshot: new_snapshot,
                 syntax_snapshot,
             }
         })
     }
 
-    /// Applies all of the changes in this buffer that intersect any of the
-    /// given `ranges` to its base buffer.
-    ///
-    /// If `ranges` is empty, then all changes will be applied. This buffer must
-    /// be a branch buffer to call this method.
-    pub fn merge_into_base(&mut self, ranges: Vec<Range<usize>>, cx: &mut Context<Self>) {
-        let Some(base_buffer) = self.base_buffer() else {
-            debug_panic!("not a branch buffer");
-            return;
-        };
-
-        let mut ranges = if ranges.is_empty() {
-            &[0..usize::MAX]
-        } else {
-            ranges.as_slice()
-        }
-        .iter()
-        .peekable();
-
-        let mut edits = Vec::new();
-        for edit in self.edits_since::<usize>(&base_buffer.read(cx).version()) {
-            let mut is_included = false;
-            while let Some(range) = ranges.peek() {
-                if range.end < edit.new.start {
-                    ranges.next().unwrap();
-                } else {
-                    if range.start <= edit.new.end {
-                        is_included = true;
-                    }
-                    break;
-                }
-            }
-
-            if is_included {
-                edits.push((
-                    edit.old.clone(),
-                    self.text_for_range(edit.new.clone()).collect::<String>(),
-                ));
-            }
-        }
-
-        let operation = base_buffer.update(cx, |base_buffer, cx| {
-            // cx.emit(BufferEvent::DiffBaseChanged);
-            base_buffer.edit(edits, None, cx)
-        });
-
-        if let Some(operation) = operation
-            && let Some(BufferBranchState {
-                merged_operations, ..
-            }) = &mut self.branch_state
-        {
-            merged_operations.push(operation);
-        }
-    }
-
-    fn on_base_buffer_event(
-        &mut self,
-        _: Entity<Buffer>,
-        event: &BufferEvent,
-        cx: &mut Context<Self>,
-    ) {
-        let BufferEvent::Operation { operation, .. } = event else {
-            return;
-        };
-        let Some(BufferBranchState {
-            merged_operations, ..
-        }) = &mut self.branch_state
-        else {
-            return;
-        };
-
-        let mut operation_to_undo = None;
-        if let Operation::Buffer(text::Operation::Edit(operation)) = &operation
-            && let Ok(ix) = merged_operations.binary_search(&operation.timestamp)
-        {
-            merged_operations.remove(ix);
-            operation_to_undo = Some(operation.timestamp);
-        }
-
-        self.apply_ops([operation.clone()], cx);
-
-        if let Some(timestamp) = operation_to_undo {
-            let counts = [(timestamp, u32::MAX)].into_iter().collect();
-            self.undo_operations(counts, cx);
-        }
-    }
-
     pub fn as_text_snapshot(&self) -> &text::BufferSnapshot {
         &self.text
     }
@@ -1685,10 +1566,6 @@ impl Buffer {
         }
     }
 
-    pub fn base_buffer(&self) -> Option<Entity<Self>> {
-        Some(self.branch_state.as_ref()?.base_buffer.clone())
-    }
-
     /// Returns the primary [`Language`] assigned to this [`Buffer`].
     pub fn language(&self) -> Option<&Arc<Language>> {
         self.language.as_ref()

crates/language/src/buffer_tests.rs 🔗

@@ -3080,169 +3080,6 @@ fn test_serialization(cx: &mut gpui::App) {
     assert_eq!(buffer2.read(cx).text(), "abcDF");
 }
 
-#[gpui::test]
-fn test_branch_and_merge(cx: &mut TestAppContext) {
-    cx.update(|cx| init_settings(cx, |_| {}));
-
-    let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
-
-    // Create a remote replica of the base buffer.
-    let base_replica = cx.new(|cx| {
-        Buffer::from_proto(
-            ReplicaId::new(1),
-            Capability::ReadWrite,
-            base.read(cx).to_proto(cx),
-            None,
-        )
-        .unwrap()
-    });
-    base.update(cx, |_buffer, cx| {
-        cx.subscribe(&base_replica, |this, _, event, cx| {
-            if let BufferEvent::Operation {
-                operation,
-                is_local: true,
-            } = event
-            {
-                this.apply_ops([operation.clone()], cx);
-            }
-        })
-        .detach();
-    });
-
-    // Create a branch, which initially has the same state as the base buffer.
-    let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
-    branch.read_with(cx, |buffer, _| {
-        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
-    });
-
-    // Edits to the branch are not applied to the base.
-    branch.update(cx, |buffer, cx| {
-        buffer.edit(
-            [
-                (Point::new(1, 0)..Point::new(1, 0), "1.5\n"),
-                (Point::new(2, 0)..Point::new(2, 5), "THREE"),
-            ],
-            None,
-            cx,
-        )
-    });
-    branch.read_with(cx, |buffer, cx| {
-        assert_eq!(base.read(cx).text(), "one\ntwo\nthree\n");
-        assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");
-    });
-
-    // Convert from branch buffer ranges to the corresponding ranges in the
-    // base buffer.
-    branch.read_with(cx, |buffer, cx| {
-        assert_eq!(
-            buffer.range_to_version(4..7, &base.read(cx).version()),
-            4..4
-        );
-        assert_eq!(
-            buffer.range_to_version(2..9, &base.read(cx).version()),
-            2..5
-        );
-    });
-
-    // Edits to the base are applied to the branch.
-    base.update(cx, |buffer, cx| {
-        buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx)
-    });
-    branch.read_with(cx, |buffer, cx| {
-        assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\nthree\n");
-        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n");
-    });
-
-    // Edits to any replica of the base are applied to the branch.
-    base_replica.update(cx, |buffer, cx| {
-        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "2.5\n")], None, cx)
-    });
-    branch.read_with(cx, |buffer, cx| {
-        assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\n2.5\nthree\n");
-        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
-    });
-
-    // Merging the branch applies all of its changes to the base.
-    branch.update(cx, |buffer, cx| {
-        buffer.merge_into_base(Vec::new(), cx);
-    });
-
-    branch.update(cx, |buffer, cx| {
-        assert_eq!(base.read(cx).text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
-        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
-    });
-}
-
-#[gpui::test]
-fn test_merge_into_base(cx: &mut TestAppContext) {
-    cx.update(|cx| init_settings(cx, |_| {}));
-
-    let base = cx.new(|cx| Buffer::local("abcdefghijk", cx));
-    let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
-
-    // Make 3 edits, merge one into the base.
-    branch.update(cx, |branch, cx| {
-        branch.edit([(0..3, "ABC"), (7..9, "HI"), (11..11, "LMN")], None, cx);
-        branch.merge_into_base(vec![5..8], cx);
-    });
-
-    branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjkLMN"));
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
-
-    // Undo the one already-merged edit. Merge that into the base.
-    branch.update(cx, |branch, cx| {
-        branch.edit([(7..9, "hi")], None, cx);
-        branch.merge_into_base(vec![5..8], cx);
-    });
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
-
-    // Merge an insertion into the base.
-    branch.update(cx, |branch, cx| {
-        branch.merge_into_base(vec![11..11], cx);
-    });
-
-    branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefghijkLMN"));
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijkLMN"));
-
-    // Deleted the inserted text and merge that into the base.
-    branch.update(cx, |branch, cx| {
-        branch.edit([(11..14, "")], None, cx);
-        branch.merge_into_base(vec![10..11], cx);
-    });
-
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
-}
-
-#[gpui::test]
-fn test_undo_after_merge_into_base(cx: &mut TestAppContext) {
-    cx.update(|cx| init_settings(cx, |_| {}));
-
-    let base = cx.new(|cx| Buffer::local("abcdefghijk", cx));
-    let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
-
-    // Make 2 edits, merge one into the base.
-    branch.update(cx, |branch, cx| {
-        branch.edit([(0..3, "ABC"), (7..9, "HI")], None, cx);
-        branch.merge_into_base(vec![7..7], cx);
-    });
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
-    branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
-
-    // Undo the merge in the base buffer.
-    base.update(cx, |base, cx| {
-        base.undo(cx);
-    });
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefghijk"));
-    branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
-
-    // Merge that operation into the base again.
-    branch.update(cx, |branch, cx| {
-        branch.merge_into_base(vec![7..7], cx);
-    });
-    base.read_with(cx, |base, _| assert_eq!(base.text(), "abcdefgHIjk"));
-    branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
-}
-
 #[gpui::test]
 async fn test_preview_edits(cx: &mut TestAppContext) {
     cx.update(|cx| {

crates/text/src/text.rs 🔗

@@ -838,19 +838,6 @@ impl Buffer {
         self.snapshot
     }
 
-    pub fn branch(&self) -> Self {
-        Self {
-            snapshot: self.snapshot.clone(),
-            history: History::new(self.base_text().clone()),
-            deferred_ops: OperationQueue::new(),
-            deferred_replicas: HashSet::default(),
-            lamport_clock: clock::Lamport::new(ReplicaId::LOCAL_BRANCH),
-            subscriptions: Default::default(),
-            edit_id_resolvers: Default::default(),
-            wait_for_version_txs: Default::default(),
-        }
-    }
-
     pub fn replica_id(&self) -> ReplicaId {
         self.lamport_clock.replica_id
     }
@@ -894,162 +881,11 @@ impl Buffer {
         edits: impl ExactSizeIterator<Item = (Range<S>, T)>,
         timestamp: clock::Lamport,
     ) -> EditOperation {
-        let mut edits_patch = Patch::default();
-        let mut edit_op = EditOperation {
-            timestamp,
-            version: self.version(),
-            ranges: Vec::with_capacity(edits.len()),
-            new_text: Vec::with_capacity(edits.len()),
-        };
-        let mut new_insertions = Vec::new();
-        let mut insertion_offset: u32 = 0;
-        let mut insertion_slices = Vec::new();
-
-        let mut edits = edits
-            .map(|(range, new_text)| (range.to_offset(&*self), new_text))
-            .peekable();
-
-        let mut new_ropes =
-            RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
-        let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>(&None);
-        let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right);
-        new_ropes.append(new_fragments.summary().text);
-
-        let mut fragment_start = old_fragments.start().visible;
-        for (range, new_text) in edits {
-            let new_text = LineEnding::normalize_arc(new_text.into());
-            let fragment_end = old_fragments.end().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.
-            if fragment_end < range.start {
-                // 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.start().visible {
-                    if fragment_end > fragment_start {
-                        let mut suffix = old_fragments.item().unwrap().clone();
-                        suffix.len = (fragment_end - fragment_start) as u32;
-                        suffix.insertion_offset +=
-                            (fragment_start - old_fragments.start().visible) as u32;
-                        new_insertions.push(InsertionFragment::insert_new(&suffix));
-                        new_ropes.push_fragment(&suffix, suffix.visible);
-                        new_fragments.push(suffix, &None);
-                    }
-                    old_fragments.next();
-                }
-
-                let slice = old_fragments.slice(&range.start, Bias::Right);
-                new_ropes.append(slice.summary().text);
-                new_fragments.append(slice, &None);
-                fragment_start = old_fragments.start().visible;
-            }
-
-            let full_range_start = FullOffset(range.start + old_fragments.start().deleted);
-
-            // Preserve any portion of the current fragment that precedes this range.
-            if fragment_start < range.start {
-                let mut prefix = old_fragments.item().unwrap().clone();
-                prefix.len = (range.start - fragment_start) as u32;
-                prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
-                prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id);
-                new_insertions.push(InsertionFragment::insert_new(&prefix));
-                new_ropes.push_fragment(&prefix, prefix.visible);
-                new_fragments.push(prefix, &None);
-                fragment_start = range.start;
-            }
-
-            // Insert the new text before any existing fragments within the range.
-            if !new_text.is_empty() {
-                let new_start = new_fragments.summary().text.visible;
-
-                let next_fragment_id = old_fragments
-                    .item()
-                    .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
-                Self::push_fragments_for_insertion(
-                    new_text.as_ref(),
-                    timestamp,
-                    &mut insertion_offset,
-                    &mut new_fragments,
-                    &mut new_insertions,
-                    &mut insertion_slices,
-                    &mut new_ropes,
-                    next_fragment_id,
-                    timestamp,
-                );
-                edits_patch.push(Edit {
-                    old: fragment_start..fragment_start,
-                    new: new_start..new_start + new_text.len(),
-                });
-            }
-
-            // Advance through every fragment that intersects this range, marking the intersecting
-            // portions as deleted.
-            while fragment_start < range.end {
-                let fragment = old_fragments.item().unwrap();
-                let fragment_end = old_fragments.end().visible;
-                let mut intersection = fragment.clone();
-                let intersection_end = cmp::min(range.end, fragment_end);
-                if fragment.visible {
-                    intersection.len = (intersection_end - fragment_start) as u32;
-                    intersection.insertion_offset +=
-                        (fragment_start - old_fragments.start().visible) as u32;
-                    intersection.id =
-                        Locator::between(&new_fragments.summary().max_id, &intersection.id);
-                    intersection.deletions.push(timestamp);
-                    intersection.visible = false;
-                }
-                if intersection.len > 0 {
-                    if fragment.visible && !intersection.visible {
-                        let new_start = new_fragments.summary().text.visible;
-                        edits_patch.push(Edit {
-                            old: fragment_start..intersection_end,
-                            new: new_start..new_start,
-                        });
-                        insertion_slices
-                            .push(InsertionSlice::from_fragment(timestamp, &intersection));
-                    }
-                    new_insertions.push(InsertionFragment::insert_new(&intersection));
-                    new_ropes.push_fragment(&intersection, fragment.visible);
-                    new_fragments.push(intersection, &None);
-                    fragment_start = intersection_end;
-                }
-                if fragment_end <= range.end {
-                    old_fragments.next();
-                }
-            }
-
-            let full_range_end = FullOffset(range.end + old_fragments.start().deleted);
-            edit_op.ranges.push(full_range_start..full_range_end);
-            edit_op.new_text.push(new_text);
-        }
-
-        // 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.start().visible {
-            let fragment_end = old_fragments.end().visible;
-            if fragment_end > fragment_start {
-                let mut suffix = old_fragments.item().unwrap().clone();
-                suffix.len = (fragment_end - fragment_start) as u32;
-                suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
-                new_insertions.push(InsertionFragment::insert_new(&suffix));
-                new_ropes.push_fragment(&suffix, suffix.visible);
-                new_fragments.push(suffix, &None);
-            }
-            old_fragments.next();
-        }
-
-        let suffix = old_fragments.suffix();
-        new_ropes.append(suffix.summary().text);
-        new_fragments.append(suffix, &None);
-        let (visible_text, deleted_text) = new_ropes.finish();
-        drop(old_fragments);
-
-        self.snapshot.fragments = new_fragments;
-        self.snapshot.insertions.edit(new_insertions, ());
-        self.snapshot.visible_text = visible_text;
-        self.snapshot.deleted_text = deleted_text;
+        let edits: Vec<_> = edits
+            .map(|(range, new_text)| (range.to_offset(&*self), new_text.into()))
+            .collect();
+        let (edit_op, edits_patch) = self.snapshot.apply_edit_internal(edits, timestamp);
         self.subscriptions.publish_mut(&edits_patch);
-        self.snapshot.insertion_slices.extend(insertion_slices);
         edit_op
     }
 
@@ -1211,7 +1047,7 @@ impl Buffer {
                 let next_fragment_id = old_fragments
                     .item()
                     .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
-                Self::push_fragments_for_insertion(
+                push_fragments_for_insertion(
                     new_text,
                     timestamp,
                     &mut insertion_offset,
@@ -1299,49 +1135,6 @@ impl Buffer {
         self.subscriptions.publish_mut(&edits_patch)
     }
 
-    fn push_fragments_for_insertion(
-        new_text: &str,
-        timestamp: clock::Lamport,
-        insertion_offset: &mut u32,
-        new_fragments: &mut SumTree<Fragment>,
-        new_insertions: &mut Vec<sum_tree::Edit<InsertionFragment>>,
-        insertion_slices: &mut Vec<InsertionSlice>,
-        new_ropes: &mut RopeBuilder,
-        next_fragment_id: &Locator,
-        edit_timestamp: clock::Lamport,
-    ) {
-        let mut text_offset = 0;
-        while text_offset < new_text.len() {
-            let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN);
-            let chunk_end = if target_end == new_text.len() {
-                target_end
-            } else {
-                new_text.floor_char_boundary(target_end)
-            };
-            if chunk_end == text_offset {
-                break;
-            }
-            let chunk_len = chunk_end - text_offset;
-
-            let fragment = Fragment {
-                id: Locator::between(&new_fragments.summary().max_id, next_fragment_id),
-                timestamp,
-                insertion_offset: *insertion_offset,
-                len: chunk_len as u32,
-                deletions: Default::default(),
-                max_undos: Default::default(),
-                visible: true,
-            };
-            insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment));
-            new_insertions.push(InsertionFragment::insert_new(&fragment));
-            new_fragments.push(fragment, &None);
-
-            *insertion_offset += chunk_len as u32;
-            text_offset = chunk_end;
-        }
-        new_ropes.push_str(new_text);
-    }
-
     fn fragment_ids_for_edits<'a>(
         &'a self,
         edit_ids: impl Iterator<Item = &'a clock::Lamport>,
@@ -2009,6 +1802,49 @@ impl Buffer {
     }
 }
 
+fn push_fragments_for_insertion(
+    new_text: &str,
+    timestamp: clock::Lamport,
+    insertion_offset: &mut u32,
+    new_fragments: &mut SumTree<Fragment>,
+    new_insertions: &mut Vec<sum_tree::Edit<InsertionFragment>>,
+    insertion_slices: &mut Vec<InsertionSlice>,
+    new_ropes: &mut RopeBuilder,
+    next_fragment_id: &Locator,
+    edit_timestamp: clock::Lamport,
+) {
+    let mut text_offset = 0;
+    while text_offset < new_text.len() {
+        let target_end = new_text.len().min(text_offset + MAX_INSERTION_LEN);
+        let chunk_end = if target_end == new_text.len() {
+            target_end
+        } else {
+            new_text.floor_char_boundary(target_end)
+        };
+        if chunk_end == text_offset {
+            break;
+        }
+        let chunk_len = chunk_end - text_offset;
+
+        let fragment = Fragment {
+            id: Locator::between(&new_fragments.summary().max_id, next_fragment_id),
+            timestamp,
+            insertion_offset: *insertion_offset,
+            len: chunk_len as u32,
+            deletions: Default::default(),
+            max_undos: Default::default(),
+            visible: true,
+        };
+        insertion_slices.push(InsertionSlice::from_fragment(edit_timestamp, &fragment));
+        new_insertions.push(InsertionFragment::insert_new(&fragment));
+        new_fragments.push(fragment, &None);
+
+        *insertion_offset += chunk_len as u32;
+        text_offset = chunk_end;
+    }
+    new_ropes.push_str(new_text);
+}
+
 impl Deref for Buffer {
     type Target = BufferSnapshot;
 
@@ -2018,6 +1854,189 @@ impl Deref for Buffer {
 }
 
 impl BufferSnapshot {
+    /// Edits the snapshot in place, applying the given edits to the text content.
+    /// This is useful for creating a modified snapshot without needing a full Buffer.
+    pub fn edit<I, S, T>(&mut self, edits: I)
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        let mut lamport_clock = clock::Lamport::new(ReplicaId::LOCAL_BRANCH);
+        let timestamp = lamport_clock.tick();
+        let edits: Vec<_> = edits
+            .into_iter()
+            .map(|(range, new_text)| (range.to_offset(self), new_text.into()))
+            .collect();
+        self.apply_edit_internal(edits, timestamp);
+        self.version.observe(timestamp);
+    }
+
+    fn apply_edit_internal(
+        &mut self,
+        edits: Vec<(Range<usize>, Arc<str>)>,
+        timestamp: clock::Lamport,
+    ) -> (EditOperation, Patch<usize>) {
+        let mut edits_patch = Patch::default();
+        let mut edit_op = EditOperation {
+            timestamp,
+            version: self.version.clone(),
+            ranges: Vec::with_capacity(edits.len()),
+            new_text: Vec::with_capacity(edits.len()),
+        };
+        let mut new_insertions = Vec::new();
+        let mut insertion_offset: u32 = 0;
+        let mut insertion_slices = Vec::new();
+
+        let mut edits = edits.into_iter().peekable();
+
+        if edits.peek().is_none() {
+            return (edit_op, edits_patch);
+        }
+
+        let mut new_ropes =
+            RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
+        let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>(&None);
+        let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right);
+        new_ropes.append(new_fragments.summary().text);
+
+        let mut fragment_start = old_fragments.start().visible;
+        for (range, new_text) in edits {
+            let new_text: Arc<str> = LineEnding::normalize_arc(new_text);
+            let fragment_end = old_fragments.end().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.
+            if fragment_end < range.start {
+                // 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.start().visible {
+                    if fragment_end > fragment_start {
+                        let mut suffix = old_fragments.item().unwrap().clone();
+                        suffix.len = (fragment_end - fragment_start) as u32;
+                        suffix.insertion_offset +=
+                            (fragment_start - old_fragments.start().visible) as u32;
+                        new_insertions.push(InsertionFragment::insert_new(&suffix));
+                        new_ropes.push_fragment(&suffix, suffix.visible);
+                        new_fragments.push(suffix, &None);
+                    }
+                    old_fragments.next();
+                }
+
+                let slice = old_fragments.slice(&range.start, Bias::Right);
+                new_ropes.append(slice.summary().text);
+                new_fragments.append(slice, &None);
+                fragment_start = old_fragments.start().visible;
+            }
+
+            let full_range_start = FullOffset(range.start + old_fragments.start().deleted);
+
+            // Preserve any portion of the current fragment that precedes this range.
+            if fragment_start < range.start {
+                let mut prefix = old_fragments.item().unwrap().clone();
+                prefix.len = (range.start - fragment_start) as u32;
+                prefix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
+                prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id);
+                new_insertions.push(InsertionFragment::insert_new(&prefix));
+                new_ropes.push_fragment(&prefix, prefix.visible);
+                new_fragments.push(prefix, &None);
+                fragment_start = range.start;
+            }
+
+            // Insert the new text before any existing fragments within the range.
+            if !new_text.is_empty() {
+                let new_start = new_fragments.summary().text.visible;
+
+                let next_fragment_id = old_fragments
+                    .item()
+                    .map_or(Locator::max_ref(), |old_fragment| &old_fragment.id);
+                push_fragments_for_insertion(
+                    new_text.as_ref(),
+                    timestamp,
+                    &mut insertion_offset,
+                    &mut new_fragments,
+                    &mut new_insertions,
+                    &mut insertion_slices,
+                    &mut new_ropes,
+                    next_fragment_id,
+                    timestamp,
+                );
+                edits_patch.push(Edit {
+                    old: fragment_start..fragment_start,
+                    new: new_start..new_start + new_text.len(),
+                });
+            }
+
+            // Advance through every fragment that intersects this range, marking the intersecting
+            // portions as deleted.
+            while fragment_start < range.end {
+                let fragment = old_fragments.item().unwrap();
+                let fragment_end = old_fragments.end().visible;
+                let mut intersection = fragment.clone();
+                let intersection_end = cmp::min(range.end, fragment_end);
+                if fragment.visible {
+                    intersection.len = (intersection_end - fragment_start) as u32;
+                    intersection.insertion_offset +=
+                        (fragment_start - old_fragments.start().visible) as u32;
+                    intersection.id =
+                        Locator::between(&new_fragments.summary().max_id, &intersection.id);
+                    intersection.deletions.push(timestamp);
+                    intersection.visible = false;
+                }
+                if intersection.len > 0 {
+                    if fragment.visible && !intersection.visible {
+                        let new_start = new_fragments.summary().text.visible;
+                        edits_patch.push(Edit {
+                            old: fragment_start..intersection_end,
+                            new: new_start..new_start,
+                        });
+                        insertion_slices
+                            .push(InsertionSlice::from_fragment(timestamp, &intersection));
+                    }
+                    new_insertions.push(InsertionFragment::insert_new(&intersection));
+                    new_ropes.push_fragment(&intersection, fragment.visible);
+                    new_fragments.push(intersection, &None);
+                    fragment_start = intersection_end;
+                }
+                if fragment_end <= range.end {
+                    old_fragments.next();
+                }
+            }
+
+            let full_range_end = FullOffset(range.end + old_fragments.start().deleted);
+            edit_op.ranges.push(full_range_start..full_range_end);
+            edit_op.new_text.push(new_text);
+        }
+
+        // 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.start().visible {
+            let fragment_end = old_fragments.end().visible;
+            if fragment_end > fragment_start {
+                let mut suffix = old_fragments.item().unwrap().clone();
+                suffix.len = (fragment_end - fragment_start) as u32;
+                suffix.insertion_offset += (fragment_start - old_fragments.start().visible) as u32;
+                new_insertions.push(InsertionFragment::insert_new(&suffix));
+                new_ropes.push_fragment(&suffix, suffix.visible);
+                new_fragments.push(suffix, &None);
+            }
+            old_fragments.next();
+        }
+
+        let suffix = old_fragments.suffix();
+        new_ropes.append(suffix.summary().text);
+        new_fragments.append(suffix, &None);
+        let (visible_text, deleted_text) = new_ropes.finish();
+        drop(old_fragments);
+
+        self.fragments = new_fragments;
+        self.insertions.edit(new_insertions, ());
+        self.visible_text = visible_text;
+        self.deleted_text = deleted_text;
+        self.insertion_slices.extend(insertion_slices);
+        (edit_op, edits_patch)
+    }
+
     pub fn as_rope(&self) -> &Rope {
         &self.visible_text
     }