Detailed changes
@@ -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;
@@ -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);
@@ -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
@@ -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()
@@ -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| {
@@ -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
}