From 554382a87e2f51496c2f907554e309d0e589b3d2 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 31 Dec 2025 20:02:45 -0500 Subject: [PATCH] git: Rework side-by-side diff to use distinct buffers for the left-hand side (#44838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR reworks the (still feature-gated) side-by-side diff view to use a different approach to representing the multibuffers on the left- and right-hand sides. Previously, these two multibuffers used identical sets of buffers and excerpts, and were made to behave differently by adding a new knob to the multibuffer controlling how diffs are displayed. Specifically, the left-hand side multibuffer would filter out the added range of each hunk from the excerpts using a new `FilteredInsertedHunk` diff transform, and the right-hand side would simply not show the deleted sides of expanded hunks. This approach has some problems: - Line numbers, and actions that navigate by line number, behaved incorrectly for the left-hand side. - Syntax highlighting and other features that use the buffer syntax tree also behaved incorrectly for the left-hand side. In this PR, we've switched to using independent buffers to build the left-hand side. These buffers are constructed using the base texts for the corresponding diffs, and their lifecycle is managed by `BufferDiff`. The red "deleted" regions on the left-hand side are represented by `BufferContent` diff transforms, not `DeletedHunk` transforms. This means each excerpt on the left represents a contiguous slice of a single buffer, which fixes the above issues by construction. The tradeoff with this new approach is that we now have to manually synchronize excerpt ranges from the right side to the left, which we do using `BufferDiffSnapshot::row_to_base_text_row`. Release Notes: - N/A --------- Co-authored-by: cameron Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com> Co-authored-by: Miguel Raz Guzmán Macedo Co-authored-by: Anthony Co-authored-by: Cameron --- Cargo.lock | 1 - crates/acp_thread/src/diff.rs | 125 +- crates/action_log/src/action_log.rs | 53 +- crates/agent_ui/src/agent_diff.rs | 6 +- crates/buffer_diff/Cargo.toml | 1 - crates/buffer_diff/src/buffer_diff.rs | 1112 ++++++------ crates/collab/src/tests/integration_tests.rs | 54 +- .../random_project_collaboration_tests.rs | 4 +- .../src/rate_prediction_modal.rs | 17 +- crates/editor/src/display_map.rs | 2 - crates/editor/src/display_map/wrap_map.rs | 1 - crates/editor/src/editor.rs | 70 +- crates/editor/src/editor_tests.rs | 262 +-- crates/editor/src/element.rs | 4 +- crates/editor/src/split.rs | 660 ++++++- crates/eval/src/example.rs | 7 +- crates/git_ui/src/commit_view.rs | 38 +- crates/git_ui/src/file_diff_view.rs | 55 +- crates/git_ui/src/project_diff.rs | 67 +- crates/git_ui/src/text_diff_view.rs | 16 +- crates/multi_buffer/src/anchor.rs | 8 +- crates/multi_buffer/src/multi_buffer.rs | 1552 +++++++++-------- crates/multi_buffer/src/multi_buffer_tests.rs | 855 +++++---- crates/multi_buffer/src/path_key.rs | 20 +- crates/project/src/git_store.rs | 79 +- crates/project/src/git_store/branch_diff.rs | 4 +- crates/project/src/project_tests.rs | 142 +- .../remote_server/src/remote_editing_tests.rs | 35 +- crates/rope/src/chunk.rs | 2 +- crates/text/src/text.rs | 1 + 30 files changed, 3038 insertions(+), 2215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 616e898868398306b4ccae0b67f3072d44bccf95..d8f8939b5dbc1ff7cce3b84c68ddb4d98392b465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2381,7 +2381,6 @@ dependencies = [ name = "buffer_diff" version = "0.1.0" dependencies = [ - "anyhow", "clock", "ctor", "futures 0.3.31", diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index cae1aad90810c217324659d29c065af443494933..80e901a4f1e8e02dda7860694782a318eb1323d7 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use buffer_diff::BufferDiff; use editor::{MultiBuffer, PathKey, multibuffer_context_lines}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; use itertools::Itertools; use language::{ - Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer, + Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer, }; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; use util::ResultExt; @@ -49,15 +49,15 @@ impl Diff { .update(cx, |multibuffer, cx| { let hunk_ranges = { let buffer = buffer.read(cx); - let diff = diff.read(cx); - diff.hunks_intersecting_range( - Anchor::min_for_buffer(buffer.remote_id()) - ..Anchor::max_for_buffer(buffer.remote_id()), - buffer, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>() + diff.read(cx) + .snapshot(cx) + .hunks_intersecting_range( + Anchor::min_for_buffer(buffer.remote_id()) + ..Anchor::max_for_buffer(buffer.remote_id()), + buffer, + ) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) + .collect::>() }; multibuffer.set_excerpts_for_path( @@ -86,17 +86,9 @@ impl Diff { pub fn new(buffer: Entity, cx: &mut Context) -> Self { let buffer_text_snapshot = buffer.read(cx).text_snapshot(); - let base_text_snapshot = buffer.read(cx).snapshot(); - let base_text = base_text_snapshot.text(); - debug_assert_eq!(buffer_text_snapshot.text(), base_text); let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); - let snapshot = diff.snapshot(cx); - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); - diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); - diff - }); + let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx); + let secondary_diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_text_snapshot, cx)); diff.set_secondary_diff(secondary_diff); diff }); @@ -109,7 +101,7 @@ impl Diff { Self::Pending(PendingDiff { multibuffer, - base_text: Arc::new(base_text), + base_text: Arc::from(buffer_text_snapshot.text().as_str()), _subscription: cx.observe(&buffer, |this, _, cx| { if let Diff::Pending(diff) = this { diff.update(cx); @@ -176,7 +168,7 @@ impl Diff { new_buffer, .. }) => { - base_text.as_str() != old_text + base_text.as_ref() != old_text || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) } Diff::Finalized(FinalizedDiff { @@ -184,7 +176,7 @@ impl Diff { new_buffer, .. }) => { - base_text.as_str() != old_text + base_text.as_ref() != old_text || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) } } @@ -193,7 +185,7 @@ impl Diff { pub struct PendingDiff { multibuffer: Entity, - base_text: Arc, + base_text: Arc, new_buffer: Entity, diff: Entity, revealed_ranges: Vec>, @@ -208,21 +200,22 @@ impl PendingDiff { let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; - let diff_snapshot = BufferDiff::update_diff( - buffer_diff.clone(), - text_snapshot.clone(), - Some(base_text), - false, - false, - None, - None, - cx, - ) - .await?; + let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?; + let update = buffer_diff + .update(cx, |diff, cx| { + diff.update_diff( + text_snapshot.clone(), + Some(base_text.clone()), + false, + language, + cx, + ) + })? + .await; buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + diff.set_snapshot(update.clone(), &text_snapshot, cx); diff.secondary_diff().unwrap().update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + diff.set_snapshot(update, &text_snapshot, cx); }); })?; diff.update(cx, |diff, cx| { @@ -319,13 +312,14 @@ impl PendingDiff { fn excerpt_ranges(&self, cx: &App) -> Vec> { let buffer = self.new_buffer.read(cx); - let diff = self.diff.read(cx); - let mut ranges = diff + let mut ranges = self + .diff + .read(cx) + .snapshot(cx) .hunks_intersecting_range( Anchor::min_for_buffer(buffer.remote_id()) ..Anchor::max_for_buffer(buffer.remote_id()), buffer, - cx, ) .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>(); @@ -357,60 +351,47 @@ impl PendingDiff { pub struct FinalizedDiff { path: String, - base_text: Arc, + base_text: Arc, new_buffer: Entity, multibuffer: Entity, _update_diff: Task>, } async fn build_buffer_diff( - old_text: Arc, + old_text: Arc, buffer: &Entity, language_registry: Option>, cx: &mut AsyncApp, ) -> Result> { + let language = cx.update(|cx| buffer.read(cx).language().cloned())?; let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - let old_text_rope = cx - .background_spawn({ - let old_text = old_text.clone(); - async move { Rope::from(old_text.as_str()) } - }) - .await; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text_rope, - buffer.language().cloned(), - language_registry, - cx, - ) - })? - .await; + let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?; - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( + let update = secondary_diff + .update(cx, |secondary_diff, cx| { + secondary_diff.update_diff( buffer.text.clone(), Some(old_text), - base_buffer, + true, + language.clone(), cx, ) })? .await; - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); - diff + secondary_diff.update(cx, |secondary_diff, cx| { + secondary_diff.language_changed(language.clone(), language_registry.clone(), cx); + secondary_diff.set_snapshot(update.clone(), &buffer, cx); })?; - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer, cx); + let diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?; + diff.update(cx, |diff, cx| { + diff.language_changed(language, language_registry, cx); + diff.set_snapshot(update.clone(), &buffer, cx); diff.set_secondary_diff(secondary_diff); - diff - }) + })?; + Ok(diff) } #[cfg(test)] diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 994780c40b9bd1cde45bfe6ba26630771a3040c3..5af44dd3f5d2621c9633d413c2f52fc54e9fdf2a 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -262,7 +262,7 @@ impl ActionLog { ); } - (Arc::new(base_text.to_string()), base_text) + (Arc::from(base_text.to_string().as_str()), base_text) } }); @@ -302,7 +302,7 @@ impl ActionLog { .context("buffer not tracked")?; let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let agent_diff_base = tracked_buffer.diff_base.clone(); - let git_diff_base = git_diff.read(cx).base_text().as_rope().clone(); + let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone(); let buffer_text = tracked_buffer.snapshot.as_rope().clone(); anyhow::Ok(cx.background_spawn(async move { let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable(); @@ -352,7 +352,7 @@ impl ActionLog { } ( - Arc::new(new_agent_diff_base.to_string()), + Arc::from(new_agent_diff_base.to_string().as_str()), new_agent_diff_base, ) })) @@ -374,11 +374,11 @@ impl ActionLog { this: &WeakEntity, buffer: &Entity, buffer_snapshot: text::BufferSnapshot, - new_base_text: Arc, + new_base_text: Arc, new_diff_base: Rope, cx: &mut AsyncApp, ) -> Result<()> { - let (diff, language, language_registry) = this.read_with(cx, |this, cx| { + let (diff, language) = this.read_with(cx, |this, cx| { let tracked_buffer = this .tracked_buffers .get(buffer) @@ -386,25 +386,28 @@ impl ActionLog { anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry(), )) })??; - let diff_snapshot = BufferDiff::update_diff( - diff.clone(), - buffer_snapshot.clone(), - Some(new_base_text), - true, - false, - language, - language_registry, - cx, - ) - .await; + let update = diff.update(cx, |diff, cx| { + diff.update_diff( + buffer_snapshot.clone(), + Some(new_base_text), + true, + language, + cx, + ) + }); let mut unreviewed_edits = Patch::default(); - if let Ok(diff_snapshot) = diff_snapshot { + if let Ok(update) = update { + let update = update.await; + + let diff_snapshot = diff.update(cx, |diff, cx| { + diff.set_snapshot(update.clone(), &buffer_snapshot, cx); + diff.snapshot(cx) + })?; + unreviewed_edits = cx .background_spawn({ - let diff_snapshot = diff_snapshot.clone(); let buffer_snapshot = buffer_snapshot.clone(); let new_diff_base = new_diff_base.clone(); async move { @@ -431,10 +434,6 @@ impl ActionLog { } }) .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx); - })?; } this.update(cx, |this, cx| { let tracked_buffer = this @@ -975,7 +974,8 @@ impl TrackedBuffer { fn has_edits(&self, cx: &App) -> bool { self.diff .read(cx) - .hunks(self.buffer.read(cx), cx) + .snapshot(cx) + .hunks(self.buffer.read(cx)) .next() .is_some() } @@ -2388,13 +2388,14 @@ mod tests { ( buffer, diff.read(cx) - .hunks(&snapshot, cx) + .snapshot(cx) + .hunks(&snapshot) .map(|hunk| HunkStatus { diff_status: hunk.status().kind, range: hunk.range, old_text: diff .read(cx) - .base_text() + .base_text(cx) .text_for_range(hunk.diff_base_byte_range) .collect(), }) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b1db9837482ec9a377615d5041a0a688e7afa884..d7bb06eec07a1f6c65ffd1a6a457f2fcc9d27090 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -146,13 +146,13 @@ impl AgentDiffPane { paths_to_delete.remove(&path_key); let snapshot = buffer.read(cx).snapshot(); - let diff = diff_handle.read(cx); - let diff_hunk_ranges = diff + let diff_hunk_ranges = diff_handle + .read(cx) + .snapshot(cx) .hunks_intersecting_range( language::Anchor::min_max_range_for_buffer(snapshot.remote_id()), &snapshot, - cx, ) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::>(); diff --git a/crates/buffer_diff/Cargo.toml b/crates/buffer_diff/Cargo.toml index 6249ae418c593f5ae8bca3408d8f5f25df7c871b..e0c56c6d3d8d71b1749d0fdc99ef515511fe5196 100644 --- a/crates/buffer_diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -15,7 +15,6 @@ path = "src/buffer_diff.rs" test-support = ["settings"] [dependencies] -anyhow.workspace = true clock.workspace = true futures.workspace = true git2.workspace = true diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index bce2bed058e9bfe27c54df5c978ad23bc2896726..75abab174128f6e3ed678404fd095387d68f4119 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1,14 +1,13 @@ use futures::channel::oneshot; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel}; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task, TaskLabel}; use language::{ - BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry, + BufferRow, Capability, DiffOptions, File, Language, LanguageName, LanguageRegistry, language_settings::language_settings, word_diff_ranges, }; use rope::Rope; use std::{ cmp::Ordering, - future::Future, iter, ops::Range, sync::{Arc, LazyLock}, @@ -22,23 +21,37 @@ pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, - inner: BufferDiffInner, + inner: BufferDiffInner>, // diff of the index vs head secondary_diff: Option>, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct BufferDiffSnapshot { - inner: BufferDiffInner, + inner: BufferDiffInner, secondary_diff: Option>, } +impl std::fmt::Debug for BufferDiffSnapshot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BufferDiffSnapshot") + .field("inner", &self.inner) + .field("secondary_diff", &self.secondary_diff) + .finish() + } +} + +#[derive(Clone)] +pub struct BufferDiffUpdate { + base_text_changed: bool, + inner: BufferDiffInner>, +} + #[derive(Clone)] -struct BufferDiffInner { +struct BufferDiffInner { hunks: SumTree, - // Used for making staging mo pending_hunks: SumTree, - base_text: language::BufferSnapshot, + base_text: BaseText, base_text_exists: bool, } @@ -175,7 +188,7 @@ impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor { } } -impl std::fmt::Debug for BufferDiffInner { +impl std::fmt::Debug for BufferDiffInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) @@ -185,129 +198,14 @@ impl std::fmt::Debug for BufferDiffInner { } impl BufferDiffSnapshot { - pub fn buffer_diff_id(&self) -> BufferId { - self.inner.base_text.remote_id() - } - - fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot { - BufferDiffSnapshot { - inner: BufferDiffInner { - base_text: language::Buffer::build_empty_snapshot(cx), - hunks: SumTree::new(buffer), - pending_hunks: SumTree::new(buffer), - base_text_exists: false, - }, - secondary_diff: None, - } - } - - fn unchanged( - buffer: &text::BufferSnapshot, - base_text: language::BufferSnapshot, - ) -> BufferDiffSnapshot { - debug_assert_eq!(buffer.text(), base_text.text()); - BufferDiffSnapshot { - inner: BufferDiffInner { - base_text, - hunks: SumTree::new(buffer), - pending_hunks: SumTree::new(buffer), - base_text_exists: false, - }, - secondary_diff: None, - } - } - - fn new_with_base_text( - buffer: text::BufferSnapshot, - base_text: Option>, - language: Option>, - language_registry: Option>, - cx: &mut App, - ) -> impl Future + use<> { - let base_text_pair; - let base_text_exists; - let base_text_snapshot; - let diff_options = build_diff_options( - None, - language.as_ref().map(|l| l.name()), - language.as_ref().map(|l| l.default_scope()), - cx, - ); - - if let Some(text) = &base_text { - let base_text_rope = Rope::from(text.as_str()); - base_text_pair = Some((text.clone(), base_text_rope.clone())); - let snapshot = - language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); - base_text_snapshot = cx.background_spawn(snapshot); - base_text_exists = true; - } else { - base_text_pair = None; - base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx)); - base_text_exists = false; - }; - - let hunks = cx - .background_executor() - .spawn_labeled(*CALCULATE_DIFF_TASK, { - let buffer = buffer.clone(); - async move { compute_hunks(base_text_pair, buffer, diff_options) } - }); - - async move { - let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); - Self { - inner: BufferDiffInner { - base_text, - hunks, - base_text_exists, - pending_hunks: SumTree::new(&buffer), - }, - secondary_diff: None, - } - } - } - - pub fn new_with_base_buffer( - buffer: text::BufferSnapshot, - base_text: Option>, - base_text_snapshot: language::BufferSnapshot, - cx: &App, - ) -> impl Future + use<> { - let diff_options = build_diff_options( - base_text_snapshot.file(), - base_text_snapshot.language().map(|l| l.name()), - base_text_snapshot.language().map(|l| l.default_scope()), - cx, - ); - let base_text_exists = base_text.is_some(); - let base_text_pair = base_text.map(|text| { - debug_assert_eq!(&*text, &base_text_snapshot.text()); - (text, base_text_snapshot.as_rope().clone()) - }); - cx.background_executor() - .spawn_labeled(*CALCULATE_DIFF_TASK, async move { - Self { - inner: BufferDiffInner { - base_text: base_text_snapshot, - pending_hunks: SumTree::new(&buffer), - hunks: compute_hunks(base_text_pair, buffer, diff_options), - base_text_exists, - }, - secondary_diff: None, - } - }) - } - #[cfg(test)] fn new_sync( buffer: text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { - cx.executor().block(cx.update(|cx| { - Self::new_with_base_text(buffer, Some(Arc::new(diff_base)), None, None, cx) - })) + let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx)) } pub fn is_empty(&self) -> bool { @@ -339,61 +237,148 @@ impl BufferDiffSnapshot { range: Range, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator { - self.inner.hunks_intersecting_range_rev(range, buffer) + let filter = move |summary: &DiffHunkSummary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }; + self.inner.hunks_intersecting_range_rev_impl(filter, buffer) + } + + pub fn hunks_intersecting_base_text_range<'a>( + &'a self, + range: Range, + main_buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); + let filter = move |summary: &DiffHunkSummary| { + let before_start = summary.diff_base_byte_range.end < range.start; + let after_end = summary.diff_base_byte_range.start > range.end; + !before_start && !after_end + }; + self.inner + .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) + } + + pub fn hunks_intersecting_base_text_range_rev<'a>( + &'a self, + range: Range, + main_buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + let filter = move |summary: &DiffHunkSummary| { + let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt(); + let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt(); + !before_start && !after_end + }; + self.inner + .hunks_intersecting_range_rev_impl(filter, main_buffer) + } + + pub fn hunks<'a>( + &'a self, + buffer_snapshot: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.hunks_intersecting_range( + Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()), + buffer_snapshot, + ) + } + + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + let start = buffer.anchor_before(Point::new(range.start, 0)); + let end = buffer.anchor_after(Point::new(range.end, 0)); + self.hunks_intersecting_range(start..end, buffer) + } + + pub fn range_to_hunk_range( + &self, + range: Range, + buffer: &text::BufferSnapshot, + ) -> (Option>, Option>) { + let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next(); + let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next(); + let range = first_hunk + .as_ref() + .zip(last_hunk.as_ref()) + .map(|(first, last)| first.buffer_range.start..last.buffer_range.end); + let base_text_range = first_hunk + .zip(last_hunk) + .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end); + (range, base_text_range) } pub fn base_text(&self) -> &language::BufferSnapshot { &self.inner.base_text } - pub fn base_texts_eq(&self, other: &Self) -> bool { + /// If this function returns `true`, the base texts are equal. If this + /// function returns `false`, they might be equal, but might not. This + /// result is used to avoid recalculating diffs in situations where we know + /// nothing has changed. + pub fn base_texts_definitely_eq(&self, other: &Self) -> bool { if self.inner.base_text_exists != other.inner.base_text_exists { return false; } let left = &self.inner.base_text; let right = &other.inner.base_text; - let (old_id, old_empty) = (left.remote_id(), left.is_empty()); - let (new_id, new_empty) = (right.remote_id(), right.is_empty()); - new_id == old_id || (new_empty && old_empty) + let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty()); + let (new_id, new_version, new_empty) = + (right.remote_id(), right.version(), right.is_empty()); + (new_id == old_id && new_version == old_version) || (new_empty && old_empty) } - pub fn row_to_base_text_row(&self, row: BufferRow, buffer: &text::BufferSnapshot) -> u32 { + pub fn row_to_base_text_row( + &self, + row: BufferRow, + bias: Bias, + buffer: &text::BufferSnapshot, + ) -> u32 { // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start - - // Find the last hunk that starts before this position. + let target = buffer.anchor_before(Point::new(row, 0)); + // Find the last hunk that starts before the target. let mut cursor = self.inner.hunks.cursor::(buffer); - let position = buffer.anchor_before(Point::new(row, 0)); - cursor.seek(&position, Bias::Left); + cursor.seek(&target, Bias::Left); if cursor .item() - .is_none_or(|hunk| hunk.buffer_range.start.cmp(&position, buffer).is_gt()) + .is_none_or(|hunk| hunk.buffer_range.start.cmp(&target, buffer).is_gt()) { cursor.prev(); } let unclipped_point = if let Some(hunk) = cursor.item() - && hunk.buffer_range.start.cmp(&position, buffer).is_le() + && hunk.buffer_range.start.cmp(&target, buffer).is_le() { - let mut unclipped_point = cursor - .end() - .diff_base_byte_range - .end - .to_point(self.base_text()); - if position.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { + // Found a hunk that starts before the target. + let hunk_base_text_end = cursor.end().diff_base_byte_range.end; + let unclipped_point = if target.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { + // Target falls strictly between two hunks. + let mut unclipped_point = hunk_base_text_end.to_point(self.base_text()); unclipped_point += Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); - } + unclipped_point + } else if bias == Bias::Right { + hunk_base_text_end.to_point(self.base_text()) + } else { + hunk.diff_base_byte_range.start.to_point(self.base_text()) + }; // Move the cursor so that at the next step we can clip with the start of the next hunk. cursor.next(); unclipped_point } else { - // Position is before the added region for the first hunk. + // Target is before the added region for the first hunk. debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { - position.cmp(&first_hunk.buffer_range.start, buffer).is_le() + target.cmp(&first_hunk.buffer_range.start, buffer).is_le() })); Point::new(row, 0) }; + // If the target falls in the region between two hunks, we added an overshoot above. + // There may be changes in the main buffer that are not reflected in the hunks, + // so we need to ensure this overshoot keeps us in the corresponding base text region. let max_point = if let Some(next_hunk) = cursor.item() { next_hunk .diff_base_byte_range @@ -406,7 +391,7 @@ impl BufferDiffSnapshot { } } -impl BufferDiffInner { +impl BufferDiffInner> { /// Returns the new index text and new pending hunks. fn stage_or_unstage_hunks_impl( &mut self, @@ -415,13 +400,14 @@ impl BufferDiffInner { hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, + cx: &mut Context, ) -> Option { let head_text = self .base_text_exists - .then(|| self.base_text.as_rope().clone()); + .then(|| self.base_text.read(cx).as_rope().clone()); let index_text = unstaged_diff .base_text_exists - .then(|| unstaged_diff.base_text.as_rope().clone()); + .then(|| unstaged_diff.base_text.read(cx).as_rope().clone()); // If the file doesn't exist in either HEAD or the index, then the // entire file must be either created or deleted in the index. @@ -614,7 +600,9 @@ impl BufferDiffInner { new_index_text.append(index_cursor.suffix()); Some(new_index_text) } +} +impl BufferDiffInner { fn hunks_intersecting_range<'a>( &'a self, range: Range, @@ -622,15 +610,22 @@ impl BufferDiffInner { secondary: Option<&'a Self>, ) -> impl 'a + Iterator { let range = range.to_offset(buffer); + let filter = move |summary: &DiffHunkSummary| { + let summary_range = summary.buffer_range.to_offset(buffer); + let before_start = summary_range.end < range.start; + let after_end = summary_range.start > range.end; + !before_start && !after_end + }; + self.hunks_intersecting_range_impl(filter, buffer, secondary) + } - let mut cursor = self - .hunks - .filter::<_, DiffHunkSummary>(buffer, move |summary| { - let summary_range = summary.buffer_range.to_offset(buffer); - let before_start = summary_range.end < range.start; - let after_end = summary_range.start > range.end; - !before_start && !after_end - }); + fn hunks_intersecting_range_impl<'a>( + &'a self, + filter: impl 'a + Fn(&DiffHunkSummary) -> bool, + buffer: &'a text::BufferSnapshot, + secondary: Option<&'a Self>, + ) -> impl 'a + Iterator { + let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter); let anchor_iter = iter::from_fn(move || { cursor.next(); @@ -749,18 +744,12 @@ impl BufferDiffInner { }) } - fn hunks_intersecting_range_rev<'a>( + fn hunks_intersecting_range_rev_impl<'a>( &'a self, - range: Range, + filter: impl 'a + Fn(&DiffHunkSummary) -> bool, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator { - let mut cursor = self - .hunks - .filter::<_, DiffHunkSummary>(buffer, move |summary| { - let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); - let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); - !before_start && !after_end - }); + let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter); iter::from_fn(move || { cursor.prev(); @@ -779,69 +768,6 @@ impl BufferDiffInner { }) }) } - - fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option> { - let mut new_cursor = self.hunks.cursor::<()>(new_snapshot); - let mut old_cursor = old.hunks.cursor::<()>(new_snapshot); - old_cursor.next(); - new_cursor.next(); - let mut start = None; - let mut end = None; - - loop { - match (new_cursor.item(), old_cursor.item()) { - (Some(new_hunk), Some(old_hunk)) => { - match new_hunk - .buffer_range - .start - .cmp(&old_hunk.buffer_range.start, new_snapshot) - { - Ordering::Less => { - start.get_or_insert(new_hunk.buffer_range.start); - end.replace(new_hunk.buffer_range.end); - new_cursor.next(); - } - Ordering::Equal => { - if new_hunk != old_hunk { - start.get_or_insert(new_hunk.buffer_range.start); - if old_hunk - .buffer_range - .end - .cmp(&new_hunk.buffer_range.end, new_snapshot) - .is_ge() - { - end.replace(old_hunk.buffer_range.end); - } else { - end.replace(new_hunk.buffer_range.end); - } - } - - new_cursor.next(); - old_cursor.next(); - } - Ordering::Greater => { - start.get_or_insert(old_hunk.buffer_range.start); - end.replace(old_hunk.buffer_range.end); - old_cursor.next(); - } - } - } - (Some(new_hunk), None) => { - start.get_or_insert(new_hunk.buffer_range.start); - end.replace(new_hunk.buffer_range.end); - new_cursor.next(); - } - (None, Some(old_hunk)) => { - start.get_or_insert(old_hunk.buffer_range.start); - end.replace(old_hunk.buffer_range.end); - old_cursor.next(); - } - (None, None) => break, - } - } - - start.zip(end).map(|(start, end)| start..end) - } } fn build_diff_options( @@ -871,7 +797,7 @@ fn build_diff_options( } fn compute_hunks( - diff_base: Option<(Arc, Rope)>, + diff_base: Option<(Arc, Rope)>, buffer: text::BufferSnapshot, diff_options: Option, ) -> SumTree { @@ -936,6 +862,98 @@ fn compute_hunks( tree } +fn compare_hunks( + new_hunks: &SumTree, + old_hunks: &SumTree, + new_snapshot: &text::BufferSnapshot, +) -> (Option>, Option>) { + let mut new_cursor = new_hunks.cursor::<()>(new_snapshot); + let mut old_cursor = old_hunks.cursor::<()>(new_snapshot); + old_cursor.next(); + new_cursor.next(); + let mut start = None; + let mut end = None; + let mut base_text_start = None; + let mut base_text_end = None; + + loop { + match (new_cursor.item(), old_cursor.item()) { + (Some(new_hunk), Some(old_hunk)) => { + match new_hunk + .buffer_range + .start + .cmp(&old_hunk.buffer_range.start, new_snapshot) + { + Ordering::Less => { + start.get_or_insert(new_hunk.buffer_range.start); + base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + end.replace(new_hunk.buffer_range.end); + base_text_end.replace(new_hunk.diff_base_byte_range.end); + new_cursor.next(); + } + Ordering::Equal => { + if new_hunk != old_hunk { + start.get_or_insert(new_hunk.buffer_range.start); + base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + if old_hunk + .buffer_range + .end + .cmp(&new_hunk.buffer_range.end, new_snapshot) + .is_ge() + { + end.replace(old_hunk.buffer_range.end); + } else { + end.replace(new_hunk.buffer_range.end); + } + + base_text_end.replace( + old_hunk + .diff_base_byte_range + .end + .max(new_hunk.diff_base_byte_range.end), + ); + } + + new_cursor.next(); + old_cursor.next(); + } + Ordering::Greater => { + start.get_or_insert(old_hunk.buffer_range.start); + base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start); + end.replace(old_hunk.buffer_range.end); + base_text_end.replace(old_hunk.diff_base_byte_range.end); + old_cursor.next(); + } + } + } + (Some(new_hunk), None) => { + start.get_or_insert(new_hunk.buffer_range.start); + base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + // TODO(cole) it seems like this could move end backward? + end.replace(new_hunk.buffer_range.end); + base_text_end = base_text_end.max(Some(new_hunk.diff_base_byte_range.end)); + new_cursor.next(); + } + (None, Some(old_hunk)) => { + start.get_or_insert(old_hunk.buffer_range.start); + base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start); + // TODO(cole) it seems like this could move end backward? + end.replace(old_hunk.buffer_range.end); + base_text_end = base_text_end.max(Some(old_hunk.diff_base_byte_range.end)); + old_cursor.next(); + } + (None, None) => break, + } + } + + ( + start.zip(end).map(|(start, end)| start..end), + base_text_start + .zip(base_text_end) + .map(|(start, end)| start..end), + ) +} + fn process_patch_hunk( patch: &GitPatch<'_>, hunk_index: usize, @@ -1057,7 +1075,6 @@ impl std::fmt::Debug for BufferDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferChangeSet") .field("buffer_id", &self.buffer_id) - .field("snapshot", &self.inner) .finish() } } @@ -1066,6 +1083,7 @@ impl std::fmt::Debug for BufferDiff { pub enum BufferDiffEvent { DiffChanged { changed_range: Option>, + base_text_changed_range: Option>, }, LanguageChanged, HunksStagedOrUnstaged(Option), @@ -1075,21 +1093,40 @@ impl EventEmitter for BufferDiff {} impl BufferDiff { pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self { + let base_text = cx.new(|cx| { + let mut buffer = language::Buffer::local("", cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer + }); + BufferDiff { buffer_id: buffer.remote_id(), - inner: BufferDiffSnapshot::empty(buffer, cx).inner, + inner: BufferDiffInner { + base_text, + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text_exists: false, + }, secondary_diff: None, } } - pub fn new_unchanged( - buffer: &text::BufferSnapshot, - base_text: language::BufferSnapshot, - ) -> Self { - debug_assert_eq!(buffer.text(), base_text.text()); + pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context) -> Self { + let base_text = buffer.text(); + let base_text = cx.new(|cx| { + let mut buffer = language::Buffer::local(base_text, cx); + buffer.set_capability(Capability::ReadOnly, cx); + buffer + }); + BufferDiff { buffer_id: buffer.remote_id(), - inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner, + inner: BufferDiffInner { + base_text, + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text_exists: true, + }, secondary_diff: None, } } @@ -1097,24 +1134,22 @@ impl BufferDiff { #[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: &str, - buffer: &Entity, - cx: &mut App, + buffer: &text::BufferSnapshot, + cx: &mut Context, ) -> Self { + let mut this = BufferDiff::new(&buffer, cx); + let executor = cx.background_executor().clone(); let mut base_text = base_text.to_owned(); text::LineEnding::normalize(&mut base_text); - let snapshot = BufferDiffSnapshot::new_with_base_text( - buffer.read(cx).text_snapshot(), - Some(base_text.into()), - None, + let inner = executor.block(this.update_diff( + buffer.clone(), + Some(Arc::from(base_text)), + true, None, cx, - ); - let snapshot = cx.background_executor().block(snapshot); - Self { - buffer_id: buffer.read(cx).remote_id(), - inner: snapshot.inner, - secondary_diff: None, - } + )); + this.set_snapshot(inner, &buffer, cx); + this } pub fn set_secondary_diff(&mut self, diff: Entity) { @@ -1133,6 +1168,7 @@ impl BufferDiff { }); cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)), + base_text_changed_range: Some(0..self.base_text(cx).len()), }); } } @@ -1145,21 +1181,30 @@ impl BufferDiff { file_exists: bool, cx: &mut Context, ) -> Option { - let new_index_text = self.inner.stage_or_unstage_hunks_impl( - &self.secondary_diff.as_ref()?.read(cx).inner, - stage, - hunks, - buffer, - file_exists, - ); + let new_index_text = self + .secondary_diff + .as_ref()? + .update(cx, |secondary_diff, cx| { + self.inner.stage_or_unstage_hunks_impl( + &secondary_diff.inner, + stage, + hunks, + buffer, + file_exists, + cx, + ) + }); cx.emit(BufferDiffEvent::HunksStagedOrUnstaged( new_index_text.clone(), )); if let Some((first, last)) = hunks.first().zip(hunks.last()) { let changed_range = first.buffer_range.start..last.buffer_range.end; + let base_text_changed_range = + first.diff_base_byte_range.start..last.diff_base_byte_range.end; cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: Some(base_text_changed_range), }); } new_index_text @@ -1173,95 +1218,102 @@ impl BufferDiff { cx: &mut Context, ) { let hunks = self - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) .collect::>(); - let Some(secondary) = self.secondary_diff.as_ref() else { + let Some(secondary) = self.secondary_diff.clone() else { return; }; - self.inner.stage_or_unstage_hunks_impl( - &secondary.read(cx).inner, - stage, - &hunks, - buffer, - file_exists, - ); + let secondary = secondary.read(cx).inner.clone(); + self.inner + .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx); if let Some((first, last)) = hunks.first().zip(hunks.last()) { let changed_range = first.buffer_range.start..last.buffer_range.end; + let base_text_changed_range = + first.diff_base_byte_range.start..last.diff_base_byte_range.end; cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: Some(base_text_changed_range), }); } } - pub fn range_to_hunk_range( + pub fn update_diff( &self, - range: Range, - buffer: &text::BufferSnapshot, - cx: &App, - ) -> Option> { - let start = self - .hunks_intersecting_range(range.clone(), buffer, cx) - .next()? - .buffer_range - .start; - let end = self - .hunks_intersecting_range_rev(range, buffer) - .next()? - .buffer_range - .end; - Some(start..end) - } - - pub async fn update_diff( - this: Entity, buffer: text::BufferSnapshot, - base_text: Option>, + base_text: Option>, base_text_changed: bool, - language_changed: bool, language: Option>, - language_registry: Option>, - cx: &mut AsyncApp, - ) -> anyhow::Result { - Ok(if base_text_changed || language_changed { - cx.update(|cx| { - BufferDiffSnapshot::new_with_base_text( - buffer.clone(), - base_text, - language.clone(), - language_registry.clone(), - cx, - ) - })? - .await - } else { - this.read_with(cx, |this, cx| { - BufferDiffSnapshot::new_with_base_buffer( + cx: &App, + ) -> Task { + let prev_base_text = self.base_text(cx).as_rope().clone(); + let diff_options = build_diff_options( + None, + language.as_ref().map(|l| l.name()), + language.as_ref().map(|l| l.default_scope()), + cx, + ); + + cx.background_executor() + .spawn_labeled(*CALCULATE_DIFF_TASK, async move { + let base_text_rope = if let Some(base_text) = &base_text { + if base_text_changed { + Rope::from(base_text.as_ref()) + } else { + prev_base_text + } + } else { + Rope::new() + }; + let base_text_exists = base_text.is_some(); + let hunks = compute_hunks( + base_text + .clone() + .map(|base_text| (base_text, base_text_rope.clone())), buffer.clone(), + diff_options, + ); + let base_text = base_text.unwrap_or_default(); + let inner = BufferDiffInner { base_text, - this.base_text().clone(), - cx, - ) - })? - .await - }) + hunks, + base_text_exists, + pending_hunks: SumTree::new(&buffer), + }; + BufferDiffUpdate { + inner, + base_text_changed, + } + }) } - pub fn language_changed(&mut self, cx: &mut Context) { + pub fn language_changed( + &mut self, + language: Option>, + language_registry: Option>, + cx: &mut Context, + ) { + self.inner.base_text.update(cx, |base_text, cx| { + base_text.set_language(language, cx); + if let Some(language_registry) = language_registry { + base_text.set_language_registry(language_registry); + } + }); cx.emit(BufferDiffEvent::LanguageChanged); } pub fn set_snapshot( &mut self, - new_snapshot: BufferDiffSnapshot, + new_state: BufferDiffUpdate, buffer: &text::BufferSnapshot, cx: &mut Context, ) -> Option> { - self.set_snapshot_with_secondary(new_snapshot, buffer, None, false, cx) + self.set_snapshot_with_secondary(new_state, buffer, None, false, cx) } pub fn set_snapshot_with_secondary( &mut self, - new_snapshot: BufferDiffSnapshot, + update: BufferDiffUpdate, buffer: &text::BufferSnapshot, secondary_diff_change: Option>, clear_pending_hunks: bool, @@ -1269,27 +1321,24 @@ impl BufferDiff { ) -> Option> { log::debug!("set snapshot with secondary {secondary_diff_change:?}"); + let old_snapshot = self.snapshot(cx); let state = &mut self.inner; - let new_state = new_snapshot.inner; - let (base_text_changed, mut changed_range) = + let new_state = update.inner; + let (mut changed_range, mut base_text_changed_range) = match (state.base_text_exists, new_state.base_text_exists) { - (false, false) => (true, None), - (true, true) - if state.base_text.remote_id() == new_state.base_text.remote_id() - && state.base_text.syntax_update_count() - == new_state.base_text.syntax_update_count() => - { - (false, new_state.compare(state, buffer)) + (false, false) => (None, None), + (true, true) if !update.base_text_changed => { + compare_hunks(&new_state.hunks, &old_snapshot.inner.hunks, buffer) } _ => ( - true, Some(text::Anchor::min_max_range_for_buffer(self.buffer_id)), + Some(0..new_state.base_text.len()), ), }; if let Some(secondary_changed_range) = secondary_diff_change - && let Some(secondary_hunk_range) = - self.range_to_hunk_range(secondary_changed_range, buffer, cx) + && let (Some(secondary_hunk_range), Some(secondary_base_range)) = + old_snapshot.range_to_hunk_range(secondary_changed_range, buffer) { if let Some(range) = &mut changed_range { range.start = *secondary_hunk_range.start.min(&range.start, buffer); @@ -1297,13 +1346,26 @@ impl BufferDiff { } else { changed_range = Some(secondary_hunk_range); } + + if let Some(base_text_range) = &mut base_text_changed_range { + base_text_range.start = secondary_base_range.start.min(base_text_range.start); + base_text_range.end = secondary_base_range.end.max(base_text_range.end); + } else { + base_text_changed_range = Some(secondary_base_range); + } } let state = &mut self.inner; state.base_text_exists = new_state.base_text_exists; - state.base_text = new_state.base_text; + if update.base_text_changed { + state.base_text.update(cx, |base_text, cx| { + base_text.set_capability(Capability::ReadWrite, cx); + base_text.set_text(new_state.base_text.clone(), cx); + base_text.set_capability(Capability::ReadOnly, cx); + }) + } state.hunks = new_state.hunks; - if base_text_changed || clear_pending_hunks { + if update.base_text_changed || clear_pending_hunks { if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) { if let Some(range) = &mut changed_range { @@ -1312,18 +1374,28 @@ impl BufferDiff { } else { changed_range = Some(first.buffer_range.start..last.buffer_range.end); } + + if let Some(base_text_range) = &mut base_text_changed_range { + base_text_range.start = + base_text_range.start.min(first.diff_base_byte_range.start); + base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end); + } else { + base_text_changed_range = + Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end); + } } state.pending_hunks = SumTree::new(buffer); } cx.emit(BufferDiffEvent::DiffChanged { changed_range: changed_range.clone(), + base_text_changed_range, }); changed_range } - pub fn base_text(&self) -> &language::BufferSnapshot { - &self.inner.base_text + pub fn base_text(&self, cx: &App) -> language::BufferSnapshot { + self.inner.base_text.read(cx).snapshot() } pub fn base_text_exists(&self) -> bool { @@ -1332,7 +1404,12 @@ impl BufferDiff { pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot { BufferDiffSnapshot { - inner: self.inner.clone(), + inner: BufferDiffInner { + hunks: self.inner.hunks.clone(), + pending_hunks: self.inner.pending_hunks.clone(), + base_text: self.inner.base_text.read(cx).snapshot(), + base_text_exists: self.inner.base_text_exists, + }, secondary_diff: self .secondary_diff .as_ref() @@ -1340,81 +1417,30 @@ impl BufferDiff { } } - pub fn hunks<'a>( - &'a self, - buffer_snapshot: &'a text::BufferSnapshot, - cx: &'a App, - ) -> impl 'a + Iterator { - self.hunks_intersecting_range( - Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()), - buffer_snapshot, - cx, - ) - } - - pub fn hunks_intersecting_range<'a>( - &'a self, - range: Range, - buffer_snapshot: &'a text::BufferSnapshot, - cx: &'a App, - ) -> impl 'a + Iterator { - let unstaged_counterpart = self - .secondary_diff - .as_ref() - .map(|diff| &diff.read(cx).inner); - self.inner - .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart) - } - - pub fn hunks_intersecting_range_rev<'a>( - &'a self, - range: Range, - buffer_snapshot: &'a text::BufferSnapshot, - ) -> impl 'a + Iterator { - self.inner - .hunks_intersecting_range_rev(range, buffer_snapshot) - } - - pub fn hunks_in_row_range<'a>( - &'a self, - range: Range, - buffer: &'a text::BufferSnapshot, - cx: &'a App, - ) -> impl 'a + Iterator { - let start = buffer.anchor_before(Point::new(range.start, 0)); - let end = buffer.anchor_after(Point::new(range.end, 0)); - self.hunks_intersecting_range(start..end, buffer, cx) - } - /// Used in cases where the change set isn't derived from git. pub fn set_base_text( &mut self, - base_text: Option>, + base_text: Option>, language: Option>, - language_registry: Option>, buffer: text::BufferSnapshot, cx: &mut Context, ) -> oneshot::Receiver<()> { let (tx, rx) = oneshot::channel(); - let this = cx.weak_entity(); - - let snapshot = BufferDiffSnapshot::new_with_base_text( - buffer.clone(), - base_text, - language, - language_registry, - cx, - ); let complete_on_drop = util::defer(|| { tx.send(()).ok(); }); - cx.spawn(async move |_, cx| { - let snapshot = snapshot.await; - let Some(this) = this.upgrade() else { + cx.spawn(async move |this, cx| { + let Some(state) = this + .update(cx, |this, cx| { + this.update_diff(buffer.clone(), base_text, true, language, cx) + }) + .log_err() + else { return; }; + let state = state.await; this.update(cx, |this, cx| { - this.set_snapshot(snapshot, &buffer, cx); + this.set_snapshot(state, &buffer, cx); }) .log_err(); drop(complete_on_drop) @@ -1423,24 +1449,24 @@ impl BufferDiff { rx } - pub fn base_text_string(&self) -> Option { + pub fn base_text_string(&self, cx: &App) -> Option { self.inner .base_text_exists - .then(|| self.inner.base_text.text()) + .then(|| self.inner.base_text.read(cx).text()) } #[cfg(any(test, feature = "test-support"))] - pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context) { - let base_text = self.base_text_string().map(Arc::new); - let snapshot = BufferDiffSnapshot::new_with_base_buffer( - buffer.clone(), - base_text, - self.inner.base_text.clone(), - cx, - ); - let snapshot = cx.background_executor().block(snapshot); + pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context) { + let language = self.base_text(cx).language().cloned(); + let base_text = self.base_text_string(cx).map(|s| s.as_str().into()); + let fut = self.update_diff(buffer.clone(), base_text, false, language, cx); + let snapshot = cx.background_executor().block(fut); self.set_snapshot(snapshot, &buffer, cx); } + + pub fn base_text_buffer(&self) -> Entity { + self.inner.base_text.clone() + } } impl DiffHunk { @@ -1580,7 +1606,7 @@ pub fn assert_hunks( #[cfg(test)] mod tests { - use std::fmt::Write as _; + use std::{fmt::Write as _, sync::mpsc}; use super::*; use gpui::TestAppContext; @@ -1638,7 +1664,7 @@ mod tests { ], ); - diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx)); + diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx)); assert_hunks::<&str, _>( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), @@ -1729,8 +1755,7 @@ mod tests { #[gpui::test] async fn test_buffer_diff_range(cx: &mut TestAppContext) { - let diff_base = Arc::new( - " + let diff_base = " one two three @@ -1742,8 +1767,7 @@ mod tests { nine ten " - .unindent(), - ); + .unindent(); let buffer_text = " A @@ -1767,17 +1791,7 @@ mod tests { .unindent(); let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); - let diff = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_text( - buffer.snapshot(), - Some(diff_base.clone()), - None, - None, - cx, - ) - }) - .await; + let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx); assert_eq!( diff.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer.remote_id()), @@ -2036,27 +2050,19 @@ mod tests { let hunk_range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end); - let unstaged = - BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx); - let uncommitted = - BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx); - - let unstaged_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(unstaged, &buffer, cx); - diff - }); + let unstaged_diff = + cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx)); let uncommitted_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(uncommitted, &buffer, cx); + let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff); diff }); uncommitted_diff.update(cx, |diff, cx| { let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) + .snapshot(cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer) .collect::>(); for hunk in &hunks { assert_ne!( @@ -2071,7 +2077,8 @@ mod tests { .to_string(); let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) + .snapshot(cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer) .collect::>(); for hunk in &hunks { assert_eq!( @@ -2110,22 +2117,15 @@ mod tests { BufferId::new(1).unwrap(), buffer_text.clone(), ); - let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); - let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); - let unstaged_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(unstaged, &buffer, cx); - diff - }); + let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx)); let uncommitted_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(uncommitted, &buffer, cx); + let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx); diff.set_secondary_diff(unstaged_diff.clone()); diff }); uncommitted_diff.update(cx, |diff, cx| { - let hunk = diff.hunks(&buffer, cx).next().unwrap(); + let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap(); let new_index_text = diff .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx) @@ -2133,7 +2133,7 @@ mod tests { .to_string(); assert_eq!(new_index_text, buffer_text); - let hunk = diff.hunks(&buffer, cx).next().unwrap(); + let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap(); assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkRemovalPending @@ -2145,7 +2145,7 @@ mod tests { .to_string(); assert_eq!(index_text, head_text); - let hunk = diff.hunks(&buffer, cx).next().unwrap(); + let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap(); // optimistically unstaged (fine, could also be HasSecondaryHunk) assert_eq!( hunk.secondary_status, @@ -2184,10 +2184,17 @@ mod tests { let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1); - let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx)); + let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx)); let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx); - let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap(); + let (range, base_text_range) = + compare_hunks(&diff_1.inner.hunks, &empty_diff.inner.hunks, &buffer); + let range = range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); + let base_text_range = base_text_range.unwrap(); + assert_eq!( + base_text_range.to_point(diff_1.base_text()), + Point::new(0, 0)..Point::new(10, 0) + ); // Edit does affects the diff because it recalculates word diffs. buffer.edit_via_marked_text( @@ -2204,13 +2211,15 @@ mod tests { .unindent(), ); let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx); + let (range, base_text_range) = + compare_hunks(&diff_2.inner.hunks, &diff_1.inner.hunks, &buffer); assert_eq!( + range.unwrap().to_point(&buffer), Point::new(4, 0)..Point::new(5, 0), - diff_2 - .inner - .compare(&diff_1.inner, &buffer) - .unwrap() - .to_point(&buffer) + ); + assert_eq!( + base_text_range.unwrap().to_point(diff_2.base_text()), + Point::new(6, 0)..Point::new(7, 0), ); // Edit turns a deletion hunk into a modification. @@ -2228,8 +2237,15 @@ mod tests { .unindent(), ); let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx); - let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap(); + let (range, base_text_range) = + compare_hunks(&diff_3.inner.hunks, &diff_2.inner.hunks, &buffer); + let range = range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); + let base_text_range = base_text_range.unwrap(); + assert_eq!( + base_text_range.to_point(diff_3.base_text()), + Point::new(2, 0)..Point::new(4, 0) + ); // Edit turns a modification hunk into a deletion. buffer.edit_via_marked_text( @@ -2245,8 +2261,15 @@ mod tests { .unindent(), ); let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx); - let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap(); + let (range, base_text_range) = + compare_hunks(&diff_4.inner.hunks, &diff_3.inner.hunks, &buffer); + let range = range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); + let base_text_range = base_text_range.unwrap(); + assert_eq!( + base_text_range.to_point(diff_4.base_text()), + Point::new(6, 0)..Point::new(7, 0) + ); // Edit introduces a new insertion hunk. buffer.edit_via_marked_text( @@ -2263,8 +2286,15 @@ mod tests { .unindent(), ); let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx); - let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap(); + let (range, base_text_range) = + compare_hunks(&diff_5.inner.hunks, &diff_4.inner.hunks, &buffer); + let range = range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); + let base_text_range = base_text_range.unwrap(); + assert_eq!( + base_text_range.to_point(diff_5.base_text()), + Point::new(5, 0)..Point::new(5, 0) + ); // Edit removes a hunk. buffer.edit_via_marked_text( @@ -2281,8 +2311,15 @@ mod tests { .unindent(), ); let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx); - let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap(); + let (range, base_text_range) = + compare_hunks(&diff_6.inner.hunks, &diff_5.inner.hunks, &buffer); + let range = range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0)); + let base_text_range = base_text_range.unwrap(); + assert_eq!( + base_text_range.to_point(diff_6.base_text()), + Point::new(9, 0)..Point::new(10, 0) + ); } #[gpui::test(iterations = 100)] @@ -2356,23 +2393,13 @@ mod tests { head_text: String, cx: &mut TestAppContext, ) -> Entity { - let inner = - BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner; - let secondary = BufferDiff { - buffer_id: working_copy.remote_id(), - inner: BufferDiffSnapshot::new_sync( - working_copy.text.clone(), - index_text.to_string(), - cx, - ) - .inner, - secondary_diff: None, - }; - let secondary = cx.new(|_| secondary); - cx.new(|_| BufferDiff { - buffer_id: working_copy.remote_id(), - inner, - secondary_diff: Some(secondary), + let secondary = cx.new(|cx| { + BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx) + }); + cx.new(|cx| { + let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx); + diff.secondary_diff = Some(secondary); + diff }) } @@ -2402,12 +2429,12 @@ mod tests { let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); let mut hunks = diff.update(cx, |diff, cx| { - diff.hunks_intersecting_range( - Anchor::min_max_range_for_buffer(diff.buffer_id), - &working_copy, - cx, - ) - .collect::>() + diff.snapshot(cx) + .hunks_intersecting_range( + Anchor::min_max_range_for_buffer(diff.buffer_id), + &working_copy, + ) + .collect::>() }); if hunks.is_empty() { return; @@ -2436,12 +2463,12 @@ mod tests { diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx); let found_hunks = diff.update(cx, |diff, cx| { - diff.hunks_intersecting_range( - Anchor::min_max_range_for_buffer(diff.buffer_id), - &working_copy, - cx, - ) - .collect::>() + diff.snapshot(cx) + .hunks_intersecting_range( + Anchor::min_max_range_for_buffer(diff.buffer_id), + &working_copy, + ) + .collect::>() }); assert_eq!(hunks.len(), found_hunks.len()); @@ -2500,21 +2527,104 @@ mod tests { let buffer_snapshot = buffer.snapshot(); let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx); let expected_results = [ - // don't format me - (0, 0), - (1, 2), - (2, 2), - (3, 5), - (4, 5), - (5, 7), - (6, 9), + // main buffer row, base text row (right bias), base text row (left bias) + (0, 0, 0), + (1, 2, 1), + (2, 2, 2), + (3, 5, 3), + (4, 5, 5), + (5, 7, 7), + (6, 9, 9), ]; - for (buffer_row, expected) in expected_results { + for (buffer_row, expected_right, expected_left) in expected_results { assert_eq!( - diff.row_to_base_text_row(buffer_row, &buffer_snapshot), - expected, + diff.row_to_base_text_row(buffer_row, Bias::Right, &buffer_snapshot), + expected_right, "{buffer_row}" ); + assert_eq!( + diff.row_to_base_text_row(buffer_row, Bias::Left, &buffer_snapshot), + expected_left, + "{buffer_row}" + ); + } + } + + #[gpui::test] + async fn test_changed_ranges(cx: &mut gpui::TestAppContext) { + let base_text = " + one + two + three + four + five + six + " + .unindent(); + let buffer_text = " + one + TWO + three + four + FIVE + six + " + .unindent(); + let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx) + }); + let (tx, rx) = mpsc::channel(); + let subscription = + cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap())); + + let snapshot = buffer.update(cx, |buffer, cx| { + buffer.set_text( + " + ONE + TWO + THREE + FOUR + FIVE + SIX + " + .unindent(), + cx, + ); + buffer.text_snapshot() + }); + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( + snapshot.clone(), + Some(base_text.as_str().into()), + false, + None, + cx, + ) + }) + .await; + diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx)); + cx.run_until_parked(); + drop(subscription); + let events = rx.into_iter().collect::>(); + match events.as_slice() { + [ + BufferDiffEvent::DiffChanged { + changed_range: _, + base_text_changed_range, + }, + ] => { + // TODO(cole) this seems like it should pass but currently fails (see compare_hunks) + // assert_eq!( + // *changed_range, + // Some(Anchor::min_max_range_for_buffer( + // buffer.read_with(cx, |buffer, _| buffer.remote_id()) + // )) + // ); + assert_eq!(*base_text_changed_range, Some(0..base_text.len())); + } + _ => panic!("unexpected events: {:?}", events), } } } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index bd37c416af5c60d4634b6bc12446921a9f22a125..65b17f834565248ebd3975764a6c3d156fe15c89 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2647,13 +2647,13 @@ async fn test_git_diff_base_change( local_unstaged_diff_a.read_with(cx_a, |diff, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[(1..2, "", "two\n", DiffHunkStatus::added_none())], ); }); @@ -2677,13 +2677,13 @@ async fn test_git_diff_base_change( remote_unstaged_diff_a.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[(1..2, "", "two\n", DiffHunkStatus::added_none())], ); }); @@ -2699,13 +2699,13 @@ async fn test_git_diff_base_change( remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(committed_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 1..2, "TWO\n", @@ -2731,13 +2731,13 @@ async fn test_git_diff_base_change( local_unstaged_diff_a.read_with(cx_a, |diff, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(new_staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[(2..3, "", "three\n", DiffHunkStatus::added_none())], ); }); @@ -2746,13 +2746,13 @@ async fn test_git_diff_base_change( remote_unstaged_diff_a.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(new_staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[(2..3, "", "three\n", DiffHunkStatus::added_none())], ); }); @@ -2760,13 +2760,13 @@ async fn test_git_diff_base_change( remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_a.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(new_committed_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 1..2, "TWO_HUNDRED\n", @@ -2813,13 +2813,13 @@ async fn test_git_diff_base_change( local_unstaged_diff_b.read_with(cx_a, |diff, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[(1..2, "", "two\n", DiffHunkStatus::added_none())], ); }); @@ -2842,11 +2842,11 @@ async fn test_git_diff_base_change( remote_unstaged_diff_b.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_b.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, &staged_text, &[(1..2, "", "two\n", DiffHunkStatus::added_none())], @@ -2864,11 +2864,11 @@ async fn test_git_diff_base_change( local_unstaged_diff_b.read_with(cx_a, |diff, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(new_staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, &new_staged_text, &[(2..3, "", "three\n", DiffHunkStatus::added_none())], @@ -2878,11 +2878,11 @@ async fn test_git_diff_base_change( remote_unstaged_diff_b.read_with(cx_b, |diff, cx| { let buffer = remote_buffer_b.read(cx); assert_eq!( - diff.base_text_string().as_deref(), + diff.base_text_string(cx).as_deref(), Some(new_staged_text.as_str()) ); assert_hunks( - diff.hunks_in_row_range(0..4, buffer, cx), + diff.snapshot(cx).hunks_in_row_range(0..4, buffer), buffer, &new_staged_text, &[(2..3, "", "three\n", DiffHunkStatus::added_none())], diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 4c7a1661ecb6a845cd16d3c98ce81572a1855393..acdffacd0dec1c31a4729737feb4f241aaae3c51 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1377,7 +1377,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_diff(host_buffer.read(cx).remote_id(), cx) .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) }); let guest_diff_base = guest_project.read_with(client_cx, |project, cx| { project @@ -1386,7 +1386,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx) .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) }); assert_eq!( guest_diff_base, host_diff_base, diff --git a/crates/edit_prediction_ui/src/rate_prediction_modal.rs b/crates/edit_prediction_ui/src/rate_prediction_modal.rs index 1af65ad58083e3cccfa51ea7b674da01cad810a0..e36f9774185b53d2f3c2ae00005c85895fb5309a 100644 --- a/crates/edit_prediction_ui/src/rate_prediction_modal.rs +++ b/crates/edit_prediction_ui/src/rate_prediction_modal.rs @@ -1,4 +1,4 @@ -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use buffer_diff::BufferDiff; use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore}; use editor::{Editor, ExcerptRange, MultiBuffer}; use feature_flags::FeatureFlag; @@ -323,22 +323,23 @@ impl RatePredictionsModal { let start = Point::new(range.start.row.saturating_sub(5), 0); let end = Point::new(range.end.row + 5, 0).min(new_buffer_snapshot.max_point()); - let diff = cx.new::(|cx| { - let diff_snapshot = BufferDiffSnapshot::new_with_base_buffer( + let language = new_buffer_snapshot.language().cloned(); + let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx)); + diff.update(cx, |diff, cx| { + let update = diff.update_diff( new_buffer_snapshot.text.clone(), Some(old_buffer_snapshot.text().into()), - old_buffer_snapshot.clone(), + true, + language, cx, ); - let diff = BufferDiff::new(&new_buffer_snapshot, cx); cx.spawn(async move |diff, cx| { - let diff_snapshot = diff_snapshot.await; + let update = update.await; diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &new_buffer_snapshot.text, cx); + diff.set_snapshot(update, &new_buffer_snapshot.text, cx); }) }) .detach(); - diff }); editor.disable_header_for_buffer(new_buffer_id, cx); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 413766cb283dfa2c5de0351b3ff10ff9b90a9c56..e7d0ef9f0faa88e888328d2028dea2b2b6457421 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -232,8 +232,6 @@ impl DisplayMap { .update(cx, |map, cx| map.sync(tab_snapshot, edits, cx)); let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot; - // todo word diff here? - DisplaySnapshot { block_snapshot, diagnostics_max_severity: self.diagnostics_max_severity, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 879ca11be1a84ffd44daa6e53677b06887172026..43309e435746e8cff1443fbb505d9f168d5c0f7e 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1119,7 +1119,6 @@ impl Iterator for WrapRows<'_> { RowInfo { buffer_id: None, buffer_row: None, - base_text_row: None, multibuffer_row: None, diff_status, expand_info: None, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 44693466d7ad3303c36ddb032d7fbba862df547a..72ff50050fee7cc1e6c60c8e461c213b4487f298 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1072,6 +1072,7 @@ pub struct Editor { minimap_visibility: MinimapVisibility, offset_content: bool, disable_expand_excerpt_buttons: bool, + delegate_expand_excerpts: bool, show_line_numbers: Option, use_relative_line_numbers: Option, show_git_diff_gutter: Option, @@ -1203,6 +1204,7 @@ pub struct Editor { hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, inline_value_cache: InlineValueCache, + number_deleted_lines: bool, selection_drag_state: SelectionDragState, colors: Option, @@ -1215,7 +1217,6 @@ pub struct Editor { applicable_language_settings: HashMap, LanguageSettings>, accent_data: Option, fetched_tree_sitter_chunks: HashMap>>, - use_base_text_line_numbers: bool, } #[derive(Debug, PartialEq)] @@ -1256,6 +1257,7 @@ pub struct EditorSnapshot { show_gutter: bool, offset_content: bool, show_line_numbers: Option, + number_deleted_lines: bool, show_git_diff_gutter: Option, show_code_actions: Option, show_runnables: Option, @@ -2237,6 +2239,7 @@ impl Editor { show_line_numbers: (!full_mode).then_some(false), use_relative_line_numbers: None, disable_expand_excerpt_buttons: !full_mode, + delegate_expand_excerpts: false, show_git_diff_gutter: None, show_code_actions: None, show_runnables: None, @@ -2405,7 +2408,7 @@ impl Editor { applicable_language_settings: HashMap::default(), accent_data: None, fetched_tree_sitter_chunks: HashMap::default(), - use_base_text_line_numbers: false, + number_deleted_lines: false, }; if is_minimap { @@ -2940,6 +2943,7 @@ impl Editor { show_gutter: self.show_gutter, offset_content: self.offset_content, show_line_numbers: self.show_line_numbers, + number_deleted_lines: self.number_deleted_lines, show_git_diff_gutter: self.show_git_diff_gutter, show_code_actions: self.show_code_actions, show_runnables: self.show_runnables, @@ -11496,7 +11500,7 @@ impl Editor { let buffer = buffer.read(cx); let original_text = diff .read(cx) - .base_text() + .base_text(cx) .as_rope() .slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0); let buffer_snapshot = buffer.snapshot(); @@ -16590,7 +16594,6 @@ impl Editor { &mut self, lines: u32, direction: ExpandExcerptDirection, - cx: &mut Context, ) { let selections = self.selections.disjoint_anchors_arc(); @@ -16601,14 +16604,24 @@ impl Editor { lines }; + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut excerpt_ids = selections + .iter() + .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) + .collect::>(); + excerpt_ids.sort(); + excerpt_ids.dedup(); + + if self.delegate_expand_excerpts { + cx.emit(EditorEvent::ExpandExcerptsRequested { + excerpt_ids, + lines, + direction, + }); + return; + } + self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let mut excerpt_ids = selections - .iter() - .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) - .collect::>(); - excerpt_ids.sort(); - excerpt_ids.dedup(); buffer.expand_excerpts(excerpt_ids, lines, direction, cx) }) } @@ -16620,8 +16633,18 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let current_scroll_position = self.scroll_position(cx); let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines; + + if self.delegate_expand_excerpts { + cx.emit(EditorEvent::ExpandExcerptsRequested { + excerpt_ids: vec![excerpt], + lines: lines_to_expand, + direction, + }); + return; + } + + let current_scroll_position = self.scroll_position(cx); let mut scroll = None; if direction == ExpandExcerptDirection::Down { @@ -19698,10 +19721,6 @@ impl Editor { self.display_map.read(cx).fold_placeholder.clone() } - pub fn set_use_base_text_line_numbers(&mut self, show: bool, _cx: &mut Context) { - self.use_base_text_line_numbers = show; - } - pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { self.buffer.update(cx, |buffer, cx| { buffer.set_all_diff_hunks_expanded(cx); @@ -19943,7 +19962,7 @@ impl Editor { buffer_word_diffs: Vec::default(), diff_base_byte_range: hunk.diff_base_byte_range.start.0 ..hunk.diff_base_byte_range.end.0, - secondary_status: hunk.secondary_status, + secondary_status: hunk.status.secondary, range: Point::zero()..Point::zero(), // unused }) .collect::>(), @@ -20572,6 +20591,10 @@ impl Editor { cx.notify(); } + pub fn set_delegate_expand_excerpts(&mut self, delegate: bool) { + self.delegate_expand_excerpts = delegate; + } + pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { self.show_git_diff_gutter = Some(show_git_diff_gutter); cx.notify(); @@ -20983,8 +21006,12 @@ impl Editor { Some(( multi_buffer.buffer(buffer.remote_id()).unwrap(), - buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, buffer) - ..buffer_diff_snapshot.row_to_base_text_row(end_row_in_buffer, buffer), + buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, Bias::Left, buffer) + ..buffer_diff_snapshot.row_to_base_text_row( + end_row_in_buffer, + Bias::Left, + buffer, + ), )) }); @@ -25409,6 +25436,11 @@ pub enum EditorEvent { ExcerptsExpanded { ids: Vec, }, + ExpandExcerptsRequested { + excerpt_ids: Vec, + lines: u32, + direction: ExpandExcerptDirection, + }, BufferEdited, Edited { transaction_id: clock::Lamport, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 26aa82dcba30036cbfcecc659cd929b61c0fa068..cb1f2ca3334d3d2bf01fee76fbf2dfbe7a57c4f4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -36,8 +36,7 @@ use languages::markdown_lang; use languages::rust_lang; use lsp::CompletionParams; use multi_buffer::{ - ExcerptRange, IndentGuide, MultiBuffer, MultiBufferFilterMode, MultiBufferOffset, - MultiBufferOffsetUtf16, PathKey, + ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey, }; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; @@ -13221,30 +13220,28 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { // Handle formatting requests to the language server. cx.lsp .set_request_handler::({ - let buffer_changes = buffer_changes.clone(); move |_, _| { - let buffer_changes = buffer_changes.clone(); // Insert blank lines between each line of the buffer. async move { - // When formatting is requested, trailing whitespace has already been stripped, - // and the trailing newline has already been added. - assert_eq!( - &buffer_changes.lock()[1..], - &[ - ( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), - "\n".into() - ), - ] - ); + // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver + // DidChangedTextDocument to the LSP before sending the formatting request. + // assert_eq!( + // &buffer_changes.lock()[1..], + // &[ + // ( + // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), + // "".into() + // ), + // ( + // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), + // "".into() + // ), + // ( + // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), + // "\n".into() + // ), + // ] + // ); Ok(Some(vec![ lsp::TextEdit { @@ -13276,7 +13273,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ] .join("\n"), ); - cx.run_until_parked(); // Submit a format request. let format = cx @@ -19884,7 +19880,9 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx) + }); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -20509,7 +20507,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) { (buffer_2.clone(), file_2_old), (buffer_3.clone(), file_3_old), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx) + }); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -20615,7 +20615,9 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) { cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx)); editor .update(cx, |editor, _window, cx| { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx) + }); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)) @@ -22049,7 +22051,9 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) { editor.buffer().update(cx, |multibuffer, cx| { let buffer = multibuffer.as_singleton().unwrap(); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx) + }); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer.add_diff(diff, cx); @@ -29223,208 +29227,6 @@ async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) { "}); } -#[gpui::test] -async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut leader_cx = EditorTestContext::new(cx).await; - - let diff_base = indoc!( - r#" - one - two - three - four - five - six - "# - ); - - let initial_state = indoc!( - r#" - ˇone - two - THREE - four - five - six - "# - ); - - leader_cx.set_state(initial_state); - - leader_cx.set_head_text(&diff_base); - leader_cx.run_until_parked(); - - let follower = leader_cx.update_multibuffer(|leader, cx| { - leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - leader.set_all_diff_hunks_expanded(cx); - leader.get_or_create_follower(cx) - }); - follower.update(cx, |follower, cx| { - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - follower.set_all_diff_hunks_expanded(cx); - }); - - let follower_editor = - leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx)); - // leader_cx.window.focus(&follower_editor.focus_handle(cx)); - - let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await; - cx.run_until_parked(); - - leader_cx.assert_editor_state(initial_state); - follower_cx.assert_editor_state(indoc! { - r#" - ˇone - two - three - four - five - six - "# - }); - - follower_cx.editor(|editor, _window, cx| { - assert!(editor.read_only(cx)); - }); - - leader_cx.update_editor(|editor, _window, cx| { - editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx); - }); - cx.run_until_parked(); - - leader_cx.assert_editor_state(indoc! { - r#" - ˇone - two - THREE - four - FIVE - six - "# - }); - - follower_cx.assert_editor_state(indoc! { - r#" - ˇone - two - three - four - five - six - "# - }); - - leader_cx.update_editor(|editor, _window, cx| { - editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx); - }); - cx.run_until_parked(); - - leader_cx.assert_editor_state(indoc! { - r#" - ˇone - two - THREE - four - FIVE - six - SEVEN"# - }); - - follower_cx.assert_editor_state(indoc! { - r#" - ˇone - two - three - four - five - six - "# - }); - - leader_cx.update_editor(|editor, window, cx| { - editor.move_down(&MoveDown, window, cx); - editor.refresh_selected_text_highlights(true, window, cx); - }); - leader_cx.run_until_parked(); -} - -#[gpui::test] -async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let base_text = "base\n"; - let buffer_text = "buffer\n"; - - let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx)); - let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx)); - - let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx)); - let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx)); - let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx)); - let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx)); - - let leader = cx.new(|cx| { - let mut leader = MultiBuffer::new(Capability::ReadWrite); - leader.set_all_diff_hunks_expanded(cx); - leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - leader - }); - let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx)); - follower.update(cx, |follower, _| { - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - }); - - leader.update(cx, |leader, cx| { - leader.insert_excerpts_after( - ExcerptId::min(), - extra_buffer_2.clone(), - vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - leader.add_diff(extra_diff_2.clone(), cx); - - leader.insert_excerpts_after( - ExcerptId::min(), - extra_buffer_1.clone(), - vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - leader.add_diff(extra_diff_1.clone(), cx); - - leader.insert_excerpts_after( - ExcerptId::min(), - buffer1.clone(), - vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - leader.add_diff(diff1.clone(), cx); - }); - - cx.run_until_parked(); - let mut cx = cx.add_empty_window(); - - let leader_editor = cx - .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx)); - let follower_editor = cx.new_window_entity(|window, cx| { - Editor::for_multibuffer(follower.clone(), None, window, cx) - }); - - let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await; - leader_cx.assert_editor_state(indoc! {" - ˇbuffer - - dummy text 1 - - dummy text 2 - "}); - let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await; - follower_cx.assert_editor_state(indoc! {" - ˇbase - - - "}); -} - #[gpui::test] async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e9d7c52b6aea4e2db5f7a0f8a192a7a524108244..4890c1ba01e3904f1201421926d2724699696e4f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3264,17 +3264,15 @@ impl EditorElement { line_number.clear(); let non_relative_number = if relative.wrapped() { row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1 - } else if self.editor.read(cx).use_base_text_line_numbers { - row_info.base_text_row?.0 + 1 } else { row_info.buffer_row? + 1 }; let relative_number = relative_rows.get(&display_row); if !(relative_line_numbers_enabled && relative_number.is_some()) + && !snapshot.number_deleted_lines && row_info .diff_status .is_some_and(|status| status.is_deleted()) - && !self.editor.read(cx).use_base_text_line_numbers { return None; } diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index b5090f06dc1e68d609413db31112775e56559689..f6cff5d92e16735df9cf9c6b2f382cd0b3999e79 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -1,9 +1,16 @@ +use std::ops::Range; + +use buffer_diff::BufferDiff; +use collections::HashMap; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{ Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity, }; -use multi_buffer::{MultiBuffer, MultiBufferFilterMode}; +use language::{Buffer, Capability}; +use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey}; use project::Project; +use rope::Point; +use text::{Bias, OffsetRangeExt as _}; use ui::{ App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, Styled as _, Window, div, @@ -33,6 +40,7 @@ struct SplitDiff; struct UnsplitDiff; pub struct SplittableEditor { + primary_multibuffer: Entity, primary_editor: Entity, secondary: Option, panes: PaneGroup, @@ -41,9 +49,12 @@ pub struct SplittableEditor { } struct SecondaryEditor { + multibuffer: Entity, editor: Entity, pane: Entity, has_latest_selection: bool, + primary_to_secondary: HashMap, + secondary_to_primary: HashMap, _subscriptions: Vec, } @@ -63,14 +74,22 @@ impl SplittableEditor { } pub fn new_unsplit( - buffer: Entity, + primary_multibuffer: Entity, project: Entity, workspace: Entity, window: &mut Window, cx: &mut Context, ) -> Self { - let primary_editor = - cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx)); + let primary_editor = cx.new(|cx| { + let mut editor = Editor::for_multibuffer( + primary_multibuffer.clone(), + Some(project.clone()), + window, + cx, + ); + editor.set_expand_all_diff_hunks(cx); + editor + }); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.downgrade(), @@ -88,17 +107,25 @@ impl SplittableEditor { }); let panes = PaneGroup::new(pane); // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary - let subscriptions = - vec![ - cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::SelectionsChanged { .. } = event - && let Some(secondary) = &mut this.secondary - { + let subscriptions = vec![cx.subscribe( + &primary_editor, + |this, _, event: &EditorEvent, cx| match event { + EditorEvent::ExpandExcerptsRequested { + excerpt_ids, + lines, + direction, + } => { + this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx); + } + EditorEvent::SelectionsChanged { .. } => { + if let Some(secondary) = &mut this.secondary { secondary.has_latest_selection = false; } - cx.emit(event.clone()) - }), - ]; + cx.emit(event.clone()); + } + _ => cx.emit(event.clone()), + }, + )]; window.defer(cx, { let workspace = workspace.downgrade(); @@ -115,6 +142,7 @@ impl SplittableEditor { }); Self { primary_editor, + primary_multibuffer, secondary: None, panes, workspace: workspace.downgrade(), @@ -133,24 +161,22 @@ impl SplittableEditor { return; }; let project = workspace.read(cx).project().clone(); - let follower = self.primary_editor.update(cx, |primary, cx| { - primary.buffer().update(cx, |buffer, cx| { - let follower = buffer.get_or_create_follower(cx); - buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - follower - }) - }); - follower.update(cx, |follower, _| { - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + + let secondary_multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadOnly); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer }); - let secondary_editor = workspace.update(cx, |workspace, cx| { - cx.new(|cx| { - let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx); - // TODO(split-diff) this should be at the multibuffer level - editor.set_use_base_text_line_numbers(true, cx); - editor.added_to_workspace(workspace, window, cx); - editor - }) + let secondary_editor = cx.new(|cx| { + let mut editor = Editor::for_multibuffer( + secondary_multibuffer.clone(), + Some(project.clone()), + window, + cx, + ); + editor.number_deleted_lines = true; + editor.set_delegate_expand_excerpts(true); + editor }); let secondary_pane = cx.new(|cx| { let mut pane = Pane::new( @@ -175,23 +201,59 @@ impl SplittableEditor { pane }); - let subscriptions = - vec![ - cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::SelectionsChanged { .. } = event - && let Some(secondary) = &mut this.secondary - { + let subscriptions = vec![cx.subscribe( + &secondary_editor, + |this, _, event: &EditorEvent, cx| match event { + EditorEvent::ExpandExcerptsRequested { + excerpt_ids, + lines, + direction, + } => { + if let Some(secondary) = &this.secondary { + let primary_ids: Vec<_> = excerpt_ids + .iter() + .filter_map(|id| secondary.secondary_to_primary.get(id).copied()) + .collect(); + this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx); + } + } + EditorEvent::SelectionsChanged { .. } => { + if let Some(secondary) = &mut this.secondary { secondary.has_latest_selection = true; } - cx.emit(event.clone()) - }), - ]; - self.secondary = Some(SecondaryEditor { + cx.emit(event.clone()); + } + _ => cx.emit(event.clone()), + }, + )]; + let mut secondary = SecondaryEditor { editor: secondary_editor, + multibuffer: secondary_multibuffer, pane: secondary_pane.clone(), has_latest_selection: false, + primary_to_secondary: HashMap::default(), + secondary_to_primary: HashMap::default(), _subscriptions: subscriptions, + }; + self.primary_editor.update(cx, |editor, cx| { + editor.set_delegate_expand_excerpts(true); + editor.buffer().update(cx, |primary_multibuffer, cx| { + primary_multibuffer.set_show_deleted_hunks(false, cx); + let paths = primary_multibuffer.paths().cloned().collect::>(); + for path in paths { + let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next() + else { + continue; + }; + let snapshot = primary_multibuffer.snapshot(cx); + let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap(); + let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap(); + secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx); + } + }) }); + self.secondary = Some(secondary); + let primary_pane = self.panes.first_pane(); self.panes .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx) @@ -205,8 +267,9 @@ impl SplittableEditor { }; self.panes.remove(&secondary.pane, cx).unwrap(); self.primary_editor.update(cx, |primary, cx| { - primary.buffer().update(cx, |buffer, _| { - buffer.set_filter_mode(None); + primary.set_delegate_expand_excerpts(false); + primary.buffer().update(cx, |buffer, cx| { + buffer.set_show_deleted_hunks(true, cx); }); }); cx.notify(); @@ -228,6 +291,299 @@ impl SplittableEditor { }); } } + + pub fn set_excerpts_for_path( + &mut self, + path: PathKey, + buffer: Entity, + ranges: impl IntoIterator> + Clone, + context_line_count: u32, + diff: Entity, + cx: &mut Context, + ) -> (Vec>, bool) { + self.primary_multibuffer + .update(cx, |primary_multibuffer, cx| { + let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path( + path.clone(), + buffer, + ranges, + context_line_count, + cx, + ); + primary_multibuffer.add_diff(diff.clone(), cx); + if let Some(secondary) = &mut self.secondary { + secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx); + } + (anchors, added_a_new_excerpt) + }) + } + + fn expand_excerpts( + &mut self, + excerpt_ids: impl Iterator + Clone, + lines: u32, + direction: ExpandExcerptDirection, + cx: &mut Context, + ) { + let mut corresponding_paths = HashMap::default(); + self.primary_multibuffer.update(cx, |multibuffer, cx| { + let snapshot = multibuffer.snapshot(cx); + if self.secondary.is_some() { + corresponding_paths = excerpt_ids + .clone() + .map(|excerpt_id| { + let path = multibuffer.path_for_excerpt(excerpt_id).unwrap(); + let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap(); + let diff = multibuffer.diff_for(buffer.remote_id()).unwrap(); + (path, diff) + }) + .collect::>(); + } + multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx); + }); + + if let Some(secondary) = &mut self.secondary { + self.primary_multibuffer.update(cx, |multibuffer, cx| { + for (path, diff) in corresponding_paths { + secondary.sync_path_excerpts(path, multibuffer, diff, cx); + } + }) + } + } + + pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { + self.primary_multibuffer.update(cx, |buffer, cx| { + buffer.remove_excerpts_for_path(path.clone(), cx) + }); + if let Some(secondary) = &mut self.secondary { + secondary.remove_mappings_for_path(&path, cx); + secondary + .multibuffer + .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx)) + } + } +} + +#[cfg(test)] +impl SplittableEditor { + fn check_invariants(&self, quiesced: bool, cx: &App) { + use buffer_diff::DiffHunkStatusKind; + use collections::HashSet; + use multi_buffer::MultiBufferOffset; + use multi_buffer::MultiBufferRow; + use multi_buffer::MultiBufferSnapshot; + + fn format_diff(snapshot: &MultiBufferSnapshot) -> String { + let text = snapshot.text(); + let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); + let boundary_rows = snapshot + .excerpt_boundaries_in_range(MultiBufferOffset(0)..) + .map(|b| b.row) + .collect::>(); + + text.split('\n') + .enumerate() + .zip(row_infos) + .map(|((ix, line), info)| { + let marker = match info.diff_status.map(|status| status.kind) { + Some(DiffHunkStatusKind::Added) => "+ ", + Some(DiffHunkStatusKind::Deleted) => "- ", + Some(DiffHunkStatusKind::Modified) => unreachable!(), + None => { + if !line.is_empty() { + " " + } else { + "" + } + } + }; + let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) { + " ----------\n" + } else { + "" + }; + let expand = info + .expand_info + .map(|expand_info| match expand_info.direction { + ExpandExcerptDirection::Up => " [↑]", + ExpandExcerptDirection::Down => " [↓]", + ExpandExcerptDirection::UpAndDown => " [↕]", + }) + .unwrap_or_default(); + + format!("{boundary_row}{marker}{line}{expand}") + }) + .collect::>() + .join("\n") + } + + let Some(secondary) = &self.secondary else { + return; + }; + + log::info!( + "primary:\n\n{}", + format_diff(&self.primary_multibuffer.read(cx).snapshot(cx)) + ); + + log::info!( + "secondary:\n\n{}", + format_diff(&secondary.multibuffer.read(cx).snapshot(cx)) + ); + + let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids(); + let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids(); + assert_eq!(primary_excerpts.len(), secondary_excerpts.len()); + + assert_eq!( + secondary.primary_to_secondary.len(), + primary_excerpts.len(), + "primary_to_secondary mapping count should match excerpt count" + ); + assert_eq!( + secondary.secondary_to_primary.len(), + secondary_excerpts.len(), + "secondary_to_primary mapping count should match excerpt count" + ); + + for primary_id in &primary_excerpts { + assert!( + secondary.primary_to_secondary.contains_key(primary_id), + "primary excerpt {:?} should have a mapping to secondary", + primary_id + ); + } + for secondary_id in &secondary_excerpts { + assert!( + secondary.secondary_to_primary.contains_key(secondary_id), + "secondary excerpt {:?} should have a mapping to primary", + secondary_id + ); + } + + for (primary_id, secondary_id) in &secondary.primary_to_secondary { + assert_eq!( + secondary.secondary_to_primary.get(secondary_id), + Some(primary_id), + "mappings should be bijective" + ); + } + + if quiesced { + let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx); + let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx); + let primary_diff_hunks = primary_snapshot + .diff_hunks() + .map(|hunk| hunk.diff_base_byte_range) + .collect::>(); + let secondary_diff_hunks = secondary_snapshot + .diff_hunks() + .map(|hunk| hunk.diff_base_byte_range) + .collect::>(); + pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks); + + // Filtering out empty lines is a bit of a hack, to work around a case where + // the base text has a trailing newline but the current text doesn't, or vice versa. + // In this case, we get the additional newline on one side, but that line is not + // marked as added/deleted by rowinfos. + let primary_unmodified_rows = primary_snapshot + .text() + .split("\n") + .zip(primary_snapshot.row_infos(MultiBufferRow(0))) + .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none()) + .map(|(line, _)| line.to_owned()) + .collect::>(); + let secondary_unmodified_rows = secondary_snapshot + .text() + .split("\n") + .zip(secondary_snapshot.row_infos(MultiBufferRow(0))) + .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none()) + .map(|(line, _)| line.to_owned()) + .collect::>(); + pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows); + } + } + + fn randomly_edit_excerpts( + &mut self, + rng: &mut impl rand::Rng, + mutation_count: usize, + cx: &mut Context, + ) { + use collections::HashSet; + use rand::prelude::*; + use std::env; + use util::RandomCharIter; + + let max_excerpts = env::var("MAX_EXCERPTS") + .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) + .unwrap_or(5); + + for _ in 0..mutation_count { + let paths = self + .primary_multibuffer + .read(cx) + .paths() + .cloned() + .collect::>(); + let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids(); + + if rng.random_bool(0.1) && !excerpt_ids.is_empty() { + let mut excerpts = HashSet::default(); + for _ in 0..rng.random_range(0..excerpt_ids.len()) { + excerpts.extend(excerpt_ids.choose(rng).copied()); + } + + let line_count = rng.random_range(0..5); + + log::info!("Expanding excerpts {excerpts:?} by {line_count} lines"); + + self.expand_excerpts( + excerpts.iter().cloned(), + line_count, + ExpandExcerptDirection::UpAndDown, + cx, + ); + continue; + } + + if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) { + let len = rng.random_range(100..500); + let text = RandomCharIter::new(&mut *rng).take(len).collect::(); + let buffer = cx.new(|cx| Buffer::local(text, cx)); + log::info!( + "Creating new buffer {} with text: {:?}", + buffer.read(cx).remote_id(), + buffer.read(cx).text() + ); + let buffer_snapshot = buffer.read(cx).snapshot(); + let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx)); + // Create some initial diff hunks. + buffer.update(cx, |buffer, cx| { + buffer.randomly_edit(rng, 1, cx); + }); + let buffer_snapshot = buffer.read(cx).text_snapshot(); + let ranges = diff.update(cx, |diff, cx| { + diff.recalculate_diff_sync(&buffer_snapshot, cx); + diff.snapshot(cx) + .hunks(&buffer_snapshot) + .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot)) + .collect::>() + }); + let path = PathKey::for_buffer(&buffer, cx); + self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx); + } else { + let remove_count = rng.random_range(1..=paths.len()); + let paths_to_remove = paths + .choose_multiple(rng, remove_count) + .cloned() + .collect::>(); + for path in paths_to_remove { + self.remove_excerpts_for_path(path.clone(), cx); + } + } + } + } } impl EventEmitter for SplittableEditor {} @@ -265,3 +621,223 @@ impl Render for SplittableEditor { .child(inner) } } + +impl SecondaryEditor { + fn sync_path_excerpts( + &mut self, + path_key: PathKey, + primary_multibuffer: &mut MultiBuffer, + diff: Entity, + cx: &mut App, + ) { + let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else { + self.remove_mappings_for_path(&path_key, cx); + self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts_for_path(path_key, cx); + }); + return; + }; + + let primary_excerpt_ids: Vec = + primary_multibuffer.excerpts_for_path(&path_key).collect(); + + let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx); + let main_buffer = primary_multibuffer_snapshot + .buffer_for_excerpt(excerpt_id) + .unwrap(); + let base_text_buffer = diff.read(cx).base_text_buffer(); + let diff_snapshot = diff.read(cx).snapshot(cx); + let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot(); + let new = primary_multibuffer + .excerpts_for_buffer(main_buffer.remote_id(), cx) + .into_iter() + .map(|(_, excerpt_range)| { + let point_range_to_base_text_point_range = |range: Range| { + let start_row = diff_snapshot.row_to_base_text_row( + range.start.row, + Bias::Left, + main_buffer, + ); + let end_row = + diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer); + let end_column = diff_snapshot.base_text().line_len(end_row); + Point::new(start_row, 0)..Point::new(end_row, end_column) + }; + let primary = excerpt_range.primary.to_point(main_buffer); + let context = excerpt_range.context.to_point(main_buffer); + ExcerptRange { + primary: point_range_to_base_text_point_range(primary), + context: point_range_to_base_text_point_range(context), + } + }) + .collect(); + + let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap(); + + self.remove_mappings_for_path(&path_key, cx); + + self.editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.update_path_excerpts( + path_key.clone(), + base_text_buffer, + &base_text_buffer_snapshot, + new, + cx, + ); + buffer.add_inverted_diff(diff, main_buffer, cx); + }) + }); + + let secondary_excerpt_ids: Vec = self + .multibuffer + .read(cx) + .excerpts_for_path(&path_key) + .collect(); + + for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids) + { + self.primary_to_secondary.insert(primary_id, secondary_id); + self.secondary_to_primary.insert(secondary_id, primary_id); + } + } + + fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) { + let secondary_excerpt_ids: Vec = self + .multibuffer + .read(cx) + .excerpts_for_path(path_key) + .collect(); + + for secondary_id in secondary_excerpt_ids { + if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) { + self.primary_to_secondary.remove(&primary_id); + } + } + } +} + +#[cfg(test)] +mod tests { + use fs::FakeFs; + use gpui::AppContext as _; + use language::Capability; + use multi_buffer::{MultiBuffer, PathKey}; + use project::Project; + use rand::rngs::StdRng; + use settings::SettingsStore; + use ui::VisualContext as _; + use workspace::Workspace; + + use crate::SplittableEditor; + + fn init_test(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); + crate::init(cx); + }); + } + + #[gpui::test(iterations = 100)] + async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) { + use rand::prelude::*; + + init_test(cx); + let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let primary_multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer + }); + let editor = cx.new_window_entity(|window, cx| { + let mut editor = + SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx); + editor.split(&Default::default(), window, cx); + editor + }); + + let operations = std::env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(20); + let rng = &mut rng; + for _ in 0..operations { + editor.update(cx, |editor, cx| { + let buffers = editor + .primary_editor + .read(cx) + .buffer() + .read(cx) + .all_buffers(); + + if buffers.is_empty() { + editor.randomly_edit_excerpts(rng, 2, cx); + editor.check_invariants(true, cx); + return; + } + + let quiesced = match rng.random_range(0..100) { + 0..=69 if !buffers.is_empty() => { + let buffer = buffers.iter().choose(rng).unwrap(); + buffer.update(cx, |buffer, cx| { + if rng.random() { + log::info!("randomly editing single buffer"); + buffer.randomly_edit(rng, 5, cx); + } else { + log::info!("randomly undoing/redoing in single buffer"); + buffer.randomly_undo_redo(rng, cx); + } + }); + false + } + 70..=79 => { + log::info!("mutating excerpts"); + editor.randomly_edit_excerpts(rng, 2, cx); + false + } + 80..=89 if !buffers.is_empty() => { + log::info!("recalculating buffer diff"); + let buffer = buffers.iter().choose(rng).unwrap(); + let diff = editor + .primary_multibuffer + .read(cx) + .diff_for(buffer.read(cx).remote_id()) + .unwrap(); + let buffer_snapshot = buffer.read(cx).text_snapshot(); + diff.update(cx, |diff, cx| { + diff.recalculate_diff_sync(&buffer_snapshot, cx); + }); + false + } + _ => { + log::info!("quiescing"); + for buffer in buffers { + let buffer_snapshot = buffer.read(cx).text_snapshot(); + let diff = editor + .primary_multibuffer + .read(cx) + .diff_for(buffer.read(cx).remote_id()) + .unwrap(); + diff.update(cx, |diff, cx| { + diff.recalculate_diff_sync(&buffer_snapshot, cx); + }); + let diff_snapshot = diff.read(cx).snapshot(cx); + let ranges = diff_snapshot + .hunks(&buffer_snapshot) + .map(|hunk| hunk.range) + .collect::>(); + let path = PathKey::for_buffer(&buffer, cx); + editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx); + } + true + } + }; + + editor.check_invariants(quiesced, cx); + }); + } + } +} diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index c4d076037f637ffdf2b8d4c8bbed05349d9ea38e..b6f93bcc875a849bbdec60c60b595fbcd1b8c5d8 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -365,11 +365,12 @@ impl ExampleContext { let snapshot = buffer.read(cx).snapshot(); let file = snapshot.file().unwrap(); - let diff = diff.read(cx); - let base_text = diff.base_text().text(); + let base_text = diff.read(cx).base_text(cx).text(); let hunks = diff - .hunks(&snapshot, cx) + .read(cx) + .snapshot(cx) + .hunks(&snapshot) .map(|hunk| FileEditHunk { base_text: base_text[hunk.diff_base_byte_range.clone()].to_string(), text: snapshot diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index fd29328c058e4ce7aed0f4015c99c3788aceb6c1..1f242e7caf26d9a670a580dcba4074258564e5d5 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -1,5 +1,5 @@ use anyhow::{Context as _, Result}; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use buffer_diff::BufferDiff; use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle}; use editor::{Editor, EditorEvent, ExcerptRange, MultiBuffer, multibuffer_context_lines}; use git::repository::{CommitDetails, CommitDiff, RepoPath}; @@ -262,7 +262,8 @@ impl CommitView { let snapshot = buffer.read(cx).snapshot(); let path = snapshot.file().unwrap().path().clone(); let excerpt_ranges = { - let mut hunks = buffer_diff.read(cx).hunks(&snapshot, cx).peekable(); + let diff_snapshot = buffer_diff.read(cx).snapshot(cx); + let mut hunks = diff_snapshot.hunks(&snapshot).peekable(); if hunks.peek().is_none() { vec![language::Point::zero()..snapshot.max_point()] } else { @@ -785,35 +786,30 @@ async fn build_buffer_diff( LineEnding::normalize(old_text); } + let language = cx.update(|cx| buffer.read(cx).language().cloned())?; let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text.as_deref().unwrap_or("").into(), - buffer.language().cloned(), - Some(language_registry.clone()), - cx, - ) - })? - .await; + let diff = cx.new(|cx| BufferDiff::new(&buffer.text, cx))?; - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( buffer.text.clone(), - old_text.map(Arc::new), - base_buffer, + old_text.map(|old_text| Arc::from(old_text.as_str())), + true, + language.clone(), cx, ) })? .await; - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer.text, cx); - diff + diff.update(cx, |diff, cx| { + diff.language_changed(language, Some(language_registry.clone()), cx); + diff.set_snapshot(update, &buffer.text, cx) }) + .ok(); + + Ok(diff) } impl EventEmitter for CommitView {} diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index b020d7a9f3ac083f1a5adf15ca298b55063a3eb8..28ade64dd4f3a04e615b7ee063362576edb69b91 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -1,14 +1,14 @@ //! FileDiffView provides a UI for displaying differences between two buffers. use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use buffer_diff::BufferDiff; use editor::{Editor, EditorEvent, MultiBuffer}; use futures::{FutureExt, select_biased}; use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, Render, Task, Window, }; -use language::Buffer; +use language::{Buffer, LanguageRegistry}; use project::Project; use std::{ any::{Any, TypeId}, @@ -52,8 +52,9 @@ impl FileDiffView { let new_buffer = project .update(cx, |project, cx| project.open_local_buffer(&new_path, cx))? .await?; + let languages = project.update(cx, |project, _| project.languages().clone())?; - let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?; + let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?; workspace.update_in(cx, |workspace, window, cx| { let diff_view = cx.new(|cx| { @@ -143,19 +144,16 @@ impl FileDiffView { this.new_buffer.read(cx).snapshot(), ) })?; - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( - new_snapshot.text.clone(), - Some(old_snapshot.text().into()), - old_snapshot, - cx, - ) - })? - .await; diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &new_snapshot, cx) - })?; + diff.set_base_text( + Some(old_snapshot.text().as_str().into()), + old_snapshot.language().cloned(), + new_snapshot.text.clone(), + cx, + ) + })? + .await + .ok(); log::trace!("finish recalculating"); } Ok(()) @@ -167,27 +165,36 @@ impl FileDiffView { async fn build_buffer_diff( old_buffer: &Entity, new_buffer: &Entity, + language_registry: Arc, cx: &mut AsyncApp, ) -> Result> { let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( + let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx))?; + + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( new_buffer_snapshot.text.clone(), Some(old_buffer_snapshot.text().into()), - old_buffer_snapshot, + true, + new_buffer_snapshot.language().cloned(), cx, ) })? .await; - cx.new(|cx| { - let mut diff = BufferDiff::new(&new_buffer_snapshot.text, cx); - diff.set_snapshot(diff_snapshot, &new_buffer_snapshot.text, cx); - diff - }) + diff.update(cx, |diff, cx| { + diff.language_changed( + new_buffer_snapshot.language().cloned(), + Some(language_registry), + cx, + ); + diff.set_snapshot(update, &new_buffer_snapshot.text, cx); + })?; + + Ok(diff) } impl EventEmitter for FileDiffView {} diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 0e0632d9d049f54a648f65c55a96d639c9103e4d..9c5a6f9f3cf82d1578a3e69cf48290a17088d079 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -423,7 +423,7 @@ impl ProjectDiff { let mut has_staged_hunks = false; let mut has_unstaged_hunks = false; for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) { - match hunk.secondary_status { + match hunk.status.secondary { DiffHunkSecondaryStatus::HasSecondaryHunk | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { has_unstaged_hunks = true; @@ -525,14 +525,13 @@ impl ProjectDiff { .expect("project diff editor should have a conflict addon"); let snapshot = buffer.read(cx).snapshot(); - let diff_read = diff.read(cx); + let diff_snapshot = diff.read(cx).snapshot(cx); let excerpt_ranges = { - let diff_hunk_ranges = diff_read + let diff_hunk_ranges = diff_snapshot .hunks_intersecting_range( - Anchor::min_max_range_for_buffer(diff_read.buffer_id), + Anchor::min_max_range_for_buffer(snapshot.remote_id()), &snapshot, - cx, ) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)); let conflicts = conflict_addon @@ -551,18 +550,21 @@ impl ProjectDiff { } }; - let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| { - let was_empty = multibuffer.is_empty(); - let (_, is_newly_added) = multibuffer.set_excerpts_for_path( + let (was_empty, is_excerpt_newly_added) = self.editor.update(cx, |editor, cx| { + let was_empty = editor + .primary_editor() + .read(cx) + .buffer() + .read(cx) + .is_empty(); + let (_, is_newly_added) = editor.set_excerpts_for_path( path_key.clone(), buffer, excerpt_ranges, multibuffer_context_lines(cx), + diff, cx, ); - if self.branch_diff.read(cx).diff_base().is_merge_base() { - multibuffer.add_diff(diff.clone(), cx); - } (was_empty, is_newly_added) }); @@ -639,9 +641,9 @@ impl ProjectDiff { } } - this.multibuffer.update(cx, |multibuffer, cx| { + this.editor.update(cx, |editor, cx| { for path in previous_paths { - if let Some(buffer) = multibuffer.buffer_for_path(&path, cx) { + if let Some(buffer) = this.multibuffer.read(cx).buffer_for_path(&path, cx) { let skip = match reason { RefreshReason::DiffChanged | RefreshReason::EditorSaved => { buffer.read(cx).is_dirty() @@ -654,7 +656,7 @@ impl ProjectDiff { } this.buffer_diff_subscriptions.remove(&path.path); - multibuffer.remove_excerpts_for_path(path.clone(), cx); + editor.remove_excerpts_for_path(path, cx); } }); buffers_to_load @@ -1689,12 +1691,6 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let diff = cx.new_window_entity(|window, cx| { - ProjectDiff::new(project.clone(), workspace, window, cx) - }); - cx.run_until_parked(); fs.set_head_for_repo( path!("/project/.git").as_ref(), @@ -1705,6 +1701,12 @@ mod tests { path!("/project/.git").as_ref(), &[("foo.txt", "foo\n".into())], ); + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let diff = cx.new_window_entity(|window, cx| { + ProjectDiff::new(project.clone(), workspace, window, cx) + }); cx.run_until_parked(); let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); @@ -1712,8 +1714,8 @@ mod tests { &editor, cx, &" - - foo - + ˇFOO + - ˇfoo + + FOO " .unindent(), ); @@ -1820,6 +1822,12 @@ mod tests { let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + fs.set_head_for_repo( + path!("/project/.git").as_ref(), + &[("foo", "original\n".into())], + "deadbeef", + ); + let buffer = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/foo"), cx) @@ -1834,13 +1842,6 @@ mod tests { }); cx.run_until_parked(); - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("foo", "original\n".into())], - "deadbeef", - ); - cx.run_until_parked(); - let diff_editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone()); @@ -1848,8 +1849,8 @@ mod tests { &diff_editor, cx, &" - - original - + ˇmodified + - ˇoriginal + + modified " .unindent(), ); @@ -1912,9 +1913,9 @@ mod tests { &diff_editor, cx, &" - - original + - ˇoriginal + different - ˇ" + " .unindent(), ); } diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 56d55415ba01f893453824be00b9eb8d6bd31a90..25f4603e4cf70bcf23e1dc64dd171b70a1d231c5 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -1,7 +1,7 @@ //! TextDiffView currently provides a UI for displaying differences between the clipboard and selected text. use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use buffer_diff::BufferDiff; use editor::{Editor, EditorEvent, MultiBuffer, ToPoint, actions::DiffClipboardWithSelectionData}; use futures::{FutureExt, select_biased}; use gpui::{ @@ -257,23 +257,25 @@ async fn update_diff_buffer( cx: &mut AsyncApp, ) -> Result<()> { let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; + let language = source_buffer_snapshot.language().cloned(); let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; let base_text = base_buffer_snapshot.text(); - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( source_buffer_snapshot.text.clone(), - Some(Arc::new(base_text)), - base_buffer_snapshot, + Some(Arc::from(base_text.as_str())), + true, + language, cx, ) })? .await; diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &source_buffer_snapshot.text, cx); + diff.set_snapshot(update, &source_buffer_snapshot.text, cx); })?; Ok(()) } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 51696ba09e4bdb1c6be065f63d3ee7ff634e6b1a..4b23f344c9d27412daf6a630e04df9a2e67ff726 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -111,8 +111,8 @@ impl Anchor { .get(&excerpt.buffer_id) .map(|diff| diff.base_text()) { - let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); + let self_anchor = self.diff_base_anchor.filter(|a| a.is_valid(base_text)); + let other_anchor = other.diff_base_anchor.filter(|a| a.is_valid(base_text)); return match (self_anchor, other_anchor) { (Some(a), Some(b)) => a.cmp(&b, base_text), (Some(_), None) => match other.text_anchor.bias { @@ -146,7 +146,7 @@ impl Anchor { .diffs .get(&excerpt.buffer_id) .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) + && a.is_valid(&base_text) { return a.bias_left(base_text); } @@ -169,7 +169,7 @@ impl Anchor { .diffs .get(&excerpt.buffer_id) .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) + && a.is_valid(&base_text) { return a.bias_right(base_text); } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 3e96a81b387fabeee77c6790dd66433da99b3985..d1a6ca83144311419ff2d04bfac1939ab391f463 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -10,12 +10,12 @@ pub use anchor::{Anchor, AnchorRangeExt}; use anyhow::{Result, anyhow}; use buffer_diff::{ - BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkSecondaryStatus, DiffHunkStatus, - DiffHunkStatusKind, + BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunk, DiffHunkSecondaryStatus, + DiffHunkStatus, DiffHunkStatusKind, }; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; -use gpui::{App, Context, Entity, EntityId, EventEmitter}; +use gpui::{App, Context, Entity, EntityId, EventEmitter, WeakEntity}; use itertools::Itertools; use language::{ AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, @@ -36,9 +36,7 @@ use std::{ any::type_name, borrow::Cow, cell::{Cell, Ref, RefCell}, - cmp, - collections::VecDeque, - fmt::{self, Debug}, + cmp, fmt, future::Future, io, iter::{self, FromIterator}, @@ -64,9 +62,6 @@ pub use self::path_key::PathKey; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ExcerptId(u32); -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct BaseTextRow(pub u32); - /// One or more [`Buffers`](Buffer) being edited in a single view. /// /// See @@ -93,14 +88,6 @@ pub struct MultiBuffer { /// The writing capability of the multi-buffer. capability: Capability, buffer_changed_since_sync: Rc>, - follower: Option>, - filter_mode: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MultiBufferFilterMode { - KeepInsertions, - KeepDeletions, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -151,25 +138,15 @@ pub struct MultiBufferDiffHunk { pub excerpt_id: ExcerptId, /// The range within the buffer's diff base that this hunk corresponds to. pub diff_base_byte_range: Range, - /// Whether or not this hunk also appears in the 'secondary diff'. - pub secondary_status: DiffHunkSecondaryStatus, + /// The status of this hunk (added/modified/deleted and secondary status). + pub status: DiffHunkStatus, /// The word diffs for this hunk. pub word_diffs: Vec>, } impl MultiBufferDiffHunk { pub fn status(&self) -> DiffHunkStatus { - let kind = if self.buffer_range.start == self.buffer_range.end { - DiffHunkStatusKind::Deleted - } else if self.diff_base_byte_range.is_empty() { - DiffHunkStatusKind::Added - } else { - DiffHunkStatusKind::Modified - }; - DiffHunkStatus { - kind, - secondary: self.secondary_status, - } + self.status } pub fn is_created_file(&self) -> bool { @@ -522,14 +499,48 @@ struct BufferState { struct DiffState { diff: Entity, + /// If set, this diff is "inverted" (i.e. showing additions as deletions). + /// This is used in the side-by-side diff view. The main_buffer is the + /// editable buffer, while the excerpt shows the base text. + main_buffer: Option>, _subscription: gpui::Subscription, } +impl DiffState { + fn snapshot(&self, cx: &App) -> DiffStateSnapshot { + DiffStateSnapshot { + diff: self.diff.read(cx).snapshot(cx), + main_buffer: self.main_buffer.as_ref().and_then(|main_buffer| { + main_buffer + .read_with(cx, |main_buffer, _| main_buffer.text_snapshot()) + .ok() + }), + } + } +} + +#[derive(Clone)] +struct DiffStateSnapshot { + diff: BufferDiffSnapshot, + main_buffer: Option, +} + +impl std::ops::Deref for DiffStateSnapshot { + type Target = BufferDiffSnapshot; + + fn deref(&self) -> &Self::Target { + &self.diff + } +} + impl DiffState { fn new(diff: Entity, cx: &mut Context) -> Self { DiffState { _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event { - BufferDiffEvent::DiffChanged { changed_range } => { + BufferDiffEvent::DiffChanged { + changed_range, + base_text_changed_range: _, + } => { if let Some(changed_range) = changed_range.clone() { this.buffer_diff_changed(diff, changed_range, cx) } @@ -539,6 +550,42 @@ impl DiffState { _ => {} }), diff, + main_buffer: None, + } + } + + fn new_inverted( + diff: Entity, + main_buffer: Entity, + cx: &mut Context, + ) -> Self { + let main_buffer = main_buffer.downgrade(); + DiffState { + _subscription: cx.subscribe(&diff, { + let main_buffer = main_buffer.clone(); + move |this, diff, event, cx| match event { + BufferDiffEvent::DiffChanged { + changed_range: _, + base_text_changed_range, + } => { + if let Some(base_text_changed_range) = base_text_changed_range.clone() { + this.inverted_buffer_diff_changed( + diff, + base_text_changed_range, + main_buffer.clone(), + cx, + ) + } + cx.emit(Event::BufferDiffChanged); + } + BufferDiffEvent::LanguageChanged => { + this.inverted_buffer_diff_language_changed(diff, main_buffer.clone(), cx) + } + _ => {} + } + }), + diff, + main_buffer: Some(main_buffer), } } } @@ -547,50 +594,57 @@ impl DiffState { #[derive(Clone, Default)] pub struct MultiBufferSnapshot { excerpts: SumTree, - diffs: TreeMap, + diffs: TreeMap, diff_transforms: SumTree, non_text_state_update_count: usize, edit_count: usize, is_dirty: bool, has_deleted_file: bool, has_conflict: bool, + has_inverted_diff: bool, /// immutable fields singleton: bool, excerpt_ids: SumTree, replaced_excerpts: TreeMap, trailing_excerpt_update_count: usize, all_diff_hunks_expanded: bool, + show_deleted_hunks: bool, show_headers: bool, } +// follower: None +// - BufferContent(Some) +// - BufferContent(None) +// - DeletedHunk +// +// follower: Some +// - BufferContent(Some) +// - BufferContent(None) + #[derive(Debug, Clone)] -/// A piece of text in the multi-buffer enum DiffTransform { - Unmodified { + // RealText + BufferContent { summary: MBTextSummary, + // modified_hunk_info + inserted_hunk_info: Option, }, - InsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, - FilteredInsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, + // ExpandedHunkText DeletedHunk { summary: TextSummary, buffer_id: BufferId, hunk_info: DiffTransformHunkInfo, + base_text_byte_range: Range, has_trailing_newline: bool, }, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] struct DiffTransformHunkInfo { excerpt_id: ExcerptId, hunk_start_anchor: text::Anchor, hunk_secondary_status: DiffHunkSecondaryStatus, - base_text_byte_range: Range, + is_logically_deleted: bool, } impl Eq for DiffTransformHunkInfo {} @@ -617,15 +671,6 @@ pub struct ExcerptInfo { pub end_row: MultiBufferRow, } -/// Used with [`MultiBuffer::push_buffer_content_transform`] -#[derive(Clone, Debug)] -struct CurrentInsertedHunk { - hunk_excerpt_start: ExcerptOffset, - insertion_end_offset: ExcerptOffset, - hunk_info: DiffTransformHunkInfo, - is_filtered: bool, -} - impl std::fmt::Debug for ExcerptInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(type_name::()) @@ -665,7 +710,6 @@ pub struct ExpandInfo { pub struct RowInfo { pub buffer_id: Option, pub buffer_row: Option, - pub base_text_row: Option, pub multibuffer_row: Option, pub diff_status: Option, pub expand_info: Option, @@ -901,7 +945,7 @@ pub struct MultiBufferChunks<'a> { excerpts: Cursor<'a, 'static, Excerpt, ExcerptOffset>, diff_transforms: Cursor<'a, 'static, DiffTransform, Dimensions>, - diffs: &'a TreeMap, + diffs: &'a TreeMap, diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, buffer_chunk: Option>, range: Range, @@ -961,12 +1005,10 @@ impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for Diff struct MultiBufferCursor<'a, MBD, BD> { excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension>, diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms>, - snapshot: &'a MultiBufferSnapshot, + diffs: &'a TreeMap, cached_region: Option>, } -/// Matches transformations to an item -/// This is essentially a more detailed version of DiffTransform #[derive(Clone)] struct MultiBufferRegion<'a, MBD, BD> { buffer: &'a BufferSnapshot, @@ -974,21 +1016,10 @@ struct MultiBufferRegion<'a, MBD, BD> { diff_hunk_status: Option, excerpt: &'a Excerpt, buffer_range: Range, - diff_base_byte_range: Option>, range: Range, has_trailing_newline: bool, } -impl<'a, MBD, BD> MultiBufferRegion<'a, MBD, BD> -where - MBD: Ord, - BD: Ord, -{ - fn is_filtered(&self) -> bool { - self.range.is_empty() && self.buffer_range.is_empty() && self.diff_hunk_status == None - } -} - struct ExcerptChunks<'a> { excerpt_id: ExcerptId, content_chunks: BufferChunks<'a>, @@ -1058,13 +1089,20 @@ impl MultiBuffer { capability, MultiBufferSnapshot { show_headers: true, + show_deleted_hunks: true, ..MultiBufferSnapshot::default() }, ) } pub fn without_headers(capability: Capability) -> Self { - Self::new_(capability, Default::default()) + Self::new_( + capability, + MultiBufferSnapshot { + show_deleted_hunks: true, + ..MultiBufferSnapshot::default() + }, + ) } pub fn singleton(buffer: Entity, cx: &mut Context) -> Self { @@ -1072,6 +1110,7 @@ impl MultiBuffer { buffer.read(cx).capability(), MultiBufferSnapshot { singleton: true, + show_deleted_hunks: true, ..MultiBufferSnapshot::default() }, ); @@ -1101,8 +1140,6 @@ impl MultiBuffer { paths_by_excerpt: Default::default(), buffer_changed_since_sync: Default::default(), history: History::default(), - follower: None, - filter_mode: None, } } @@ -1136,8 +1173,8 @@ impl MultiBuffer { Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), buffers: buffers, - excerpts_by_path: self.excerpts_by_path.clone(), - paths_by_excerpt: self.paths_by_excerpt.clone(), + excerpts_by_path: Default::default(), + paths_by_excerpt: Default::default(), diffs: diff_bases, subscriptions: Default::default(), singleton: self.singleton, @@ -1145,46 +1182,6 @@ impl MultiBuffer { history: self.history.clone(), title: self.title.clone(), buffer_changed_since_sync, - follower: None, - filter_mode: None, - } - } - - pub fn get_or_create_follower(&mut self, cx: &mut Context) -> Entity { - use gpui::AppContext as _; - - if let Some(follower) = &self.follower { - return follower.clone(); - } - - let follower = cx.new(|cx| self.clone(cx)); - follower.update(cx, |follower, _cx| { - follower.capability = Capability::ReadOnly; - }); - self.follower = Some(follower.clone()); - follower - } - - pub fn set_filter_mode(&mut self, new_mode: Option) { - self.filter_mode = new_mode; - let excerpt_len = self - .snapshot - .get_mut() - .diff_transforms - .summary() - .excerpt_len(); - let edits = Self::sync_diff_transforms( - self.snapshot.get_mut(), - vec![Edit { - old: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - new: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - }], - // TODO(split-diff) is this right? - DiffChangeKind::BufferEdited, - new_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); } } @@ -1668,7 +1665,7 @@ impl MultiBuffer { cx: &mut Context, ) -> Vec where - O: text::ToOffset + Clone, + O: text::ToOffset, { self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) } @@ -1707,7 +1704,7 @@ impl MultiBuffer { cx: &mut Context, ) -> Vec where - O: text::ToOffset + Clone, + O: text::ToOffset, { let mut ids = Vec::new(); let mut next_excerpt_id = @@ -1736,13 +1733,10 @@ impl MultiBuffer { ranges: impl IntoIterator)>, cx: &mut Context, ) where - O: text::ToOffset + Clone, + O: text::ToOffset, { - // TODO(split-diff) see if it's worth time avoiding collecting here later - let collected_ranges: Vec<_> = ranges.into_iter().collect(); - assert_eq!(self.history.transaction_depth(), 0); - let mut ranges = collected_ranges.iter().cloned().peekable(); + let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_none() { return Default::default(); } @@ -1842,23 +1836,11 @@ impl MultiBuffer { new: edit_start..edit_end, }], DiffChangeKind::BufferEdited, - self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer.clone(), - collected_ranges, - cx, - ); - }) - } - cx.emit(Event::Edited { edited_buffer: None, }); @@ -1885,11 +1867,13 @@ impl MultiBuffer { is_dirty, has_deleted_file, has_conflict, + has_inverted_diff, singleton: _, excerpt_ids: _, replaced_excerpts, trailing_excerpt_update_count, all_diff_hunks_expanded: _, + show_deleted_hunks: _, show_headers: _, } = self.snapshot.get_mut(); let start = ExcerptDimension(MultiBufferOffset::ZERO); @@ -1899,6 +1883,7 @@ impl MultiBuffer { *is_dirty = false; *has_deleted_file = false; *has_conflict = false; + *has_inverted_diff = false; replaced_excerpts.clear(); let edits = Self::sync_diff_transforms( @@ -1908,16 +1893,10 @@ impl MultiBuffer { new: start..start, }], DiffChangeKind::BufferEdited, - self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.clear(cx); - }) - } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2203,26 +2182,22 @@ impl MultiBuffer { snapshot.diffs.remove(buffer_id); } + // Recalculate has_inverted_diff after removing diffs + if !removed_buffer_ids.is_empty() { + snapshot.has_inverted_diff = snapshot + .diffs + .iter() + .any(|(_, diff)| diff.main_buffer.is_some()); + } + if changed_trailing_excerpt { snapshot.trailing_excerpt_update_count += 1; } - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); if !edits.is_empty() { self.subscriptions.publish(edits); } - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts(ids.clone(), cx); - }) - } - cx.emit(Event::Edited { edited_buffer: None, }); @@ -2312,10 +2287,33 @@ impl MultiBuffer { fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let diff = diff.snapshot(cx); + let diff = DiffStateSnapshot { + diff: diff.snapshot(cx), + main_buffer: None, + }; self.snapshot.get_mut().diffs.insert(buffer_id, diff); } + fn inverted_buffer_diff_language_changed( + &mut self, + diff: Entity, + main_buffer: WeakEntity, + cx: &mut Context, + ) { + let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); + let diff = diff.read(cx); + let diff = DiffStateSnapshot { + diff: diff.snapshot(cx), + main_buffer: main_buffer + .update(cx, |main_buffer, _| main_buffer.text_snapshot()) + .ok(), + }; + self.snapshot + .get_mut() + .diffs + .insert(base_text_buffer_id, diff); + } + fn buffer_diff_changed( &mut self, diff: Entity, @@ -2326,62 +2324,32 @@ impl MultiBuffer { let diff = diff.read(cx); let buffer_id = diff.buffer_id; + let Some(buffer_state) = self.buffers.get(&buffer_id) else { return; }; - self.buffer_changed_since_sync.replace(true); - - let buffer = buffer_state.buffer.read(cx); - let diff_change_range = range.to_offset(buffer); - - let new_diff = diff.snapshot(cx); + let new_diff = DiffStateSnapshot { + diff: diff.snapshot(cx), + main_buffer: None, + }; let mut snapshot = self.snapshot.get_mut(); let base_text_changed = snapshot .diffs .get(&buffer_id) - .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); - + .is_none_or(|old_diff| !new_diff.base_texts_definitely_eq(old_diff)); snapshot.diffs.insert_or_replace(buffer_id, new_diff); + self.buffer_changed_since_sync.replace(true); - let mut excerpt_edits = Vec::new(); - for locator in &buffer_state.excerpts { - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *locator - { - let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); - if diff_change_range.end < excerpt_buffer_range.start - || diff_change_range.start > excerpt_buffer_range.end - { - continue; - } - let excerpt_start = cursor.start().1; - let excerpt_len = excerpt.text_summary.len; - let diff_change_start_in_excerpt = diff_change_range - .start - .saturating_sub(excerpt_buffer_range.start); - let diff_change_end_in_excerpt = diff_change_range - .end - .saturating_sub(excerpt_buffer_range.start); - let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); - let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); - excerpt_edits.push(Edit { - old: edit_start..edit_end, - new: edit_start..edit_end, - }); - } - } + let buffer = buffer_state.buffer.read(cx); + let diff_change_range = range.to_offset(buffer); + let excerpt_edits = snapshot.excerpt_edits_for_diff_change(buffer_state, diff_change_range); let edits = Self::sync_diff_transforms( &mut snapshot, excerpt_edits, DiffChangeKind::DiffUpdated { base_changed: base_text_changed, }, - self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); @@ -2391,15 +2359,64 @@ impl MultiBuffer { }); } + fn inverted_buffer_diff_changed( + &mut self, + diff: Entity, + diff_change_range: Range, + main_buffer: WeakEntity, + cx: &mut Context, + ) { + self.sync_mut(cx); + + let diff = diff.read(cx); + let base_text_buffer_id = diff.base_text(cx).remote_id(); + let Some(buffer_state) = self.buffers.get(&base_text_buffer_id) else { + return; + }; + self.buffer_changed_since_sync.replace(true); + + let new_diff = DiffStateSnapshot { + diff: diff.snapshot(cx), + main_buffer: main_buffer + .update(cx, |main_buffer, _| main_buffer.text_snapshot()) + .ok(), + }; + let mut snapshot = self.snapshot.get_mut(); + snapshot + .diffs + .insert_or_replace(base_text_buffer_id, new_diff); + + let excerpt_edits = snapshot.excerpt_edits_for_diff_change(buffer_state, diff_change_range); + let edits = Self::sync_diff_transforms( + &mut snapshot, + excerpt_edits, + DiffChangeKind::DiffUpdated { + // We don't use read this field for inverted diffs. + base_changed: false, + }, + ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } + cx.emit(Event::Edited { + edited_buffer: None, + }); + } + + pub fn all_buffers_iter(&self) -> impl Iterator> { + self.buffers.values().map(|state| state.buffer.clone()) + } + pub fn all_buffers(&self) -> HashSet> { - self.buffers - .values() - .map(|state| state.buffer.clone()) - .collect() + self.all_buffers_iter().collect() + } + + pub fn all_buffer_ids_iter(&self) -> impl Iterator { + self.buffers.keys().copied() } pub fn all_buffer_ids(&self) -> Vec { - self.buffers.keys().copied().collect() + self.all_buffer_ids_iter().collect() } pub fn buffer(&self, buffer_id: BufferId) -> Option> { @@ -2544,14 +2561,31 @@ impl MultiBuffer { text::Anchor::min_max_range_for_buffer(buffer_id), cx, ); - self.diffs - .insert(buffer_id, DiffState::new(diff.clone(), cx)); + self.diffs.insert(buffer_id, DiffState::new(diff, cx)); + } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.add_diff(diff, cx); - }) - } + pub fn add_inverted_diff( + &mut self, + diff: Entity, + main_buffer: Entity, + cx: &mut Context, + ) { + let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); + let diff_change_range = 0..diff.read(cx).base_text(cx).len(); + self.snapshot.get_mut().has_inverted_diff = true; + main_buffer.update(cx, |buffer, _| { + buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); + }); + self.inverted_buffer_diff_changed( + diff.clone(), + diff_change_range, + main_buffer.downgrade(), + cx, + ); + self.diffs.insert( + base_text_buffer_id, + DiffState::new_inverted(diff, main_buffer, cx), + ); } pub fn diff_for(&self, buffer_id: BufferId) -> Option> { @@ -2580,6 +2614,11 @@ impl MultiBuffer { self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], false, cx); } + pub fn set_show_deleted_hunks(&mut self, show: bool, cx: &mut Context) { + self.snapshot.get_mut().show_deleted_hunks = show; + self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); + } + pub fn has_multiple_hunks(&self, cx: &App) -> bool { self.read(cx) .diff_hunks_in_range(Anchor::min()..Anchor::max()) @@ -2663,7 +2702,6 @@ impl MultiBuffer { &mut snapshot, excerpt_edits, DiffChangeKind::ExpandOrCollapseHunks { expand }, - self.filter_mode, ); if !edits.is_empty() { self.subscriptions.publish(edits); @@ -2751,16 +2789,7 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| follower.resize_excerpt(id, range, cx)); - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); if !edits.is_empty() { self.subscriptions.publish(edits); } @@ -2794,7 +2823,7 @@ impl MultiBuffer { let mut cursor = snapshot .excerpts .cursor::, ExcerptOffset>>(()); - let mut excerpt_edits = Vec::>::new(); + let mut edits = Vec::>::new(); for locator in &locators { let prefix = cursor.slice(&Some(locator), Bias::Left); @@ -2846,15 +2875,15 @@ impl MultiBuffer { new: new_start_offset..new_start_offset + new_text_len, }; - if let Some(last_edit) = excerpt_edits.last_mut() { + if let Some(last_edit) = edits.last_mut() { if last_edit.old.end == edit.old.start { last_edit.old.end = edit.old.end; last_edit.new.end = edit.new.end; } else { - excerpt_edits.push(edit); + edits.push(edit); } } else { - excerpt_edits.push(edit); + edits.push(edit); } new_excerpts.push(excerpt, ()); @@ -2865,22 +2894,12 @@ impl MultiBuffer { new_excerpts.append(cursor.suffix(), ()); drop(cursor); - snapshot.excerpts = new_excerpts.clone(); + snapshot.excerpts = new_excerpts; - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits.clone(), - DiffChangeKind::BufferEdited, - self.filter_mode, - ); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); if !edits.is_empty() { self.subscriptions.publish(edits); } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.expand_excerpts(ids.clone(), line_count, direction, cx); - }) - } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2898,7 +2917,6 @@ impl MultiBuffer { &mut self.snapshot.borrow_mut(), &self.buffers, &self.diffs, - self.filter_mode, cx, ); if !edits.is_empty() { @@ -2911,13 +2929,8 @@ impl MultiBuffer { if !changed { return; } - let edits = Self::sync_from_buffer_changes( - self.snapshot.get_mut(), - &self.buffers, - &self.diffs, - self.filter_mode, - cx, - ); + let edits = + Self::sync_from_buffer_changes(self.snapshot.get_mut(), &self.buffers, &self.diffs, cx); if !edits.is_empty() { self.subscriptions.publish(edits); @@ -2928,7 +2941,6 @@ impl MultiBuffer { snapshot: &mut MultiBufferSnapshot, buffers: &HashMap, diffs: &HashMap, - filter_mode: Option, cx: &App, ) -> Vec> { let MultiBufferSnapshot { @@ -2940,11 +2952,13 @@ impl MultiBuffer { is_dirty, has_deleted_file, has_conflict, + has_inverted_diff: _, singleton: _, excerpt_ids: _, replaced_excerpts: _, trailing_excerpt_update_count: _, all_diff_hunks_expanded: _, + show_deleted_hunks: _, show_headers: _, } = snapshot; *is_dirty = false; @@ -2992,11 +3006,70 @@ impl MultiBuffer { for (id, diff) in diffs.iter() { if buffer_diff.get(id).is_none() { - buffer_diff.insert(*id, diff.diff.read(cx).snapshot(cx)); + buffer_diff.insert(*id, diff.snapshot(cx)); + } + } + + // Check for main buffer changes in inverted diffs + let mut main_buffer_changed_diffs = Vec::new(); + for (id, diff_state) in diffs.iter() { + if let Some(main_buffer) = &diff_state.main_buffer { + if let Ok(current_main_buffer) = + main_buffer.read_with(cx, |buffer, _| buffer.text_snapshot()) + { + if let Some(stored_diff) = buffer_diff.get(id) { + if let Some(stored_main_buffer) = &stored_diff.main_buffer { + if current_main_buffer + .version() + .changed_since(stored_main_buffer.version()) + { + main_buffer_changed_diffs.push(( + *id, + stored_diff.clone(), + current_main_buffer, + )); + } + } + } + } + } + } + + let mut inverted_diff_touch_info: HashMap< + Locator, + ( + BufferDiffSnapshot, + text::BufferSnapshot, + text::BufferSnapshot, + ), + > = HashMap::default(); + for (buffer_id, old_diff_snapshot, new_main_buffer) in &main_buffer_changed_diffs { + if let Some(old_main_buffer) = &old_diff_snapshot.main_buffer { + if let Some(buffer_state) = buffers.get(buffer_id) { + for locator in &buffer_state.excerpts { + inverted_diff_touch_info.insert( + locator.clone(), + ( + old_diff_snapshot.diff.clone(), + old_main_buffer.clone(), + new_main_buffer.clone(), + ), + ); + excerpts_to_edit.push((locator, buffer_state.buffer.clone(), false)); + } + } } } excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); + excerpts_to_edit.dedup_by(|a, b| { + if a.0 == b.0 { + b.2 |= a.2; + true + } else { + false + } + }); let mut edits = Vec::new(); let mut new_excerpts = SumTree::default(); @@ -3008,6 +3081,42 @@ impl MultiBuffer { let buffer = buffer.read(cx); let buffer_id = buffer.remote_id(); + let excerpt_old_start = cursor.start().1; + let excerpt_new_start = ExcerptDimension(new_excerpts.summary().text.len); + + if !buffer_edited + && let Some((old_diff, old_main_buffer, new_main_buffer)) = + inverted_diff_touch_info.get(locator) + { + let excerpt_buffer_start = old_excerpt + .range + .context + .start + .to_offset(&old_excerpt.buffer); + let excerpt_buffer_end = excerpt_buffer_start + old_excerpt.text_summary.len; + + for hunk in old_diff.hunks_intersecting_base_text_range( + excerpt_buffer_start..excerpt_buffer_end, + old_main_buffer, + ) { + if hunk.buffer_range.start.is_valid(new_main_buffer) { + continue; + } + let hunk_buffer_start = hunk.diff_base_byte_range.start; + if hunk_buffer_start >= excerpt_buffer_start + && hunk_buffer_start <= excerpt_buffer_end + { + let hunk_offset = hunk_buffer_start - excerpt_buffer_start; + let old_hunk_pos = excerpt_old_start + hunk_offset; + let new_hunk_pos = excerpt_new_start + hunk_offset; + edits.push(Edit { + old: old_hunk_pos..old_hunk_pos, + new: new_hunk_pos..new_hunk_pos, + }); + } + } + } + let mut new_excerpt; if buffer_edited { edits.extend( @@ -3047,18 +3156,26 @@ impl MultiBuffer { new_excerpts.push(new_excerpt, ()); cursor.next(); } - new_excerpts.append(cursor.suffix(), ()); + new_excerpts.append(cursor.suffix(), ()); + + drop(cursor); + *excerpts = new_excerpts; + + for (buffer_id, _, new_main_buffer) in main_buffer_changed_diffs { + if let Some(stored) = buffer_diff.get(&buffer_id) { + let mut updated = stored.clone(); + updated.main_buffer = Some(new_main_buffer); + buffer_diff.insert(buffer_id, updated); + } + } - drop(cursor); - *excerpts = new_excerpts; - Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited, filter_mode) + Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited) } fn sync_diff_transforms( snapshot: &mut MultiBufferSnapshot, excerpt_edits: Vec>, change_kind: DiffChangeKind, - filter_mode: Option, ) -> Vec> { if excerpt_edits.is_empty() { return vec![]; @@ -3075,8 +3192,8 @@ impl MultiBuffer { let mut at_transform_boundary = true; let mut end_of_current_insert = None; - let mut excerpt_edits: VecDeque<_> = excerpt_edits.into_iter().collect(); - while let Some(edit) = excerpt_edits.pop_front() { + let mut excerpt_edits = excerpt_edits.into_iter().peekable(); + while let Some(edit) = excerpt_edits.next() { excerpts.seek_forward(&edit.new.start, Bias::Right); if excerpts.item().is_none() && *excerpts.start() == edit.new.start { excerpts.prev(); @@ -3097,13 +3214,7 @@ impl MultiBuffer { } // Compute the start of the edit in output coordinates. - let edit_start_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { .. }) = - old_diff_transforms.item() - { - 0 - } else { - edit.old.start - old_diff_transforms.start().0 - }; + let edit_start_overshoot = edit.old.start - old_diff_transforms.start().0; let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; let edit_new_start = MultiBufferOffset((edit_old_start.0 as isize + output_delta) as usize); @@ -3117,56 +3228,12 @@ impl MultiBuffer { &mut old_expanded_hunks, snapshot, change_kind, - filter_mode, ); - // When the added range of a hunk is edited, the end anchor of the hunk may be moved later - // in response by hunks_intersecting_range to keep it at a row boundary. In KeepDeletions - // mode, we need to make sure that the whole added range is still filtered out in this situation. - // We do that by adding an additional edit that covers the rest of the hunk added range. - if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - // No additional edit needed if we've already covered the whole added range. - && current_inserted_hunk.insertion_end_offset > edit.new.end - // No additional edit needed if this edit just touched the start of the hunk - // (this also prevents pushing the deleted region for the hunk twice). - && edit.new.end > current_inserted_hunk.hunk_excerpt_start - // No additional edit needed if there is a subsequent edit that intersects - // the same hunk (the last such edit will take care of it). - && excerpt_edits.front().is_none_or(|next_edit| { - next_edit.new.start >= current_inserted_hunk.insertion_end_offset - }) - { - let overshoot = current_inserted_hunk.insertion_end_offset - edit.new.end; - let additional_edit = Edit { - old: edit.old.end..edit.old.end + overshoot, - new: edit.new.end..current_inserted_hunk.insertion_end_offset, - }; - excerpt_edits.push_front(additional_edit); - } - // Compute the end of the edit in output coordinates. - let edit_old_end_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { - .. - }) = old_diff_transforms.item() - { - ExcerptDimension(MultiBufferOffset(0)) - } else { - ExcerptDimension(MultiBufferOffset( - edit.old.end - old_diff_transforms.start().0, - )) - }; - let edit_new_end_overshoot = if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - { - let insertion_end_offset = current_inserted_hunk.insertion_end_offset; - let excerpt_len = new_diff_transforms.summary().excerpt_len(); - let base = insertion_end_offset.max(excerpt_len); - edit.new.end.saturating_sub(base) - } else { - edit.new.end - new_diff_transforms.summary().excerpt_len() - }; - let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot.0; + let edit_old_end_overshoot = edit.old.end - old_diff_transforms.start().0; + let edit_new_end_overshoot = edit.new.end - new_diff_transforms.summary().excerpt_len(); + let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot; let edit_new_end = new_diff_transforms.summary().output.len + edit_new_end_overshoot; let output_edit = Edit { old: edit_old_start..edit_old_end, @@ -3183,16 +3250,21 @@ impl MultiBuffer { // then recreate the content up to the end of this transform, to prepare // for reusing additional slices of the old transforms. if excerpt_edits - .front() + .peek() .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { - Some( - DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. }, - ) => excerpts.item().is_some_and(|excerpt| { - hunk_info.hunk_start_anchor.is_valid(&excerpt.buffer) + Some(DiffTransform::BufferContent { + inserted_hunk_info: Some(hunk), + .. + }) => excerpts.item().is_some_and(|excerpt| { + if let Some(diff) = snapshot.diffs.get(&excerpt.buffer_id) + && let Some(main_buffer) = &diff.main_buffer + { + return hunk.hunk_start_anchor.is_valid(main_buffer); + } + hunk.hunk_start_anchor.is_valid(&excerpt.buffer) }), _ => true, }; @@ -3208,7 +3280,7 @@ impl MultiBuffer { snapshot, &mut new_diff_transforms, excerpt_offset, - end_of_current_insert.as_ref(), + end_of_current_insert, ); at_transform_boundary = true; } @@ -3220,8 +3292,9 @@ impl MultiBuffer { // Ensure there's always at least one buffer content transform. if new_diff_transforms.is_empty() { new_diff_transforms.push( - DiffTransform::Unmodified { + DiffTransform::BufferContent { summary: Default::default(), + inserted_hunk_info: None, }, (), ); @@ -3245,11 +3318,10 @@ impl MultiBuffer { Dimensions, >, new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option, + end_of_current_insert: &mut Option<(ExcerptOffset, DiffTransformHunkInfo)>, old_expanded_hunks: &mut HashSet, snapshot: &MultiBufferSnapshot, change_kind: DiffChangeKind, - filter_mode: Option, ) -> bool { log::trace!( "recomputing diff transform for edit {:?} => {:?}", @@ -3273,10 +3345,12 @@ impl MultiBuffer { } // Avoid querying diff hunks if there's no possibility of hunks being expanded. + // For inverted diffs, hunks are always shown, so we can't skip this. let all_diff_hunks_expanded = snapshot.all_diff_hunks_expanded; if old_expanded_hunks.is_empty() && change_kind == DiffChangeKind::BufferEdited && !all_diff_hunks_expanded + && !snapshot.has_inverted_diff { return false; } @@ -3297,104 +3371,133 @@ impl MultiBuffer { let edit_buffer_end = excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start); let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end); - let edit_anchor_range = - buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end); - - for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { - if hunk.is_created_file() && !all_diff_hunks_expanded { - continue; - } - let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); - if hunk_buffer_range.start < excerpt_buffer_start { - log::trace!("skipping hunk that starts before excerpt"); - continue; + if let Some(main_buffer) = &diff.main_buffer { + for hunk in diff.hunks_intersecting_base_text_range( + edit_buffer_start..edit_buffer_end, + main_buffer, + ) { + let hunk_buffer_range = hunk.diff_base_byte_range.clone(); + if hunk_buffer_range.start < excerpt_buffer_start { + log::trace!("skipping hunk that starts before excerpt"); + continue; + } + hunk_buffer_range.end.to_point(&excerpt.buffer); + let hunk_excerpt_start = excerpt_start + + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start); + let hunk_excerpt_end = excerpt_end + .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start)); + Self::push_buffer_content_transform( + snapshot, + new_diff_transforms, + hunk_excerpt_start, + *end_of_current_insert, + ); + if !hunk_buffer_range.is_empty() { + let hunk_info = DiffTransformHunkInfo { + excerpt_id: excerpt.id, + hunk_start_anchor: hunk.buffer_range.start, + hunk_secondary_status: hunk.secondary_status, + is_logically_deleted: true, + }; + *end_of_current_insert = + Some((hunk_excerpt_end.min(excerpt_end), hunk_info)); + } } + } else { + let edit_anchor_range = buffer.anchor_before(edit_buffer_start) + ..buffer.anchor_after(edit_buffer_end); + for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { + if hunk.is_created_file() && !all_diff_hunks_expanded { + continue; + } - let hunk_info = DiffTransformHunkInfo { - excerpt_id: excerpt.id, - hunk_start_anchor: hunk.buffer_range.start, - hunk_secondary_status: hunk.secondary_status, - base_text_byte_range: hunk.diff_base_byte_range.clone(), - }; - - let hunk_excerpt_start = excerpt_start - + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start); - let hunk_excerpt_end = excerpt_end - .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start)); + let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); + if hunk_buffer_range.start < excerpt_buffer_start { + log::trace!("skipping hunk that starts before excerpt"); + continue; + } - Self::push_buffer_content_transform( - snapshot, - new_diff_transforms, - hunk_excerpt_start, - end_of_current_insert.as_ref(), - ); + let hunk_info = DiffTransformHunkInfo { + excerpt_id: excerpt.id, + hunk_start_anchor: hunk.buffer_range.start, + hunk_secondary_status: hunk.secondary_status, + is_logically_deleted: false, + }; - // For every existing hunk, determine if it was previously expanded - // and if it should currently be expanded. - let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); - let should_expand_hunk = match &change_kind { - DiffChangeKind::DiffUpdated { base_changed: true } => { - was_previously_expanded || all_diff_hunks_expanded - } - DiffChangeKind::ExpandOrCollapseHunks { expand } => { - let intersects = hunk_buffer_range.is_empty() - || hunk_buffer_range.end > edit_buffer_start; - if *expand { - intersects || was_previously_expanded || all_diff_hunks_expanded - } else { - !intersects && (was_previously_expanded || all_diff_hunks_expanded) - } - } - _ => was_previously_expanded || all_diff_hunks_expanded, - }; + let hunk_excerpt_start = excerpt_start + + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start); + let hunk_excerpt_end = excerpt_end + .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start)); - if should_expand_hunk { - did_expand_hunks = true; - log::trace!( - "expanding hunk {:?}, excerpt:{:?}", - hunk_excerpt_start..hunk_excerpt_end, - excerpt.id + Self::push_buffer_content_transform( + snapshot, + new_diff_transforms, + hunk_excerpt_start, + *end_of_current_insert, ); - if !hunk.diff_base_byte_range.is_empty() - && hunk_buffer_range.start >= edit_buffer_start - && hunk_buffer_range.start <= excerpt_buffer_end - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let base_text = diff.base_text(); - let mut text_cursor = - base_text.as_rope().cursor(hunk.diff_base_byte_range.start); - let mut base_text_summary = - text_cursor.summary::(hunk.diff_base_byte_range.end); - - let mut has_trailing_newline = false; - if base_text_summary.last_line_chars > 0 { - base_text_summary += TextSummary::newline(); - has_trailing_newline = true; + // For every existing hunk, determine if it was previously expanded + // and if it should currently be expanded. + let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); + let should_expand_hunk = match &change_kind { + DiffChangeKind::DiffUpdated { base_changed: true } => { + was_previously_expanded || all_diff_hunks_expanded + } + DiffChangeKind::ExpandOrCollapseHunks { expand } => { + let intersects = hunk_buffer_range.is_empty() + || hunk_buffer_range.end > edit_buffer_start; + if *expand { + intersects || was_previously_expanded || all_diff_hunks_expanded + } else { + !intersects + && (was_previously_expanded || all_diff_hunks_expanded) + } } + _ => was_previously_expanded || all_diff_hunks_expanded, + }; - new_diff_transforms.push( - DiffTransform::DeletedHunk { - summary: base_text_summary, - buffer_id: excerpt.buffer_id, - hunk_info: hunk_info.clone(), - has_trailing_newline, - }, - (), + if should_expand_hunk { + did_expand_hunks = true; + log::trace!( + "expanding hunk {:?}, excerpt:{:?}", + hunk_excerpt_start..hunk_excerpt_end, + excerpt.id ); - } - if !hunk_buffer_range.is_empty() { - let is_filtered = - filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let insertion_end_offset = hunk_excerpt_end.min(excerpt_end); - *end_of_current_insert = Some(CurrentInsertedHunk { - hunk_excerpt_start, - insertion_end_offset, - hunk_info, - is_filtered, - }); + if !hunk.diff_base_byte_range.is_empty() + && hunk_buffer_range.start >= edit_buffer_start + && hunk_buffer_range.start <= excerpt_buffer_end + && snapshot.show_deleted_hunks + { + let base_text = diff.base_text(); + let mut text_cursor = + base_text.as_rope().cursor(hunk.diff_base_byte_range.start); + let mut base_text_summary = text_cursor + .summary::(hunk.diff_base_byte_range.end); + + let mut has_trailing_newline = false; + if base_text_summary.last_line_chars > 0 { + base_text_summary += TextSummary::newline(); + has_trailing_newline = true; + } + + new_diff_transforms.push( + DiffTransform::DeletedHunk { + base_text_byte_range: hunk.diff_base_byte_range.clone(), + summary: base_text_summary, + buffer_id: excerpt.buffer_id, + hunk_info, + has_trailing_newline, + }, + (), + ); + } + + if !hunk_buffer_range.is_empty() { + *end_of_current_insert = + Some((hunk_excerpt_end.min(excerpt_end), hunk_info)); + } } } } @@ -3414,8 +3517,15 @@ impl MultiBuffer { new_transforms: &mut SumTree, subtree: SumTree, ) { - if let Some(transform) = subtree.first() - && Self::extend_last_buffer_content_transform(new_transforms, transform) + if let Some(DiffTransform::BufferContent { + inserted_hunk_info, + summary, + }) = subtree.first() + && Self::extend_last_buffer_content_transform( + new_transforms, + *inserted_hunk_info, + *summary, + ) { let mut cursor = subtree.cursor::<()>(()); cursor.next(); @@ -3427,7 +3537,16 @@ impl MultiBuffer { } fn push_diff_transform(new_transforms: &mut SumTree, transform: DiffTransform) { - if Self::extend_last_buffer_content_transform(new_transforms, &transform) { + if let DiffTransform::BufferContent { + inserted_hunk_info: inserted_hunk_anchor, + summary, + } = transform + && Self::extend_last_buffer_content_transform( + new_transforms, + inserted_hunk_anchor, + summary, + ) + { return; } new_transforms.push(transform, ()); @@ -3437,56 +3556,55 @@ impl MultiBuffer { old_snapshot: &MultiBufferSnapshot, new_transforms: &mut SumTree, end_offset: ExcerptOffset, - current_inserted_hunk: Option<&CurrentInsertedHunk>, + current_inserted_hunk: Option<(ExcerptOffset, DiffTransformHunkInfo)>, ) { - if let Some(current_inserted_hunk) = current_inserted_hunk { - let start_offset = new_transforms.summary().excerpt_len(); - let end_offset = current_inserted_hunk.insertion_end_offset.min(end_offset); - if end_offset > start_offset { - let summary_to_add = old_snapshot - .text_summary_for_excerpt_offset_range::( - start_offset..end_offset, - ); + let inserted_region = current_inserted_hunk.map(|(insertion_end_offset, hunk_info)| { + (end_offset.min(insertion_end_offset), Some(hunk_info)) + }); + let unchanged_region = [(end_offset, None)]; - let transform = if current_inserted_hunk.is_filtered { - DiffTransform::FilteredInsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - } else { - DiffTransform::InsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) - } + for (end_offset, inserted_hunk_info) in inserted_region.into_iter().chain(unchanged_region) + { + let start_offset = new_transforms.summary().excerpt_len(); + if end_offset <= start_offset { + continue; } - } - - let start_offset = new_transforms.summary().excerpt_len(); - if end_offset > start_offset { let summary_to_add = old_snapshot .text_summary_for_excerpt_offset_range::(start_offset..end_offset); - let transform = DiffTransform::Unmodified { - summary: summary_to_add, - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) + if !Self::extend_last_buffer_content_transform( + new_transforms, + inserted_hunk_info, + summary_to_add, + ) { + new_transforms.push( + DiffTransform::BufferContent { + summary: summary_to_add, + inserted_hunk_info, + }, + (), + ) } } } fn extend_last_buffer_content_transform( new_transforms: &mut SumTree, - transform: &DiffTransform, + new_inserted_hunk_info: Option, + summary_to_add: MBTextSummary, ) -> bool { let mut did_extend = false; new_transforms.update_last( |last_transform| { - did_extend = last_transform.merge_with(&transform); + if let DiffTransform::BufferContent { + summary, + inserted_hunk_info: inserted_hunk_anchor, + } = last_transform + && *inserted_hunk_anchor == new_inserted_hunk_info + { + *summary += summary_to_add; + did_extend = true; + } }, (), ); @@ -3494,72 +3612,6 @@ impl MultiBuffer { } } -impl DiffTransform { - /// Ergonomic wrapper for [`DiffTransform::merged_with`] that applies the - /// merging in-place. Returns `true` if merging was possible. - #[must_use = "check whether merging actually succeeded"] - fn merge_with(&mut self, other: &Self) -> bool { - match self.to_owned().merged_with(other) { - Some(merged) => { - *self = merged; - true - } - None => false, - } - } - - /// Attempt to merge `self` with `other`, and return the merged transform. - /// - /// This will succeed if all of the following are true: - /// - both transforms are the same variant - /// - neither transform is [`DiffTransform::DeletedHunk`] - /// - if both transform are either [`DiffTransform::InsertedHunk`] or - /// [`DiffTransform::FilteredInsertedHunk`], then their - /// `hunk_info.hunk_start_anchor`s match - #[must_use = "check whether merging actually succeeded"] - #[rustfmt::skip] - fn merged_with(self, other: &Self) -> Option { - match (self, other) { - ( - DiffTransform::Unmodified { mut summary }, - DiffTransform::Unmodified { summary: other_summary }, - ) => { - summary += *other_summary; - Some(DiffTransform::Unmodified { summary }) - } - ( - DiffTransform::FilteredInsertedHunk { mut summary, hunk_info }, - DiffTransform::FilteredInsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::FilteredInsertedHunk { summary, hunk_info }) - } else { - None - } - } - ( - DiffTransform::InsertedHunk { mut summary, hunk_info }, - DiffTransform::InsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::InsertedHunk { summary, hunk_info }) - } else { - None - } - } - _ => return None, - } - } -} - fn build_excerpt_ranges( ranges: impl IntoIterator>, context_line_count: u32, @@ -3860,19 +3912,38 @@ impl MultiBufferSnapshot { let query_range = range.start.to_point(self)..range.end.to_point(self); self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| { let diff = self.diffs.get(&buffer.remote_id())?; - let buffer_start = buffer.anchor_before(buffer_range.start); - let buffer_end = buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_start..buffer_end, buffer) - .filter_map(|hunk| { - if hunk.is_created_file() && !self.all_diff_hunks_expanded { - return None; - } - Some((hunk.range.clone(), hunk)) - }), - ) + let iter: Box> = + if let Some(main_buffer) = &diff.main_buffer { + let buffer_start = buffer.point_to_offset(buffer_range.start); + let buffer_end = buffer.point_to_offset(buffer_range.end); + Box::new( + diff.hunks_intersecting_base_text_range( + buffer_start..buffer_end, + main_buffer, + ) + .map(move |hunk| (hunk, buffer, true)), + ) + } else { + let buffer_start = buffer.anchor_before(buffer_range.start); + let buffer_end = buffer.anchor_after(buffer_range.end); + Box::new( + diff.hunks_intersecting_range(buffer_start..buffer_end, buffer) + .map(move |hunk| (hunk, buffer, false)), + ) + }; + Some(iter.filter_map(|(hunk, buffer, is_inverted)| { + if hunk.is_created_file() && !self.all_diff_hunks_expanded { + return None; + } + let range = if is_inverted { + hunk.diff_base_byte_range.to_point(&buffer) + } else { + hunk.range.clone() + }; + Some((range, (hunk, is_inverted))) + })) }) - .filter_map(move |(range, hunk, excerpt)| { + .filter_map(move |(range, (hunk, is_inverted), excerpt)| { if range.start != range.end && range.end == query_range.start && !hunk.range.is_empty() { return None; @@ -3883,33 +3954,62 @@ impl MultiBufferSnapshot { range.end.row + 1 }; - let word_diffs = (!hunk.base_word_diffs.is_empty() - || !hunk.buffer_word_diffs.is_empty()) - .then(|| { - let hunk_start_offset = - Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_offset(self); - - hunk.base_word_diffs - .iter() - .map(|diff| hunk_start_offset + diff.start..hunk_start_offset + diff.end) - .chain( - hunk.buffer_word_diffs - .into_iter() - .map(|diff| Anchor::range_in_buffer(excerpt.id, diff).to_offset(self)), - ) - .collect() - }) - .unwrap_or_default(); + let word_diffs = + (!hunk.base_word_diffs.is_empty() || !hunk.buffer_word_diffs.is_empty()) + .then(|| { + let mut word_diffs = Vec::new(); + + if self.show_deleted_hunks || is_inverted { + let hunk_start_offset = if is_inverted { + Anchor::in_buffer( + excerpt.id, + excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start), + ) + .to_offset(self) + } else { + Anchor::in_buffer(excerpt.id, hunk.buffer_range.start) + .to_offset(self) + }; + + word_diffs.extend(hunk.base_word_diffs.iter().map(|diff| { + hunk_start_offset + diff.start..hunk_start_offset + diff.end + })); + } + + if !is_inverted { + word_diffs.extend(hunk.buffer_word_diffs.into_iter().map(|diff| { + Anchor::range_in_buffer(excerpt.id, diff).to_offset(self) + })); + } + word_diffs + }) + .unwrap_or_default(); + let buffer_range = if is_inverted { + excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start) + ..excerpt.buffer.anchor_before(hunk.diff_base_byte_range.end) + } else { + hunk.buffer_range.clone() + }; + let status_kind = if hunk.buffer_range.start == hunk.buffer_range.end { + DiffHunkStatusKind::Deleted + } else if hunk.diff_base_byte_range.is_empty() { + DiffHunkStatusKind::Added + } else { + DiffHunkStatusKind::Modified + }; Some(MultiBufferDiffHunk { row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row), buffer_id: excerpt.buffer_id, excerpt_id: excerpt.id, - buffer_range: hunk.buffer_range.clone(), + buffer_range, word_diffs, diff_base_byte_range: BufferOffset(hunk.diff_base_byte_range.start) ..BufferOffset(hunk.diff_base_byte_range.end), - secondary_status: hunk.secondary_status, + status: DiffHunkStatus { + kind: status_kind, + secondary: hunk.secondary_status, + }, }) }) } @@ -4225,26 +4325,42 @@ impl MultiBufferSnapshot { cursor.seek_to_start_of_current_excerpt(); let excerpt = cursor.excerpt()?; + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer); let current_position = self .anchor_before(offset) .text_anchor .to_offset(&excerpt.buffer); - let excerpt_end = excerpt - .buffer - .anchor_before(excerpt_end.min(current_position)); if let Some(diff) = self.diffs.get(&excerpt.buffer_id) { - for hunk in diff.hunks_intersecting_range_rev( - excerpt.range.context.start..excerpt_end, - &excerpt.buffer, - ) { - let hunk_end = hunk.buffer_range.end.to_offset(&excerpt.buffer); - if hunk_end >= current_position { - continue; + if let Some(main_buffer) = diff.main_buffer.as_ref() { + for hunk in diff.hunks_intersecting_base_text_range_rev( + excerpt_start..excerpt_end, + &main_buffer, + ) { + if hunk.diff_base_byte_range.end >= current_position { + continue; + } + let hunk_start = excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start); + let start = Anchor::in_buffer(excerpt.id, hunk_start).to_point(self); + return Some(MultiBufferRow(start.row)); + } + } else { + let excerpt_end = excerpt + .buffer + .anchor_before(excerpt_end.min(current_position)); + for hunk in diff.hunks_intersecting_range_rev( + excerpt.range.context.start..excerpt_end, + &excerpt.buffer, + ) { + let hunk_end = hunk.buffer_range.end.to_offset(&excerpt.buffer); + if hunk_end >= current_position { + continue; + } + let start = + Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); + return Some(MultiBufferRow(start.row)); } - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); } } @@ -4255,13 +4371,29 @@ impl MultiBufferSnapshot { let Some(diff) = self.diffs.get(&excerpt.buffer_id) else { continue; }; - let mut hunks = - diff.hunks_intersecting_range_rev(excerpt.range.context.clone(), &excerpt.buffer); - let Some(hunk) = hunks.next() else { - continue; - }; - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); + if let Some(main_buffer) = diff.main_buffer.as_ref() { + let Some(hunk) = diff + .hunks_intersecting_base_text_range_rev( + excerpt.range.context.to_offset(&excerpt.buffer), + main_buffer, + ) + .next() + else { + continue; + }; + let hunk_start = excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start); + let start = Anchor::in_buffer(excerpt.id, hunk_start).to_point(self); + return Some(MultiBufferRow(start.row)); + } else { + let Some(hunk) = diff + .hunks_intersecting_range_rev(excerpt.range.context.clone(), &excerpt.buffer) + .next() + else { + continue; + }; + let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); + return Some(MultiBufferRow(start.row)); + } } } @@ -4831,20 +4963,19 @@ impl MultiBufferSnapshot { let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; let mut result = match first_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { + DiffTransform::BufferContent { .. } => { let excerpt_start = cursor.start().1 + start_overshoot; let excerpt_end = cursor.start().1 + end_overshoot; self.text_summary_for_excerpt_offset_range(excerpt_start..excerpt_end) } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), DiffTransform::DeletedHunk { buffer_id, + base_text_byte_range, has_trailing_newline, - hunk_info, .. } => { - let buffer_start = hunk_info.base_text_byte_range.start + start_overshoot; - let mut buffer_end = hunk_info.base_text_byte_range.start + end_overshoot; + let buffer_start = base_text_byte_range.start + start_overshoot; + let mut buffer_end = base_text_byte_range.start + end_overshoot; let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.start) }; @@ -4886,26 +5017,25 @@ impl MultiBufferSnapshot { let overshoot = range.end - cursor.start().0; let suffix = match last_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { + DiffTransform::BufferContent { .. } => { let end = cursor.start().1 + overshoot; self.text_summary_for_excerpt_offset_range::(cursor.start().1..end) } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), DiffTransform::DeletedHunk { + base_text_byte_range, buffer_id, has_trailing_newline, - hunk_info, .. } => { - let buffer_end = hunk_info.base_text_byte_range.start + overshoot; + let buffer_end = base_text_byte_range.start + overshoot; let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.end) }; let mut suffix = base_text.text_summary_for_range::( - hunk_info.base_text_byte_range.start..buffer_end, + base_text_byte_range.start..buffer_end, ); - if *has_trailing_newline && buffer_end == hunk_info.base_text_byte_range.end + 1 { + if *has_trailing_newline && buffer_end == base_text_byte_range.end + 1 { suffix.add_assign(&::from_text_summary( &TextSummary::from("\n"), )) @@ -5011,21 +5141,21 @@ impl MultiBufferSnapshot { match diff_transforms.item() { Some(DiffTransform::DeletedHunk { buffer_id, - hunk_info, + base_text_byte_range, .. }) => { if let Some(diff_base_anchor) = &anchor.diff_base_anchor && let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) - && base_text.can_resolve(diff_base_anchor) + && diff_base_anchor.is_valid(&base_text) { let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= hunk_info.base_text_byte_range.start - && base_text_offset <= hunk_info.base_text_byte_range.end + if base_text_offset >= base_text_byte_range.start + && base_text_offset <= base_text_byte_range.end { let position_in_hunk = base_text .text_summary_for_range::( - hunk_info.base_text_byte_range.start..base_text_offset, + base_text_byte_range.start..base_text_offset, ); position.0.add_text_dim(&position_in_hunk); } else if at_transform_end { @@ -5039,14 +5169,8 @@ impl MultiBufferSnapshot { diff_transforms.next(); continue; } - - if !matches!( - diff_transforms.item(), - Some(DiffTransform::FilteredInsertedHunk { .. }) - ) { - let overshoot = excerpt_position - diff_transforms.start().0; - position += overshoot; - } + let overshoot = excerpt_position - diff_transforms.start().0; + position += overshoot; } } @@ -5336,12 +5460,11 @@ impl MultiBufferSnapshot { let mut diff_base_anchor = None; if let Some(DiffTransform::DeletedHunk { buffer_id, + base_text_byte_range, has_trailing_newline, - hunk_info, .. }) = diff_transforms.item() { - let base_text_byte_range = &hunk_info.base_text_byte_range; let diff = self.diffs.get(buffer_id).expect("missing diff"); if offset_in_transform > base_text_byte_range.len() { debug_assert!(*has_trailing_newline); @@ -5461,6 +5584,13 @@ impl MultiBufferSnapshot { Some(self.excerpt(excerpt_id)?.range.context.clone()) } + pub fn excerpt_range_for_excerpt( + &self, + excerpt_id: ExcerptId, + ) -> Option> { + Some(self.excerpt(excerpt_id)?.range.clone()) + } + pub fn can_resolve(&self, anchor: &Anchor) -> bool { if anchor.is_min() || anchor.is_max() { // todo(lw): should be `!self.is_empty()` @@ -5490,7 +5620,7 @@ impl MultiBufferSnapshot { MultiBufferCursor { excerpts, diff_transforms, - snapshot: &self, + diffs: &self.diffs, cached_region: None, } } @@ -6578,7 +6708,7 @@ impl MultiBufferSnapshot { } pub fn diff_for_buffer_id(&self, buffer_id: BufferId) -> Option<&BufferDiffSnapshot> { - self.diffs.get(&buffer_id) + self.diffs.get(&buffer_id).map(|diff| &diff.diff) } /// Visually annotates a position or range with the `Debug` representation of a value. The @@ -6619,6 +6749,45 @@ impl MultiBufferSnapshot { debug_ranges.insert(key, text_ranges, format!("{value:?}").into()) }); } + + fn excerpt_edits_for_diff_change( + &self, + buffer_state: &BufferState, + diff_change_range: Range, + ) -> Vec>> { + let mut excerpt_edits = Vec::new(); + for locator in &buffer_state.excerpts { + let mut cursor = self + .excerpts + .cursor::, ExcerptOffset>>(()); + cursor.seek_forward(&Some(locator), Bias::Left); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *locator + { + let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); + if diff_change_range.end < excerpt_buffer_range.start + || diff_change_range.start > excerpt_buffer_range.end + { + continue; + } + let excerpt_start = cursor.start().1; + let excerpt_len = excerpt.text_summary.len; + let diff_change_start_in_excerpt = diff_change_range + .start + .saturating_sub(excerpt_buffer_range.start); + let diff_change_end_in_excerpt = diff_change_range + .end + .saturating_sub(excerpt_buffer_range.start); + let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); + let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); + excerpt_edits.push(Edit { + old: edit_start..edit_end, + new: edit_start..edit_end, + }); + } + } + excerpt_edits + } } #[cfg(any(test, feature = "test-support"))] @@ -6638,11 +6807,6 @@ impl MultiBufferSnapshot { let excerpts = self.excerpts.items(()); let excerpt_ids = self.excerpt_ids.items(()); - assert!( - self.excerpts.is_empty() || !self.diff_transforms.is_empty(), - "must be at least one diff transform if excerpts exist" - ); - for (ix, excerpt) in excerpts.iter().enumerate() { if ix == 0 { if excerpt.locator <= Locator::min() { @@ -6665,26 +6829,36 @@ impl MultiBufferSnapshot { if self.diff_transforms.summary().input != self.excerpts.summary().text { panic!( - "incorrect input summary. expected {:#?}, got {:#?}. transforms: {:#?}", + "incorrect input summary. expected {:?}, got {:?}. transforms: {:+?}", self.excerpts.summary().text, self.diff_transforms.summary().input, self.diff_transforms.items(()), ); } - for (left, right) in self.diff_transforms.iter().tuple_windows() { - use sum_tree::Item; - - if left.is_buffer_content() - && left.summary(()).input.len == MultiBufferOffset(0) - && !self.is_empty() + let mut prev_transform: Option<&DiffTransform> = None; + for item in self.diff_transforms.iter() { + if let DiffTransform::BufferContent { + summary, + inserted_hunk_info, + } = item { - panic!("empty buffer content transform in non-empty snapshot"); + if let Some(DiffTransform::BufferContent { + inserted_hunk_info: prev_inserted_hunk_info, + .. + }) = prev_transform + && *inserted_hunk_info == *prev_inserted_hunk_info + { + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", + self.diff_transforms.items(()) + ); + } + if summary.len == MultiBufferOffset(0) && !self.is_empty() { + panic!("empty buffer content transform"); + } } - assert!( - left.clone().merged_with(right).is_none(), - "two consecutive diff transforms could have been merged, but weren't" - ); + prev_transform = Some(item); } } } @@ -6706,9 +6880,7 @@ where } let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { let overshoot = position - self.diff_transforms.start().output_dimension; excerpt_position += overshoot; } @@ -6731,9 +6903,7 @@ where let overshoot = position - self.diff_transforms.start().output_dimension; let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { excerpt_position += overshoot; } @@ -6772,12 +6942,8 @@ where .excerpt_dimension .cmp(&self.excerpts.end()) { - cmp::Ordering::Less => { - self.diff_transforms.next(); - } - cmp::Ordering::Greater => { - self.excerpts.next(); - } + cmp::Ordering::Less => self.diff_transforms.next(), + cmp::Ordering::Greater => self.excerpts.next(), cmp::Ordering::Equal => { self.diff_transforms.next(); if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() @@ -6836,7 +7002,9 @@ where let prev_transform = self.diff_transforms.item(); self.diff_transforms.next(); - prev_transform.is_none_or(|prev_transform| prev_transform.is_buffer_content()) + prev_transform.is_none_or(|next_transform| { + matches!(next_transform, DiffTransform::BufferContent { .. }) + }) } fn is_at_end_of_excerpt(&mut self) -> bool { @@ -6850,9 +7018,7 @@ where let next_transform = self.diff_transforms.next_item(); next_transform.is_none_or(|next_transform| match next_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => true, + DiffTransform::BufferContent { .. } => true, DiffTransform::DeletedHunk { hunk_info, .. } => self .excerpts .item() @@ -6875,21 +7041,20 @@ where match self.diff_transforms.item()? { DiffTransform::DeletedHunk { buffer_id, + base_text_byte_range, has_trailing_newline, hunk_info, .. } => { - let diff = self.snapshot.diffs.get(buffer_id)?; + let diff = self.diffs.get(buffer_id)?; let buffer = diff.base_text(); let mut rope_cursor = buffer.as_rope().cursor(0); - let buffer_start = rope_cursor.summary::(hunk_info.base_text_byte_range.start); - let buffer_range_len = - rope_cursor.summary::(hunk_info.base_text_byte_range.end); + let buffer_start = rope_cursor.summary::(base_text_byte_range.start); + let buffer_range_len = rope_cursor.summary::(base_text_byte_range.end); let mut buffer_end = buffer_start; TextDimension::add_assign(&mut buffer_end, &buffer_range_len); let start = self.diff_transforms.start().output_dimension.0; let end = self.diff_transforms.end().output_dimension.0; - Some(MultiBufferRegion { buffer, excerpt, @@ -6900,20 +7065,11 @@ where )), buffer_range: buffer_start..buffer_end, range: start..end, - diff_base_byte_range: Some(hunk_info.base_text_byte_range.clone()), }) } - transform @ (DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. }) => { - let mut diff_hunk_status = transform - .hunk_info() - .map(|hunk_info| DiffHunkStatus::added(hunk_info.hunk_secondary_status)); - - let diff_base_byte_range = transform - .hunk_info() - .map(|hunk_info| hunk_info.base_text_byte_range); - + DiffTransform::BufferContent { + inserted_hunk_info, .. + } => { let buffer = &excerpt.buffer; let buffer_context_start = excerpt.range.context.start.summary::(buffer); @@ -6948,11 +7104,13 @@ where has_trailing_newline = excerpt.has_trailing_newline; }; - if matches!(transform, DiffTransform::FilteredInsertedHunk { .. }) { - buffer_end = buffer_start; - end = start; - diff_hunk_status = None; - } + let diff_hunk_status = inserted_hunk_info.map(|info| { + if info.is_logically_deleted { + DiffHunkStatus::deleted(info.hunk_secondary_status) + } else { + DiffHunkStatus::added(info.hunk_secondary_status) + } + }); Some(MultiBufferRegion { buffer, @@ -6960,7 +7118,6 @@ where has_trailing_newline, is_main_buffer: true, diff_hunk_status, - diff_base_byte_range, buffer_range: buffer_start..buffer_end, range: start..end, }) @@ -7125,11 +7282,7 @@ impl<'a> MultiBufferExcerpt<'a> { fn map_offset_to_buffer_internal(&self, offset: MultiBufferOffset) -> BufferOffset { let mut excerpt_offset = self.diff_transforms.start().excerpt_dimension; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { excerpt_offset += offset - self.diff_transforms.start().output_dimension.0; }; let offset_in_excerpt = excerpt_offset.saturating_sub(self.excerpt_offset); @@ -7272,19 +7425,10 @@ impl sum_tree::KeyedItem for ExcerptIdMapping { impl DiffTransform { fn hunk_info(&self) -> Option { match self { - DiffTransform::DeletedHunk { hunk_info, .. } - | DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. } => Some(hunk_info.clone()), - DiffTransform::Unmodified { .. } => None, - } - } - - fn is_buffer_content(&self) -> bool { - match self { - Self::Unmodified { .. } - | Self::InsertedHunk { .. } - | Self::FilteredInsertedHunk { .. } => true, - Self::DeletedHunk { .. } => false, + DiffTransform::DeletedHunk { hunk_info, .. } => Some(*hunk_info), + DiffTransform::BufferContent { + inserted_hunk_info, .. + } => *inserted_hunk_info, } } } @@ -7294,8 +7438,7 @@ impl sum_tree::Item for DiffTransform { fn summary(&self, _: ::Context<'_>) -> Self::Summary { match self { - DiffTransform::InsertedHunk { summary, .. } - | DiffTransform::Unmodified { summary, .. } => DiffTransformSummary { + DiffTransform::BufferContent { summary, .. } => DiffTransformSummary { input: *summary, output: *summary, }, @@ -7303,10 +7446,6 @@ impl sum_tree::Item for DiffTransform { input: MBTextSummary::default(), output: summary.into(), }, - DiffTransform::FilteredInsertedHunk { summary, .. } => DiffTransformSummary { - input: *summary, - output: MBTextSummary::default(), - }, } } } @@ -7595,7 +7734,6 @@ impl Iterator for MultiBufferRows<'_> { return Some(RowInfo { buffer_id: None, buffer_row: Some(0), - base_text_row: Some(BaseTextRow(0)), multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, @@ -7621,14 +7759,6 @@ impl Iterator for MultiBufferRows<'_> { .end .to_point(&last_excerpt.buffer) .row; - // TODO(split-diff) perf - let base_text_row = self - .cursor - .snapshot - .diffs - .get(&last_excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(last_row, &last_excerpt.buffer)) - .map(BaseTextRow); let first_row = last_excerpt .range @@ -7641,12 +7771,8 @@ impl Iterator for MultiBufferRows<'_> { None } else { let needs_expand_up = first_row == last_row - && (last_row > 0) - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()) - && !(region.is_filtered() - && region - .diff_base_byte_range - .is_some_and(|range| !range.is_empty())); + && last_row > 0 + && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; if needs_expand_up && needs_expand_down { @@ -7667,7 +7793,6 @@ impl Iterator for MultiBufferRows<'_> { return Some(RowInfo { buffer_id: Some(last_excerpt.buffer_id), buffer_row: Some(last_row), - base_text_row, multibuffer_row: Some(multibuffer_row), diff_status: None, wrapped_buffer_row: None, @@ -7680,31 +7805,6 @@ impl Iterator for MultiBufferRows<'_> { let overshoot = self.point - region.range.start; let buffer_point = region.buffer_range.start + overshoot; - let diff_status = region - .diff_hunk_status - .filter(|_| self.point < region.range.end); - let base_text_row = match diff_status { - // TODO(split-diff) perf - None => self - .cursor - .snapshot - .diffs - .get(®ion.excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(buffer_point.row, ®ion.buffer)) - .map(BaseTextRow), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_point.row)), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; let expand_info = if self.is_singleton { None } else { @@ -7735,9 +7835,10 @@ impl Iterator for MultiBufferRows<'_> { let result = Some(RowInfo { buffer_id: Some(region.buffer.remote_id()), buffer_row: Some(buffer_point.row), - base_text_row, multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status, + diff_status: region + .diff_hunk_status + .filter(|_| self.point < region.range.end), expand_info, wrapped_buffer_row: None, }); @@ -7754,22 +7855,14 @@ impl<'a> MultiBufferChunks<'a> { pub fn seek(&mut self, range: Range) { self.diff_transforms.seek(&range.end, Bias::Right); let mut excerpt_end = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { let overshoot = range.end - self.diff_transforms.start().0; excerpt_end += overshoot; } self.diff_transforms.seek(&range.start, Bias::Right); let mut excerpt_start = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { + if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { let overshoot = range.start - self.diff_transforms.start().0; excerpt_start += overshoot; } @@ -7832,12 +7925,6 @@ impl<'a> Iterator for ReversedMultiBufferChunks<'a> { let mut region = self.cursor.region()?; if self.offset == region.range.start { self.cursor.prev(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.prev(); - } region = self.cursor.region()?; let start_overshoot = self.start.saturating_sub(region.range.start); self.current_chunks = Some(region.buffer.reversed_chunks_in_range( @@ -7866,13 +7953,6 @@ impl<'a> Iterator for MultiBufferChunks<'a> { if self.range.start == self.diff_transforms.end().0 { self.diff_transforms.next(); } - while let Some(DiffTransform::FilteredInsertedHunk { .. }) = self.diff_transforms.item() { - self.diff_transforms.next(); - let mut range = self.excerpt_offset_range.clone(); - range.start = self.diff_transforms.start().1; - self.seek_to_excerpt_offset_range(range); - self.buffer_chunk.take(); - } let diff_transform_start = self.diff_transforms.start().0; let diff_transform_end = self.diff_transforms.end().0; @@ -7886,9 +7966,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { let diff_transform = self.diff_transforms.item()?; match diff_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => { + DiffTransform::BufferContent { .. } => { let chunk = if let Some(chunk) = &mut self.buffer_chunk { chunk } else { @@ -7924,15 +8002,15 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } DiffTransform::DeletedHunk { buffer_id, - hunk_info, + base_text_byte_range, has_trailing_newline, .. } => { - let base_text_start = hunk_info.base_text_byte_range.start - + (self.range.start - diff_transform_start); + let base_text_start = + base_text_byte_range.start + (self.range.start - diff_transform_start); let base_text_end = - hunk_info.base_text_byte_range.start + (self.range.end - diff_transform_start); - let base_text_end = base_text_end.min(hunk_info.base_text_byte_range.end); + base_text_byte_range.start + (self.range.end - diff_transform_start); + let base_text_end = base_text_end.min(base_text_byte_range.end); let mut chunks = if let Some((_, mut chunks)) = self .diff_base_chunks @@ -7981,12 +8059,6 @@ impl MultiBufferBytes<'_> { self.chunk = b"\n"; } else { self.cursor.next(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.next(); - } if let Some(region) = self.cursor.region() { let mut excerpt_bytes = region.buffer.bytes_in_range( region.buffer_range.start diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index fb6dce079268e3dfed868a0c65c81bd12e226704..eb5de25a0d526068cf46467601506f560af8f7b8 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -29,7 +29,6 @@ fn test_empty_singleton(cx: &mut App) { [RowInfo { buffer_id: Some(buffer_id), buffer_row: Some(0), - base_text_row: None, multibuffer_row: Some(MultiBufferRow(0)), diff_status: None, expand_info: None, @@ -355,7 +354,8 @@ async fn test_diff_boundary_anchors(cx: &mut TestAppContext) { let base_text = "one\ntwo\nthree\n"; let text = "one\nthree\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx)); @@ -397,7 +397,8 @@ async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; let text = "one\nfour\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { (multibuffer.snapshot(cx), multibuffer.subscribe()) @@ -472,12 +473,70 @@ async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_inverted_diff_hunks_in_range(cx: &mut TestAppContext) { + let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; + let text = "ZERO\none\nTHREE\nfour\nseven\nEIGHT\nNINE\n"; + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); + let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); + let multibuffer = cx.new(|cx| MultiBuffer::singleton(base_text_buffer.clone(), cx)); + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_inverted_diff(diff, buffer.clone(), cx); + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! { + " one + - two + - three + four + - five + - six + seven + - eight + " + }, + ); + + assert_eq!( + snapshot + .diff_hunks_in_range(Point::new(0, 0)..Point::MAX) + .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) + .collect::>(), + vec![0..0, 1..3, 4..6, 7..8] + ); + + assert_eq!( + snapshot.diff_hunk_before(Point::new(1, 1)), + Some(MultiBufferRow(0)) + ); + assert_eq!( + snapshot.diff_hunk_before(Point::new(7, 0)), + Some(MultiBufferRow(4)) + ); + assert_eq!( + snapshot.diff_hunk_before(Point::new(4, 0)), + Some(MultiBufferRow(1)) + ); +} + #[gpui::test] async fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { @@ -910,7 +969,8 @@ async fn test_empty_diff_excerpt(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("", cx)); let base_text = "a\nb\nc"; - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts(buffer.clone(), [ExcerptRange::new(0..0)], cx); multibuffer.set_all_diff_hunks_expanded(cx); @@ -936,7 +996,7 @@ async fn test_empty_diff_excerpt(cx: &mut TestAppContext) { buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, "a\nb\nc")], None, cx); diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); + diff.recalculate_diff_sync(&buffer.text_snapshot(), cx); }); assert_eq!(buffer.text(), "a\nb\nc") }); @@ -948,7 +1008,7 @@ async fn test_empty_diff_excerpt(cx: &mut TestAppContext) { buffer.update(cx, |buffer, cx| { buffer.undo(cx); diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); + diff.recalculate_diff_sync(&buffer.text_snapshot(), cx); }); assert_eq!(buffer.text(), "") }); @@ -1257,7 +1317,8 @@ async fn test_basic_diff_hunks(cx: &mut TestAppContext) { ); let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -1448,7 +1509,7 @@ async fn test_basic_diff_hunks(cx: &mut TestAppContext) { // Recalculate the diff, changing the first diff hunk. diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx); + diff.recalculate_diff_sync(&buffer.read(cx).text_snapshot(), cx); }); cx.run_until_parked(); assert_new_snapshot( @@ -1501,7 +1562,8 @@ async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { ); let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -2036,8 +2098,12 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); + let diff_1 = cx.new(|cx| { + BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx) + }); + let diff_2 = cx.new(|cx| { + BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx) + }); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -2113,8 +2179,8 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); - let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().remote_id()); - let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().remote_id()); + let base_id_1 = diff_1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id()); + let base_id_2 = diff_2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id()); let buffer_lines = (0..=snapshot.max_row().0) .map(|row| { @@ -2229,6 +2295,7 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { struct ReferenceMultibuffer { excerpts: Vec, diffs: HashMap>, + inverted_diffs: HashMap, WeakEntity)>, } #[derive(Debug)] @@ -2275,7 +2342,7 @@ impl ReferenceMultibuffer { .unwrap(); let excerpt = self.excerpts.remove(ix); let buffer = excerpt.buffer.read(cx); - let id = buffer.remote_id(); + let buffer_id = buffer.remote_id(); log::info!( "Removing excerpt {}: {:?}", ix, @@ -2286,9 +2353,10 @@ impl ReferenceMultibuffer { if !self .excerpts .iter() - .any(|excerpt| excerpt.buffer.read(cx).remote_id() == id) + .any(|excerpt| excerpt.buffer.read(cx).remote_id() == buffer_id) { - self.diffs.remove(&id); + self.diffs.remove(&buffer_id); + self.inverted_diffs.remove(&buffer_id); } } @@ -2326,11 +2394,21 @@ impl ReferenceMultibuffer { .unwrap(); let buffer = excerpt.buffer.read(cx).snapshot(); let buffer_id = buffer.remote_id(); + + // Skip inverted excerpts - hunks are always expanded + if self.inverted_diffs.contains_key(&buffer_id) { + return; + } + let Some(diff) = self.diffs.get(&buffer_id) else { return; }; let excerpt_range = excerpt.range.to_offset(&buffer); - for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) { + for hunk in diff + .read(cx) + .snapshot(cx) + .hunks_intersecting_range(range, &buffer) + { let hunk_range = hunk.buffer_range.to_offset(&buffer); if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end { continue; @@ -2354,128 +2432,183 @@ impl ReferenceMultibuffer { } } - fn expected_content( - &self, - filter_mode: Option, - all_diff_hunks_expanded: bool, - cx: &App, - ) -> (String, Vec, HashSet) { + fn expected_content(&self, cx: &App) -> (String, Vec, HashSet) { let mut text = String::new(); let mut regions = Vec::::new(); - let mut filtered_regions = Vec::::new(); let mut excerpt_boundary_rows = HashSet::default(); for excerpt in &self.excerpts { excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); let buffer = excerpt.buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_range = excerpt.range.to_offset(buffer); - let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx); - let base_buffer = diff.base_text(); - - let mut offset = buffer_range.start; - let hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) - .peekable(); - - for hunk in hunks { - // Ignore hunks that are outside the excerpt range. - let mut hunk_range = hunk.buffer_range.to_offset(buffer); - - hunk_range.end = hunk_range.end.min(buffer_range.end); - if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start { - log::trace!("skipping hunk outside excerpt range"); - continue; - } - if !all_diff_hunks_expanded - && !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - expanded_anchor.to_offset(buffer).max(buffer_range.start) - == hunk_range.start.max(buffer_range.start) - }) + if let Some((diff, main_buffer)) = self.inverted_diffs.get(&buffer_id) { + let diff_snapshot = diff.read(cx).snapshot(cx); + let main_buffer_snapshot = main_buffer + .read_with(cx, |main_buffer, _| main_buffer.snapshot()) + .unwrap(); + + let mut offset = buffer_range.start; + for hunk in diff_snapshot + .hunks_intersecting_base_text_range(buffer_range.clone(), &main_buffer_snapshot) { - log::trace!("skipping a hunk that's not marked as expanded"); - continue; - } + let mut hunk_base_range = hunk.diff_base_byte_range.clone(); - if !hunk.buffer_range.start.is_valid(buffer) { - log::trace!("skipping hunk with deleted start: {:?}", hunk.range); - continue; - } + hunk_base_range.end = hunk_base_range.end.min(buffer_range.end); + if hunk_base_range.start > buffer_range.end + || hunk_base_range.start < buffer_range.start + { + continue; + } - if hunk_range.start >= offset { - // Add the buffer text before the hunk - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.start)); - if text.len() > len { - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); + if !hunk.buffer_range.start.is_valid(&main_buffer_snapshot) { + continue; } - // Add the deleted text for the hunk. - if !hunk.diff_base_byte_range.is_empty() - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let mut base_text = base_buffer - .text_for_range(hunk.diff_base_byte_range.clone()) - .collect::(); - if !base_text.ends_with('\n') { - base_text.push('\n'); + // Add the text before the hunk + if hunk_base_range.start >= offset { + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_base_range.start)); + if text.len() > len { + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some( + (offset..hunk_base_range.start).to_point(&buffer), + ), + status: None, + excerpt_id: Some(excerpt.id), + }); } + } + + // Add the "deleted" region (base text that's not in main) + if !hunk_base_range.is_empty() { let len = text.len(); - text.push_str(&base_text); + text.extend(buffer.text_for_range(hunk_base_range.clone())); regions.push(ReferenceRegion { - buffer_id: Some(base_buffer.remote_id()), + buffer_id: Some(buffer_id), range: len..text.len(), - buffer_range: Some(hunk.diff_base_byte_range.to_point(&base_buffer)), + buffer_range: Some(hunk_base_range.to_point(&buffer)), status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), excerpt_id: Some(excerpt.id), }); } - offset = hunk_range.start; + offset = hunk_base_range.end; } - // Add the inserted text for the hunk. - if hunk_range.end > offset { - let is_filtered = filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let range = if is_filtered { - text.len()..text.len() - } else { + // Add remaining buffer text + let len = text.len(); + text.extend(buffer.text_for_range(offset..buffer_range.end)); + text.push('\n'); + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } else { + let diff = self.diffs.get(&buffer_id).unwrap().read(cx).snapshot(cx); + let base_buffer = diff.base_text(); + + let mut offset = buffer_range.start; + let hunks = diff + .hunks_intersecting_range(excerpt.range.clone(), buffer) + .peekable(); + + for hunk in hunks { + // Ignore hunks that are outside the excerpt range. + let mut hunk_range = hunk.buffer_range.to_offset(buffer); + + hunk_range.end = hunk_range.end.min(buffer_range.end); + if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start + { + log::trace!("skipping hunk outside excerpt range"); + continue; + } + + if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { + expanded_anchor.to_offset(buffer).max(buffer_range.start) + == hunk_range.start.max(buffer_range.start) + }) { + log::trace!("skipping a hunk that's not marked as expanded"); + continue; + } + + if !hunk.buffer_range.start.is_valid(buffer) { + log::trace!("skipping hunk with deleted start: {:?}", hunk.range); + continue; + } + + if hunk_range.start >= offset { + // Add the buffer text before the hunk + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_range.start)); + if text.len() > len { + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } + + // Add the deleted text for the hunk. + if !hunk.diff_base_byte_range.is_empty() { + let mut base_text = base_buffer + .text_for_range(hunk.diff_base_byte_range.clone()) + .collect::(); + if !base_text.ends_with('\n') { + base_text.push('\n'); + } + let len = text.len(); + text.push_str(&base_text); + regions.push(ReferenceRegion { + buffer_id: Some(base_buffer.remote_id()), + range: len..text.len(), + buffer_range: Some( + hunk.diff_base_byte_range.to_point(&base_buffer), + ), + status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), + excerpt_id: Some(excerpt.id), + }); + } + + offset = hunk_range.start; + } + + // Add the inserted text for the hunk. + if hunk_range.end > offset { let len = text.len(); text.extend(buffer.text_for_range(offset..hunk_range.end)); - len..text.len() - }; - let region = ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range, - buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), - status: Some(DiffHunkStatus::added(hunk.secondary_status)), - excerpt_id: Some(excerpt.id), - }; - offset = hunk_range.end; - if is_filtered { - filtered_regions.push(region); - } else { + let range = len..text.len(); + let region = ReferenceRegion { + buffer_id: Some(buffer_id), + range, + buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), + status: Some(DiffHunkStatus::added(hunk.secondary_status)), + excerpt_id: Some(excerpt.id), + }; + offset = hunk_range.end; regions.push(region); } } - } - // Add the buffer text for the rest of the excerpt. - let len = text.len(); - text.extend(buffer.text_for_range(offset..buffer_range.end)); - text.push('\n'); - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); + // Add the buffer text for the rest of the excerpt. + let len = text.len(); + text.extend(buffer.text_for_range(offset..buffer_range.end)); + text.push('\n'); + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } } // Remove final trailing newline. @@ -2511,38 +2644,6 @@ impl ReferenceMultibuffer { .iter() .find(|e| e.id == region.excerpt_id.unwrap()) .map(|e| e.buffer.clone()); - let base_text_row = match region.status { - None => Some( - main_buffer - .as_ref() - .map(|main_buffer| { - let diff = self - .diffs - .get(&main_buffer.read(cx).remote_id()) - .unwrap(); - let buffer_row = buffer_row.unwrap(); - BaseTextRow( - diff.read(cx).snapshot(cx).row_to_base_text_row( - buffer_row, - &main_buffer.read(cx).snapshot(), - ), - ) - }) - .unwrap_or_default(), - ), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_row.unwrap())), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; let is_excerpt_start = region_ix == 0 || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id || regions[region_ix - 1].range.is_empty(); @@ -2559,7 +2660,8 @@ impl ReferenceMultibuffer { }; if region_ix < regions.len() - 1 && !text[ix..].contains("\n") - && region.status == Some(DiffHunkStatus::added_none()) + && (region.status == Some(DiffHunkStatus::added_none()) + || region.status.is_some_and(|s| s.is_deleted())) && regions[region_ix + 1].excerpt_id == region.excerpt_id && regions[region_ix + 1].range.start == text.len() { @@ -2589,7 +2691,6 @@ impl ReferenceMultibuffer { buffer_id: region.buffer_id, diff_status: region.status, buffer_row, - base_text_row, wrapped_buffer_row: None, multibuffer_row: Some(multibuffer_row), @@ -2612,10 +2713,19 @@ impl ReferenceMultibuffer { fn diffs_updated(&mut self, cx: &App) { for excerpt in &mut self.excerpts { let buffer = excerpt.buffer.read(cx).snapshot(); - let excerpt_range = excerpt.range.to_offset(&buffer); let buffer_id = buffer.remote_id(); - let diff = self.diffs.get(&buffer_id).unwrap().read(cx); - let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable(); + + // Skip inverted diff excerpts - hunks are always expanded + if self.inverted_diffs.contains_key(&buffer_id) { + continue; + } + + let excerpt_range = excerpt.range.to_offset(&buffer); + let Some(diff) = self.diffs.get(&buffer_id) else { + continue; + }; + let diff = diff.read(cx).snapshot(cx); + let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable(); excerpt.expanded_diff_hunks.retain(|hunk_anchor| { if !hunk_anchor.is_valid(&buffer) { return false; @@ -2642,6 +2752,17 @@ impl ReferenceMultibuffer { let buffer_id = diff.read(cx).buffer_id; self.diffs.insert(buffer_id, diff); } + + fn add_inverted_diff( + &mut self, + diff: Entity, + main_buffer: Entity, + cx: &App, + ) { + let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); + self.inverted_diffs + .insert(base_text_buffer_id, (diff, main_buffer.downgrade())); + } } #[gpui::test(iterations = 100)] @@ -2719,50 +2840,19 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { } } -// TODO(split-diff) bump up iterations -// #[gpui::test(iterations = 100)] -#[gpui::test] -async fn test_random_filtered_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - multibuffer - }); - let follower = multibuffer.update(cx, |multibuffer, cx| multibuffer.get_or_create_follower(cx)); - follower.update(cx, |follower, _| { - assert!(follower.all_diff_hunks_expanded()); - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - }); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - #[gpui::test(iterations = 100)] -async fn test_random_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - -async fn test_random_multibuffer_impl( - multibuffer: Entity, - cx: &mut TestAppContext, - mut rng: StdRng, -) { +async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - - multibuffer.read_with(cx, |multibuffer, _| assert!(multibuffer.is_empty())); - let all_diff_hunks_expanded = - multibuffer.read_with(cx, |multibuffer, _| multibuffer.all_diff_hunks_expanded()); + let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let mut buffers: Vec> = Vec::new(); let mut base_texts: HashMap = HashMap::default(); let mut reference = ReferenceMultibuffer::default(); let mut anchors = Vec::new(); let mut old_versions = Vec::new(); - let mut old_follower_versions = Vec::new(); let mut needs_diff_calculation = false; - + let mut inverted_diff_main_buffers: HashMap> = HashMap::default(); for _ in 0..operations { match rng.random_range(0..100) { 0..=14 if !buffers.is_empty() => { @@ -2859,11 +2949,18 @@ async fn test_random_multibuffer_impl( assert!(excerpt.contains(anchor)); } } - 45..=55 if !reference.excerpts.is_empty() && !all_diff_hunks_expanded => { + 45..=55 if !reference.excerpts.is_empty() => { multibuffer.update(cx, |multibuffer, cx| { let snapshot = multibuffer.snapshot(cx); let excerpt_ix = rng.random_range(0..reference.excerpts.len()); let excerpt = &reference.excerpts[excerpt_ix]; + + // Skip inverted excerpts - hunks can't be collapsed + let buffer_id = excerpt.buffer.read(cx).remote_id(); + if reference.inverted_diffs.contains_key(&buffer_id) { + return; + } + let start = excerpt.range.start; let end = excerpt.range.end; let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() @@ -2873,7 +2970,7 @@ async fn test_random_multibuffer_impl( "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})", range.to_offset(&snapshot), excerpt.id, - excerpt.buffer.read(cx).remote_id(), + buffer_id, ); reference.expand_diff_hunks(excerpt.id, start..end, cx); multibuffer.expand_diff_hunks(vec![range], cx); @@ -2883,38 +2980,32 @@ async fn test_random_multibuffer_impl( multibuffer.update(cx, |multibuffer, cx| { for buffer in multibuffer.all_buffers() { let snapshot = buffer.read(cx).snapshot(); - multibuffer.diff_for(snapshot.remote_id()).unwrap().update( - cx, - |diff, cx| { + let buffer_id = snapshot.remote_id(); + + if let Some(diff) = multibuffer.diff_for(buffer_id) { + diff.update(cx, |diff, cx| { + log::info!("recalculating diff for buffer {:?}", buffer_id,); + diff.recalculate_diff_sync(&snapshot.text, cx); + }); + } + + if let Some(inverted_diff) = inverted_diff_main_buffers.get(&buffer_id) { + inverted_diff.update(cx, |diff, cx| { log::info!( - "recalculating diff for buffer {:?}", - snapshot.remote_id(), + "recalculating inverted diff for main buffer {:?}", + buffer_id, ); - diff.recalculate_diff_sync(snapshot.text, cx); - }, - ); + diff.recalculate_diff_sync(&snapshot.text, cx); + }); + } } reference.diffs_updated(cx); needs_diff_calculation = false; }); } _ => { - let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) { - let mut base_text = util::RandomCharIter::new(&mut rng) - .take(256) - .collect::(); - - let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); - text::LineEnding::normalize(&mut base_text); - base_texts.insert( - buffer.read_with(cx, |buffer, _| buffer.remote_id()), - base_text, - ); - buffers.push(buffer); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; + // Decide if we're creating a new buffer or reusing an existing one + let create_new_buffer = buffers.is_empty() || rng.random_bool(0.4); let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); let prev_excerpt_id = reference @@ -2923,7 +3014,84 @@ async fn test_random_multibuffer_impl( .map_or(ExcerptId::max(), |e| e.id); let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); - let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { + let (excerpt_buffer, diff, inverted_main_buffer) = if create_new_buffer { + let create_inverted = rng.random_bool(0.3); + + if create_inverted { + let mut main_buffer_text = util::RandomCharIter::new(&mut rng) + .take(256) + .collect::(); + let main_buffer = cx.new(|cx| Buffer::local(main_buffer_text.clone(), cx)); + text::LineEnding::normalize(&mut main_buffer_text); + let main_buffer_id = + main_buffer.read_with(cx, |buffer, _| buffer.remote_id()); + base_texts.insert(main_buffer_id, main_buffer_text.clone()); + buffers.push(main_buffer.clone()); + + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text( + &main_buffer_text, + &main_buffer.read(cx).text_snapshot(), + cx, + ) + }); + + let base_text_buffer = + diff.read_with(cx, |diff, _| diff.base_text_buffer()); + + // Track for recalculation when main buffer is edited + inverted_diff_main_buffers.insert(main_buffer_id, diff.clone()); + + (base_text_buffer, diff, Some(main_buffer)) + } else { + let mut base_text = util::RandomCharIter::new(&mut rng) + .take(256) + .collect::(); + + let buffer_handle = cx.new(|cx| Buffer::local(base_text.clone(), cx)); + text::LineEnding::normalize(&mut base_text); + let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id()); + base_texts.insert(buffer_id, base_text.clone()); + buffers.push(buffer_handle.clone()); + + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text( + &base_text, + &buffer_handle.read(cx).text_snapshot(), + cx, + ) + }); + + (buffer_handle, diff, None) + } + } else { + // Reuse an existing buffer + let buffer_handle = buffers.choose(&mut rng).unwrap().clone(); + let buffer_id = buffer_handle.read_with(cx, |buffer, _| buffer.remote_id()); + + if let Some(diff) = inverted_diff_main_buffers.get(&buffer_id) { + let base_text_buffer = + diff.read_with(cx, |diff, _| diff.base_text_buffer()); + (base_text_buffer, diff.clone(), Some(buffer_handle)) + } else { + // Get existing diff or create new one for regular buffer + let diff = multibuffer + .read_with(cx, |mb, _| mb.diff_for(buffer_id)) + .unwrap_or_else(|| { + let base_text = base_texts.get(&buffer_id).unwrap(); + cx.new(|cx| { + BufferDiff::new_with_base_text( + base_text, + &buffer_handle.read(cx).text_snapshot(), + cx, + ) + }) + }); + (buffer_handle, diff, None) + } + }; + + let (range, anchor_range) = excerpt_buffer.read_with(cx, |buffer, _| { let end_row = rng.random_range(0..=buffer.max_point().row); let start_row = rng.random_range(0..=end_row); let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); @@ -2947,7 +3115,7 @@ async fn test_random_multibuffer_impl( multibuffer .insert_excerpts_after( prev_excerpt_id, - buffer_handle.clone(), + excerpt_buffer.clone(), [ExcerptRange::new(range.clone())], cx, ) @@ -2958,17 +3126,20 @@ async fn test_random_multibuffer_impl( reference.insert_excerpt_after( prev_excerpt_id, excerpt_id, - (buffer_handle.clone(), anchor_range), + (excerpt_buffer.clone(), anchor_range), ); + let excerpt_buffer_id = + excerpt_buffer.read_with(cx, |buffer, _| buffer.remote_id()); multibuffer.update(cx, |multibuffer, cx| { - let id = buffer_handle.read(cx).remote_id(); - if multibuffer.diff_for(id).is_none() { - let base_text = base_texts.get(&id).unwrap(); - let diff = cx - .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); - reference.add_diff(diff.clone(), cx); - multibuffer.add_diff(diff, cx) + if multibuffer.diff_for(excerpt_buffer_id).is_none() { + if let Some(main_buffer) = inverted_main_buffer { + reference.add_inverted_diff(diff.clone(), main_buffer.clone(), cx); + multibuffer.add_inverted_diff(diff, main_buffer, cx); + } else { + reference.add_diff(diff.clone(), cx); + multibuffer.add_diff(diff, cx); + } } }); } @@ -2977,35 +3148,17 @@ async fn test_random_multibuffer_impl( if rng.random_bool(0.3) { multibuffer.update(cx, |multibuffer, cx| { old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); - - if let Some(follower) = &multibuffer.follower { - follower.update(cx, |follower, cx| { - old_follower_versions.push((follower.snapshot(cx), follower.subscribe())); - }) - } }) } multibuffer.read_with(cx, |multibuffer, cx| { check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng); - - if let Some(follower) = &multibuffer.follower { - check_multibuffer(follower.read(cx), &reference, &anchors, cx, &mut rng); - } }); } - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); for (old_snapshot, subscription) in old_versions { check_multibuffer_edits(&snapshot, &old_snapshot, subscription); } - if let Some(follower) = multibuffer.read_with(cx, |multibuffer, _| multibuffer.follower.clone()) - { - let snapshot = follower.read_with(cx, |follower, cx| follower.snapshot(cx)); - for (old_snapshot, subscription) in old_follower_versions { - check_multibuffer_edits(&snapshot, &old_snapshot, subscription); - } - } } fn check_multibuffer( @@ -3016,8 +3169,6 @@ fn check_multibuffer( rng: &mut StdRng, ) { let snapshot = multibuffer.snapshot(cx); - let filter_mode = multibuffer.filter_mode; - assert!(filter_mode.is_some() == snapshot.all_diff_hunks_expanded); let actual_text = snapshot.text(); let actual_boundary_rows = snapshot .excerpt_boundaries_in_range(MultiBufferOffset(0)..) @@ -3026,15 +3177,12 @@ fn check_multibuffer( let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); let (expected_text, expected_row_infos, expected_boundary_rows) = - reference.expected_content(filter_mode, snapshot.all_diff_hunks_expanded, cx); - - let (unfiltered_text, unfiltered_row_infos, unfiltered_boundary_rows) = - reference.expected_content(None, snapshot.all_diff_hunks_expanded, cx); + reference.expected_content(cx); let has_diff = actual_row_infos .iter() .any(|info| info.diff_status.is_some()) - || unfiltered_row_infos + || expected_row_infos .iter() .any(|info| info.diff_status.is_some()); let actual_diff = format_diff( @@ -3051,17 +3199,6 @@ fn check_multibuffer( ); log::info!("Multibuffer content:\n{}", actual_diff); - if filter_mode.is_some() { - log::info!( - "Unfiltered multibuffer content:\n{}", - format_diff( - &unfiltered_text, - &unfiltered_row_infos, - &unfiltered_boundary_rows, - None, - ), - ); - } assert_eq!( actual_row_infos.len(), @@ -3090,7 +3227,12 @@ fn check_multibuffer( expected_row_infos .into_iter() .filter_map(|info| { - if info.diff_status.is_some_and(|status| status.is_deleted()) { + // For inverted diffs, deleted rows are visible and should be counted. + // Only filter out deleted rows that are NOT from inverted diffs. + let is_inverted_diff = info + .buffer_id + .is_some_and(|id| reference.inverted_diffs.contains_key(&id)); + if info.diff_status.is_some_and(|status| status.is_deleted()) && !is_inverted_diff { None } else { info.buffer_row @@ -3501,8 +3643,12 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) { let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); + let diff_1 = cx.new(|cx| { + BufferDiff::new_with_base_text(base_text_1, &buffer_1.read(cx).text_snapshot(), cx) + }); + let diff_2 = cx.new(|cx| { + BufferDiff::new_with_base_text(base_text_2, &buffer_2.read(cx).text_snapshot(), cx) + }); cx.run_until_parked(); let mut ids = vec![]; @@ -3559,7 +3705,9 @@ async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) { let text_1 = "one\n".to_owned(); let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx)); + let diff_1 = cx.new(|cx| { + BufferDiff::new_with_base_text(&base_text_1, &buffer_1.read(cx).text_snapshot(), cx) + }); cx.run_until_parked(); let multibuffer = cx.new(|cx| { @@ -3730,7 +3878,63 @@ fn format_diff( // } #[gpui::test] -async fn test_basic_filtering(cx: &mut TestAppContext) { +async fn test_inverted_diff_hunk_invalidation_on_main_buffer_edit(cx: &mut TestAppContext) { + let text = "one\ntwo\nthree\n"; + let base_text = "one\nTWO\nthree\n"; + + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); + cx.run_until_parked(); + + let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx); + multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + one + - TWO + three + " + ), + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(3..5, "")], None, cx); + }); + cx.run_until_parked(); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + one + TWO + three + " + ), + ); +} + +#[gpui::test] +async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { let text = indoc!( " ZERO @@ -3752,14 +3956,16 @@ async fn test_basic_filtering(cx: &mut TestAppContext) { ); let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); cx.run_until_parked(); + let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); + let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); + let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx); multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); + multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx); multibuffer }); @@ -3800,6 +4006,23 @@ async fn test_basic_filtering(cx: &mut TestAppContext) { cx, ); }); + cx.run_until_parked(); + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( + buffer.read(cx).text_snapshot(), + Some(base_text.into()), + false, + None, + cx, + ) + }) + .await; + diff.update(cx, |diff, cx| { + diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx); + }); + cx.run_until_parked(); + assert_new_snapshot( &multibuffer, &mut snapshot, @@ -3809,76 +4032,72 @@ async fn test_basic_filtering(cx: &mut TestAppContext) { " one - two + - three - four - five six " }, ); -} -#[gpui::test] -async fn test_base_text_line_numbers(cx: &mut TestAppContext) { - let base_text = indoc! {" - one - two - three - four - five - six - "}; - let buffer_text = indoc! {" - two - THREE - five - six - SEVEN - "}; - let multibuffer = cx.update(|cx| MultiBuffer::build_simple(buffer_text, cx)); - multibuffer.update(cx, |multibuffer, cx| { - let buffer = multibuffer.all_buffers().into_iter().next().unwrap(); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff, cx); + buffer.update(cx, |buffer, cx| { + buffer.set_text("ZERO\nONE\nTWO\n", cx); }); - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) + cx.run_until_parked(); + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( + buffer.read(cx).text_snapshot(), + Some(base_text.into()), + false, + None, + cx, + ) + }) + .await; + diff.update(cx, |diff, cx| { + diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx); }); + cx.run_until_parked(); assert_new_snapshot( &multibuffer, &mut snapshot, &mut subscription, cx, - indoc! {" + indoc! { + " - one - two + - two - three - four - + THREE - five - six - + SEVEN - "}, + - five + - six + " + }, ); - let base_text_rows = snapshot - .row_infos(MultiBufferRow(0)) - .map(|row_info| row_info.base_text_row) - .collect::>(); - pretty_assertions::assert_eq!( - base_text_rows, - vec![ - Some(BaseTextRow(0)), - Some(BaseTextRow(1)), - Some(BaseTextRow(2)), - Some(BaseTextRow(3)), - None, - Some(BaseTextRow(4)), - Some(BaseTextRow(5)), + + diff.update(cx, |diff, cx| { + diff.set_base_text( + Some("new base\n".into()), None, - Some(BaseTextRow(6)), - ] - ) + buffer.read(cx).text_snapshot(), + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc! {" + - new base + "}, + ); } #[track_caller] @@ -4333,8 +4552,13 @@ fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) { } } - let diff = - cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_handle, cx)); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text( + &base_text, + &buffer_handle.read(cx).text_snapshot(), + cx, + ) + }); diffs.push(diff.clone()); multibuffer.add_diff(diff, cx); } @@ -4444,7 +4668,8 @@ fn collect_word_diffs( cx: &mut TestAppContext, ) -> Vec { let buffer = cx.new(|cx| Buffer::local(modified_text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); cx.run_until_parked(); let multibuffer = cx.new(|cx| { diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index 10d4088fd4bc28449c8a4ee74095ad31a45fbcf3..8c20f211f61990b3775d231dc37a80352d7b9b98 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -47,15 +47,23 @@ impl MultiBuffer { self.excerpts_by_path.keys() } + pub fn excerpts_for_path(&self, path: &PathKey) -> impl '_ + Iterator { + self.excerpts_by_path + .get(path) + .map(|excerpts| excerpts.as_slice()) + .unwrap_or(&[]) + .iter() + .copied() + } + + pub fn path_for_excerpt(&self, excerpt: ExcerptId) -> Option { + self.paths_by_excerpt.get(&excerpt).cloned() + } + pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { if let Some(to_remove) = self.excerpts_by_path.remove(&path) { self.remove_excerpts(to_remove, cx) } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts_for_path(path, cx); - }); - } } pub fn buffer_for_path(&self, path: &PathKey, cx: &App) -> Option> { @@ -278,7 +286,7 @@ impl MultiBuffer { (result, added_a_new_excerpt) } - fn update_path_excerpts( + pub fn update_path_excerpts( &mut self, path: PathKey, buffer: Entity, diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 9ae4f36a497b63ae63ea09440f76fb8823380f5d..fbe1f32bea065b8a97073ff8ce7e8cdd4189b3cd 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -129,8 +129,8 @@ struct BufferGitState { hunk_staging_operation_count: usize, hunk_staging_operation_count_as_of_write: usize, - head_text: Option>, - index_text: Option>, + head_text: Option>, + index_text: Option>, head_changed: bool, index_changed: bool, language_changed: bool, @@ -693,7 +693,6 @@ impl GitStore { oid: Option, buffer: Entity, repo: Entity, - languages: Arc, cx: &mut Context, ) -> Task>> { cx.spawn(async move |this, cx| { @@ -710,9 +709,8 @@ impl GitStore { buffer_diff .update(cx, |buffer_diff, cx| { buffer_diff.set_base_text( - content.map(Arc::new), + content.map(|s| s.as_str().into()), buffer_snapshot.language().cloned(), - Some(languages.clone()), buffer_snapshot.text, cx, ) @@ -823,6 +821,7 @@ impl GitStore { cx.subscribe(&diff, Self::on_buffer_diff_event).detach(); diff_state.update(cx, |diff_state, cx| { + diff_state.language_changed = true; diff_state.language = language; diff_state.language_registry = language_registry; @@ -2652,7 +2651,7 @@ impl GitStore { .or_default(); shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone()); })?; - let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?; + let staged_text = diff.read_with(&cx, |diff, cx| diff.base_text_string(cx))?; Ok(proto::OpenUnstagedDiffResponse { staged_text }) } @@ -2682,14 +2681,14 @@ impl GitStore { let unstaged_diff = diff.secondary_diff(); let index_snapshot = unstaged_diff.and_then(|diff| { let diff = diff.read(cx); - diff.base_text_exists().then(|| diff.base_text()) + diff.base_text_exists().then(|| diff.base_text(cx)) }); let mode; let staged_text; let committed_text; if diff.base_text_exists() { - let committed_snapshot = diff.base_text(); + let committed_snapshot = diff.base_text(cx); committed_text = Some(committed_snapshot.text()); if let Some(index_text) = index_snapshot { if index_text.remote_id() == committed_snapshot.remote_id() { @@ -3025,21 +3024,21 @@ impl BufferGitState { Some(DiffBasesChange::SetIndex(index)) => { self.index_text = index.map(|mut index| { text::LineEnding::normalize(&mut index); - Arc::new(index) + Arc::from(index.as_str()) }); self.index_changed = true; } Some(DiffBasesChange::SetHead(head)) => { self.head_text = head.map(|mut head| { text::LineEnding::normalize(&mut head); - Arc::new(head) + Arc::from(head.as_str()) }); self.head_changed = true; } Some(DiffBasesChange::SetBoth(text)) => { let text = text.map(|mut text| { text::LineEnding::normalize(&mut text); - Arc::new(text) + Arc::from(text.as_str()) }); self.head_text = text.clone(); self.index_text = text; @@ -3049,12 +3048,12 @@ impl BufferGitState { Some(DiffBasesChange::SetEach { index, head }) => { self.index_text = index.map(|mut index| { text::LineEnding::normalize(&mut index); - Arc::new(index) + Arc::from(index.as_str()) }); self.index_changed = true; self.head_text = head.map(|mut head| { text::LineEnding::normalize(&mut head); - Arc::new(head) + Arc::from(head.as_str()) }); self.head_changed = true; } @@ -3091,17 +3090,16 @@ impl BufferGitState { let mut new_unstaged_diff = None; if let Some(unstaged_diff) = &unstaged_diff { new_unstaged_diff = Some( - BufferDiff::update_diff( - unstaged_diff.clone(), - buffer.clone(), - index, - index_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, + cx.update(|cx| { + unstaged_diff.read(cx).update_diff( + buffer.clone(), + index, + index_changed, + language.clone(), + cx, + ) + })? + .await, ); } @@ -3115,17 +3113,16 @@ impl BufferGitState { new_unstaged_diff.clone() } else { Some( - BufferDiff::update_diff( - uncommitted_diff.clone(), - buffer.clone(), - head, - head_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, + cx.update(|cx| { + uncommitted_diff.read(cx).update_diff( + buffer.clone(), + head, + head_changed, + language.clone(), + cx, + ) + })? + .await, ) } } @@ -3164,7 +3161,7 @@ impl BufferGitState { { unstaged_diff.update(cx, |diff, cx| { if language_changed { - diff.language_changed(cx); + diff.language_changed(language.clone(), language_registry.clone(), cx); } diff.set_snapshot(new_unstaged_diff, &buffer, cx) })? @@ -3179,7 +3176,7 @@ impl BufferGitState { { uncommitted_diff.update(cx, |diff, cx| { if language_changed { - diff.language_changed(cx); + diff.language_changed(language, language_registry, cx); } diff.set_snapshot_with_secondary( new_uncommitted_diff, @@ -3689,9 +3686,9 @@ impl Repository { match (current_index_text.as_ref(), current_head_text.as_ref()) { (Some(current_index), Some(current_head)) => { let index_changed = - index_text.as_ref() != current_index.as_deref(); + index_text.as_deref() != current_index.as_deref(); let head_changed = - head_text.as_ref() != current_head.as_deref(); + head_text.as_deref() != current_head.as_deref(); if index_changed && head_changed { if index_text == head_text { Some(DiffBasesChange::SetBoth(head_text)) @@ -3711,13 +3708,13 @@ impl Repository { } (Some(current_index), None) => { let index_changed = - index_text.as_ref() != current_index.as_deref(); + index_text.as_deref() != current_index.as_deref(); index_changed .then_some(DiffBasesChange::SetIndex(index_text)) } (None, Some(current_head)) => { let head_changed = - head_text.as_ref() != current_head.as_deref(); + head_text.as_deref() != current_head.as_deref(); head_changed.then_some(DiffBasesChange::SetHead(head_text)) } (None, None) => None, diff --git a/crates/project/src/git_store/branch_diff.rs b/crates/project/src/git_store/branch_diff.rs index dd0026961ec7ad77b674e2e9506b3133f07ce3f2..661c64a57e3b9e53073beb4c7797a8db150ce194 100644 --- a/crates/project/src/git_store/branch_diff.rs +++ b/crates/project/src/git_store/branch_diff.rs @@ -332,8 +332,6 @@ impl BranchDiff { .update(cx, |project, cx| project.open_buffer(project_path, cx))? .await?; - let languages = project.update(cx, |project, _cx| project.languages().clone())?; - let changes = if let Some(entry) = branch_diff { let oid = match entry { git::status::TreeDiffStatus::Added { .. } => None, @@ -343,7 +341,7 @@ impl BranchDiff { project .update(cx, |project, cx| { project.git_store().update(cx, |git_store, cx| { - git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx) + git_store.open_diff_since(oid, buffer.clone(), repo, cx) }) })? .await? diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 2fda659a6e0839bb8245fee68ae7e22533e75aab..2087cf72e8b0fc589fea566f6ad441383d2f24f7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -7224,9 +7224,9 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.hunks(&snapshot, cx), + unstaged_diff.snapshot(cx).hunks(&snapshot), &snapshot, - &unstaged_diff.base_text_string().unwrap(), + &unstaged_diff.base_text_string(cx).unwrap(), &[ (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()), ( @@ -7252,9 +7252,11 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + unstaged_diff + .snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &unstaged_diff.base_text().text(), + &unstaged_diff.base_text(cx).text(), &[( 2..3, "", @@ -7334,16 +7336,17 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - diff_1.read_with(cx, |diff, _| { - assert_eq!(diff.base_text().language().cloned(), Some(language)) + diff_1.read_with(cx, |diff, cx| { + assert_eq!(diff.base_text(cx).language().cloned(), Some(language)) }); cx.run_until_parked(); diff_1.update(cx, |diff, cx| { let snapshot = buffer_1.read(cx).snapshot(); assert_hunks( - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + diff.snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..1, @@ -7382,9 +7385,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { diff_1.update(cx, |diff, cx| { let snapshot = buffer_1.read(cx).snapshot(); assert_hunks( - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + diff.snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &diff.base_text().text(), + &diff.base_text(cx).text(), &[( 2..3, "", @@ -7411,9 +7415,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { diff_2.update(cx, |diff, cx| { let snapshot = buffer_2.read(cx).snapshot(); assert_hunks( - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + diff.snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 0..0, "// the-deleted-contents\n", @@ -7432,9 +7437,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { diff_2.update(cx, |diff, cx| { let snapshot = buffer_2.read(cx).snapshot(); assert_hunks( - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), + diff.snapshot(cx) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 0..0, "// the-deleted-contents\n", @@ -7503,9 +7509,9 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { // The hunks are initially unstaged. uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7534,14 +7540,15 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let range = snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0)); let hunks = diff - .hunks_intersecting_range(range, &snapshot, cx) + .snapshot(cx) + .hunks_intersecting_range(range, &snapshot) .collect::>(); diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx); assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7573,6 +7580,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let event = diff_events.next().await.unwrap(); if let BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: _, } = event { let changed_range = changed_range.to_point(&snapshot); @@ -7585,9 +7593,9 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { cx.run_until_parked(); uncommitted_diff.update(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7615,6 +7623,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let event = diff_events.next().await.unwrap(); if let BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: _, } = event { let changed_range = changed_range.to_point(&snapshot); @@ -7634,14 +7643,15 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let range = snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0)); let hunks = diff - .hunks_intersecting_range(range, &snapshot, cx) + .snapshot(cx) + .hunks_intersecting_range(range, &snapshot) .collect::>(); diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx); assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7671,6 +7681,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let event = diff_events.next().await.unwrap(); if let BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: _, } = event { let changed_range = changed_range.to_point(&snapshot); @@ -7683,9 +7694,9 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { cx.run_until_parked(); uncommitted_diff.update(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7712,6 +7723,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { let event = diff_events.next().await.unwrap(); if let BufferDiffEvent::DiffChanged { changed_range: Some(changed_range), + base_text_changed_range: _, } = event { let changed_range = changed_range.to_point(&snapshot); @@ -7725,7 +7737,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { // Stage two hunks with separate operations. uncommitted_diff.update(cx, |diff, cx| { - let hunks = diff.hunks(&snapshot, cx).collect::>(); + let hunks = diff.snapshot(cx).hunks(&snapshot).collect::>(); diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx); diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx); }); @@ -7733,9 +7745,9 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { // Both staged hunks appear as pending. uncommitted_diff.update(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7763,9 +7775,9 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) { cx.run_until_parked(); uncommitted_diff.update(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)), ( @@ -7847,9 +7859,9 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) // The hunks are initially unstaged. uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7878,12 +7890,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) // Stage the first hunk. uncommitted_diff.update(cx, |diff, cx| { - let hunk = diff.hunks(&snapshot, cx).next().unwrap(); + let hunk = diff.snapshot(cx).hunks(&snapshot).next().unwrap(); diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx); assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7910,12 +7922,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) // Stage the second hunk *before* receiving the FS event for the first hunk. cx.run_until_parked(); uncommitted_diff.update(cx, |diff, cx| { - let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap(); + let hunk = diff.snapshot(cx).hunks(&snapshot).nth(1).unwrap(); diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx); assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ ( 0..0, @@ -7945,7 +7957,7 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) // Stage the third hunk before receiving the second FS event. uncommitted_diff.update(cx, |diff, cx| { - let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap(); + let hunk = diff.snapshot(cx).hunks(&snapshot).nth(2).unwrap(); diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx); }); @@ -7957,9 +7969,9 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) cx.run_until_parked(); uncommitted_diff.update(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[ (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)), ( @@ -8043,8 +8055,9 @@ async fn test_staging_random_hunks( .await .unwrap(); - let mut hunks = - uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::>()); + let mut hunks = uncommitted_diff.update(cx, |diff, cx| { + diff.snapshot(cx).hunks(&snapshot).collect::>() + }); assert_eq!(hunks.len(), 6); for _i in 0..operations { @@ -8095,7 +8108,8 @@ async fn test_staging_random_hunks( .map(|hunk| (hunk.range.start.row, hunk.secondary_status)) .collect::>(); let actual_hunks = diff - .hunks(&snapshot, cx) + .snapshot(cx) + .hunks(&snapshot) .map(|hunk| (hunk.range.start.row, hunk.secondary_status)) .collect::>(); assert_eq!(actual_hunks, expected_hunks); @@ -8160,9 +8174,9 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.hunks(&snapshot, cx), + uncommitted_diff.snapshot(cx).hunks(&snapshot), &snapshot, - &uncommitted_diff.base_text_string().unwrap(), + &uncommitted_diff.base_text_string(cx).unwrap(), &[( 1..2, " println!(\"hello from HEAD\");\n", @@ -8225,7 +8239,7 @@ async fn test_staging_hunk_preserve_executable_permission(cx: &mut gpui::TestApp .unwrap(); uncommitted_diff.update(cx, |diff, cx| { - let hunks = diff.hunks(&snapshot, cx).collect::>(); + let hunks = diff.snapshot(cx).hunks(&snapshot).collect::>(); diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx); }); @@ -10342,8 +10356,8 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo cx.run_until_parked(); - unstaged_diff.update(cx, |unstaged_diff, _cx| { - let base_text = unstaged_diff.base_text_string().unwrap(); + unstaged_diff.update(cx, |unstaged_diff, cx| { + let base_text = unstaged_diff.base_text_string(cx).unwrap(); assert_eq!(base_text, file_1_staged, "Should start with file_1 staged"); }); @@ -10367,13 +10381,13 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo // the `BufferChangedFilePath` event being handled. unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); - let base_text = unstaged_diff.base_text_string().unwrap(); + let base_text = unstaged_diff.base_text_string(cx).unwrap(); assert_eq!( base_text, file_2_staged, "Diff bases should be automatically updated to file_2 staged content" ); - let hunks: Vec<_> = unstaged_diff.hunks(&snapshot, cx).collect(); + let hunks: Vec<_> = unstaged_diff.snapshot(cx).hunks(&snapshot).collect(); assert!(!hunks.is_empty(), "Should have diff hunks for file_2"); }); @@ -10386,8 +10400,8 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo cx.run_until_parked(); - uncommitted_diff.update(cx, |uncommitted_diff, _cx| { - let base_text = uncommitted_diff.base_text_string().unwrap(); + uncommitted_diff.update(cx, |uncommitted_diff, cx| { + let base_text = uncommitted_diff.base_text_string(cx).unwrap(); assert_eq!( base_text, file_2_committed, "Uncommitted diff should compare against file_2 committed content" @@ -10975,9 +10989,9 @@ async fn test_optimistic_hunks_in_staged_files(cx: &mut gpui::TestAppContext) { // The hunk is initially unstaged. uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 1..2, "two\n", @@ -11002,7 +11016,9 @@ async fn test_optimistic_hunks_in_staged_files(cx: &mut gpui::TestAppContext) { for _ in 0..10 { cx.executor().tick(); let [hunk]: [_; 1] = uncommitted_diff - .read_with(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::>()) + .read_with(cx, |diff, cx| { + diff.snapshot(cx).hunks(&snapshot).collect::>() + }) .try_into() .unwrap(); match hunk.secondary_status { @@ -11014,9 +11030,9 @@ async fn test_optimistic_hunks_in_staged_files(cx: &mut gpui::TestAppContext) { } uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 1..2, "two\n", @@ -11033,9 +11049,9 @@ async fn test_optimistic_hunks_in_staged_files(cx: &mut gpui::TestAppContext) { // The hunk is now fully staged. uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[( 1..2, "two\n", @@ -11058,9 +11074,9 @@ async fn test_optimistic_hunks_in_staged_files(cx: &mut gpui::TestAppContext) { // After committing, there are no more hunks. uncommitted_diff.read_with(cx, |diff, cx| { assert_hunks( - diff.hunks(&snapshot, cx), + diff.snapshot(cx).hunks(&snapshot), &snapshot, - &diff.base_text_string().unwrap(), + &diff.base_text_string(cx).unwrap(), &[] as &[(Range, &str, &str, DiffHunkStatus)], ); }); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index ff739e454f9da175a8ad44386e9519cdbfd22793..3e0a8680dd6ce75ffc926bd1f468d3f9aae66f31 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -96,8 +96,11 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test .await .unwrap(); - diff.update(cx, |diff, _| { - assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }"); + diff.update(cx, |diff, cx| { + assert_eq!( + diff.base_text_string(cx).unwrap(), + "fn one() -> usize { 0 }" + ); }); buffer.update(cx, |buffer, cx| { @@ -157,9 +160,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test &[("src/lib2.rs", "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); - diff.update(cx, |diff, _| { + diff.update(cx, |diff, cx| { assert_eq!( - diff.base_text_string().unwrap(), + diff.base_text_string(cx).unwrap(), "fn one() -> usize { 100 }" ); }); @@ -1443,12 +1446,12 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC .unwrap(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); + assert_eq!(diff.base_text_string(cx).unwrap(), text_1); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_1 ); @@ -1462,12 +1465,12 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC cx.executor().run_until_parked(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); + assert_eq!(diff.base_text_string(cx).unwrap(), text_1); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_2 ); @@ -1482,12 +1485,12 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC cx.executor().run_until_parked(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); + assert_eq!(diff.base_text_string(cx).unwrap(), text_2); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_2 ); @@ -1588,12 +1591,12 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay( .unwrap(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); + assert_eq!(diff.base_text_string(cx).unwrap(), text_1); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_1 ); @@ -1607,12 +1610,12 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay( cx.executor().run_until_parked(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); + assert_eq!(diff.base_text_string(cx).unwrap(), text_1); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_2 ); @@ -1627,12 +1630,12 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay( cx.executor().run_until_parked(); diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); + assert_eq!(diff.base_text_string(cx).unwrap(), text_2); assert_eq!( diff.secondary_diff() .unwrap() .read(cx) - .base_text_string() + .base_text_string(cx) .unwrap(), text_2 ); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index c1916768c1f8a0980fb4d5aa1b718483b08c6087..96d0e60877731bc104595cfb38400face4866e39 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -168,7 +168,7 @@ impl Chunk { if self.is_char_boundary(offset) { return true; } - if PANIC { + if PANIC || cfg!(debug_assertions) { panic_char_boundary(&self.text, offset); } else { log_err_char_boundary(&self.text, offset); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 866552e4e5d9039a9517a556323a4ba7a89fcee1..663be407fc1a297da9c5a854d234d6977864afb7 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -3155,6 +3155,7 @@ impl ToOffset for Point { } impl ToOffset for usize { + #[track_caller] fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { if snapshot .as_rope()