From 0fa26c2c6ec7671dbb956438abbf41d56d9e3433 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Apr 2026 15:55:43 -0700 Subject: [PATCH] Remove Buffer branch APIs Update preview_edits to use a new simpler API, BufferSnapshot::edit, that applies edits to a buffer snapshot without affecting its original buffer. --- 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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e6f597de7ff9138b226cd2474353ef8c2ce16ebb..27599ea29b45b4189bc75ddf6da5cb0623ef2448 100644 --- a/crates/editor/src/editor.rs +++ b/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.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.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) { if hovered != self.gutter_hovered { self.gutter_hovered = hovered; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 512fbb8855aa11d8c540065a55eb296919012821..536f01c3e0b2178a2bd9d97458f6cffc1f4949ef 100644 --- a/crates/editor/src/element.rs +++ b/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); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d2c157014330cc26f0024ace87ee0e3688f85eaa..43cf1542be6da15396f8a93175a87675a66a1e77 100644 --- a/crates/editor/src/items.rs +++ b/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::>(); + let buffers = buffers.into_iter().collect::>(); let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave { buffers diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1e54134efcab4f0074a73b241f8e0d04cfbcbcdd..261991d751b3f07d78f45720d3c15539783c461e 100644 --- a/crates/language/src/buffer.rs +++ b/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, /// Filesystem state, `None` when there is no path. file: Option>, /// 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, - merged_operations: Vec, -} - /// 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) -> Entity { - 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>, cx: &mut Context) { - 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::(&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::(), - )); - } - } - - 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, - event: &BufferEvent, - cx: &mut Context, - ) { - 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> { - Some(self.branch_state.as_ref()?.base_buffer.clone()) - } - /// Returns the primary [`Language`] assigned to this [`Buffer`]. pub fn language(&self) -> Option<&Arc> { self.language.as_ref() diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9f4562bf547f389c5ecc5ca29470ac4e49da0e04..ab57dbedb08c7e133c69a78bb122771853781c76 100644 --- a/crates/language/src/buffer_tests.rs +++ b/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| { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 026f1272790740c9c2277004e8e96800d87bab15..2a435dd24e9de2f60c896e73a5140116aac3ec91 100644 --- a/crates/text/src/text.rs +++ b/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, 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::(&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, - new_insertions: &mut Vec>, - insertion_slices: &mut Vec, - 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, @@ -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, + new_insertions: &mut Vec>, + insertion_slices: &mut Vec, + 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(&mut self, edits: I) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + 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, Arc)>, + timestamp: clock::Lamport, + ) -> (EditOperation, Patch) { + 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::(&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 = 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 }