Cargo.lock ๐
@@ -2381,7 +2381,6 @@ dependencies = [
name = "buffer_diff"
version = "0.1.0"
dependencies = [
- "anyhow",
"clock",
"ctor",
"futures 0.3.31",
Cole Miller , cameron , HactarCE , Miguel Raz Guzmรกn Macedo , Anthony , and Cameron created
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 <cameron.studdstreet@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
Co-authored-by: Miguel Raz Guzmรกn Macedo <miguel@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cameron <cameron@zed.dev>
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 | 726 ++--
crates/collab/src/tests/integration_tests.rs | 54
crates/collab/src/tests/random_project_collaboration_tests.rs | 4
crates/edit_prediction_ui/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 | 593 ++-
crates/multi_buffer/src/multi_buffer_tests.rs | 720 +++-
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
crates/remote_server/src/remote_editing_tests.rs | 35
crates/rope/src/chunk.rs | 2
crates/text/src/text.rs | 1
30 files changed, 2,282 insertions(+), 1,491 deletions(-)
@@ -2381,7 +2381,6 @@ dependencies = [
name = "buffer_diff"
version = "0.1.0"
dependencies = [
- "anyhow",
"clock",
"ctor",
"futures 0.3.31",
@@ -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::<Vec<_>>()
+ 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::<Vec<_>>()
};
multibuffer.set_excerpts_for_path(
@@ -86,17 +86,9 @@ impl Diff {
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> 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<MultiBuffer>,
- base_text: Arc<String>,
+ base_text: Arc<str>,
new_buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>,
@@ -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<Range<Point>> {
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::<Vec<_>>();
@@ -357,60 +351,47 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: String,
- base_text: Arc<String>,
+ base_text: Arc<str>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>,
}
async fn build_buffer_diff(
- old_text: Arc<String>,
+ old_text: Arc<str>,
buffer: &Entity<Buffer>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
+ 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)]
@@ -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<ActionLog>,
buffer: &Entity<Buffer>,
buffer_snapshot: text::BufferSnapshot,
- new_base_text: Arc<String>,
+ new_base_text: Arc<str>,
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(),
})
@@ -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::<Vec<_>>();
@@ -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
@@ -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<Entity<language::Buffer>>,
// diff of the index vs head
secondary_diff: Option<Entity<BufferDiff>>,
}
-#[derive(Clone, Debug)]
+#[derive(Clone)]
pub struct BufferDiffSnapshot {
- inner: BufferDiffInner,
+ inner: BufferDiffInner<language::BufferSnapshot>,
secondary_diff: Option<Box<BufferDiffSnapshot>>,
}
+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<Arc<str>>,
+}
+
#[derive(Clone)]
-struct BufferDiffInner {
+struct BufferDiffInner<BaseText> {
hunks: SumTree<InternalDiffHunk>,
- // Used for making staging mo
pending_hunks: SumTree<PendingHunk>,
- 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<language::BufferSnapshot> {
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<Arc<String>>,
- language: Option<Arc<Language>>,
- language_registry: Option<Arc<LanguageRegistry>>,
- cx: &mut App,
- ) -> impl Future<Output = Self> + 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<Arc<String>>,
- base_text_snapshot: language::BufferSnapshot,
- cx: &App,
- ) -> impl Future<Output = Self> + 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<Anchor>,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk> {
- 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<usize>,
+ main_buffer: &'a text::BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ 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<usize>,
+ main_buffer: &'a text::BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ 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<Item = DiffHunk> {
+ 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<u32>,
+ buffer: &'a text::BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ 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<Anchor>,
+ buffer: &text::BufferSnapshot,
+ ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
+ 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::<DiffHunkSummary>(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<Entity<language::Buffer>> {
/// 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<BufferDiff>,
) -> Option<Rope> {
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<language::BufferSnapshot> {
fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
@@ -622,15 +610,22 @@ impl BufferDiffInner {
secondary: Option<&'a Self>,
) -> impl 'a + Iterator<Item = DiffHunk> {
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<Item = DiffHunk> {
+ 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<Anchor>,
+ filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk> {
- 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<Range<Anchor>> {
- 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<String>, Rope)>,
+ diff_base: Option<(Arc<str>, Rope)>,
buffer: text::BufferSnapshot,
diff_options: Option<DiffOptions>,
) -> SumTree<InternalDiffHunk> {
@@ -936,6 +862,98 @@ fn compute_hunks(
tree
}
+fn compare_hunks(
+ new_hunks: &SumTree<InternalDiffHunk>,
+ old_hunks: &SumTree<InternalDiffHunk>,
+ new_snapshot: &text::BufferSnapshot,
+) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
+ 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<Range<text::Anchor>>,
+ base_text_changed_range: Option<Range<usize>>,
},
LanguageChanged,
HunksStagedOrUnstaged(Option<Rope>),
@@ -1075,21 +1093,40 @@ impl EventEmitter<BufferDiffEvent> 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>) -> 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<language::Buffer>,
- cx: &mut App,
+ buffer: &text::BufferSnapshot,
+ cx: &mut Context<Self>,
) -> 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<BufferDiff>) {
@@ -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<Self>,
) -> Option<Rope> {
- 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<Self>,
) {
let hunks = self
- .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
+ .snapshot(cx)
+ .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
.collect::<Vec<_>>();
- 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<Anchor>,
- buffer: &text::BufferSnapshot,
- cx: &App,
- ) -> Option<Range<Anchor>> {
- 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<BufferDiff>,
buffer: text::BufferSnapshot,
- base_text: Option<Arc<String>>,
+ base_text: Option<Arc<str>>,
base_text_changed: bool,
- language_changed: bool,
language: Option<Arc<Language>>,
- language_registry: Option<Arc<LanguageRegistry>>,
- cx: &mut AsyncApp,
- ) -> anyhow::Result<BufferDiffSnapshot> {
- 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<BufferDiffUpdate> {
+ 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<Self>) {
+ pub fn language_changed(
+ &mut self,
+ language: Option<Arc<Language>>,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Self>,
) -> Option<Range<Anchor>> {
- 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<Range<Anchor>>,
clear_pending_hunks: bool,
@@ -1269,27 +1321,24 @@ impl BufferDiff {
) -> Option<Range<Anchor>> {
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 {
@@ -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())],
@@ -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,
@@ -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::<BufferDiff>(|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);
@@ -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,
@@ -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,
@@ -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<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -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<LspColorData>,
@@ -1215,7 +1217,6 @@ pub struct Editor {
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
accent_data: Option<AccentData>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
- 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<bool>,
+ number_deleted_lines: bool,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
show_runnables: Option<bool>,
@@ -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<Self>,
) {
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::<Vec<_>>();
+ 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::<Vec<_>>();
- 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<Self>,
) {
- 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>) {
- 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::<Vec<_>>(),
@@ -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>) {
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<ExcerptId>,
},
+ ExpandExcerptsRequested {
+ excerpt_ids: Vec<ExcerptId>,
+ lines: u32,
+ direction: ExpandExcerptDirection,
+ },
BufferEdited,
Edited {
transaction_id: clock::Lamport,
@@ -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::<lsp::request::Formatting, _, _>({
- 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, |_| {});
@@ -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;
}
@@ -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<MultiBuffer>,
primary_editor: Entity<Editor>,
secondary: Option<SecondaryEditor>,
panes: PaneGroup,
@@ -41,9 +49,12 @@ pub struct SplittableEditor {
}
struct SecondaryEditor {
+ multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
pane: Entity<Pane>,
has_latest_selection: bool,
+ primary_to_secondary: HashMap<ExcerptId, ExcerptId>,
+ secondary_to_primary: HashMap<ExcerptId, ExcerptId>,
_subscriptions: Vec<Subscription>,
}
@@ -63,14 +74,22 @@ impl SplittableEditor {
}
pub fn new_unsplit(
- buffer: Entity<MultiBuffer>,
+ primary_multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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::<Vec<_>>();
+ 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<Buffer>,
+ ranges: impl IntoIterator<Item = Range<Point>> + Clone,
+ context_line_count: u32,
+ diff: Entity<BufferDiff>,
+ cx: &mut Context<Self>,
+ ) -> (Vec<Range<Anchor>>, 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<Item = ExcerptId> + Clone,
+ lines: u32,
+ direction: ExpandExcerptDirection,
+ cx: &mut Context<Self>,
+ ) {
+ 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::<HashMap<_, _>>();
+ }
+ 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>) {
+ 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::<Vec<_>>();
+ let boundary_rows = snapshot
+ .excerpt_boundaries_in_range(MultiBufferOffset(0)..)
+ .map(|b| b.row)
+ .collect::<HashSet<_>>();
+
+ 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::<Vec<_>>()
+ .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::<Vec<_>>();
+ let secondary_diff_hunks = secondary_snapshot
+ .diff_hunks()
+ .map(|hunk| hunk.diff_base_byte_range)
+ .collect::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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<Self>,
+ ) {
+ 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::<Vec<_>>();
+ 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::<String>();
+ 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::<Vec<_>>()
+ });
+ 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::<Vec<_>>();
+ for path in paths_to_remove {
+ self.remove_excerpts_for_path(path.clone(), cx);
+ }
+ }
+ }
+ }
}
impl EventEmitter<EditorEvent> 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<BufferDiff>,
+ 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<ExcerptId> =
+ 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<Point>| {
+ 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<ExcerptId> = 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<ExcerptId> = 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::<Vec<_>>();
+ let path = PathKey::for_buffer(&buffer, cx);
+ editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
+ }
+ true
+ }
+ };
+
+ editor.check_invariants(quiesced, cx);
+ });
+ }
+ }
+}
@@ -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
@@ -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<EditorEvent> for CommitView {}
@@ -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<Buffer>,
new_buffer: &Entity<Buffer>,
+ language_registry: Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
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<EditorEvent> for FileDiffView {}
@@ -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(),
);
}
@@ -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(())
}
@@ -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);
}
@@ -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 <https://zed.dev/features#multi-buffers>
@@ -93,14 +88,6 @@ pub struct MultiBuffer {
/// The writing capability of the multi-buffer.
capability: Capability,
buffer_changed_since_sync: Rc<Cell<bool>>,
- follower: Option<Entity<MultiBuffer>>,
- filter_mode: Option<MultiBufferFilterMode>,
-}
-
-#[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<BufferOffset>,
- /// 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<Range<MultiBufferOffset>>,
}
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<BufferDiff>,
+ /// 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<WeakEntity<Buffer>>,
_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<text::BufferSnapshot>,
+}
+
+impl std::ops::Deref for DiffStateSnapshot {
+ type Target = BufferDiffSnapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.diff
+ }
+}
+
impl DiffState {
fn new(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> 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<BufferDiff>,
+ main_buffer: Entity<Buffer>,
+ cx: &mut Context<MultiBuffer>,
+ ) -> 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<Excerpt>,
- diffs: TreeMap<BufferId, BufferDiffSnapshot>,
+ diffs: TreeMap<BufferId, DiffStateSnapshot>,
diff_transforms: SumTree<DiffTransform>,
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<ExcerptIdMapping>,
replaced_excerpts: TreeMap<ExcerptId, ExcerptId>,
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<DiffTransformHunkInfo>,
},
- 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<usize>,
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<usize>,
+ 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::<Self>())
@@ -665,7 +710,6 @@ pub struct ExpandInfo {
pub struct RowInfo {
pub buffer_id: Option<BufferId>,
pub buffer_row: Option<u32>,
- pub base_text_row: Option<BaseTextRow>,
pub multibuffer_row: Option<MultiBufferRow>,
pub diff_status: Option<buffer_diff::DiffHunkStatus>,
pub expand_info: Option<ExpandInfo>,
@@ -901,7 +945,7 @@ pub struct MultiBufferChunks<'a> {
excerpts: Cursor<'a, 'static, Excerpt, ExcerptOffset>,
diff_transforms:
Cursor<'a, 'static, DiffTransform, Dimensions<MultiBufferOffset, ExcerptOffset>>,
- diffs: &'a TreeMap<BufferId, BufferDiffSnapshot>,
+ diffs: &'a TreeMap<BufferId, DiffStateSnapshot>,
diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>,
buffer_chunk: Option<Chunk<'a>>,
range: Range<MultiBufferOffset>,
@@ -961,12 +1005,10 @@ impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for Diff
struct MultiBufferCursor<'a, MBD, BD> {
excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension<MBD>>,
diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms<MBD>>,
- snapshot: &'a MultiBufferSnapshot,
+ diffs: &'a TreeMap<BufferId, DiffStateSnapshot>,
cached_region: Option<MultiBufferRegion<'a, MBD, BD>>,
}
-/// 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<DiffHunkStatus>,
excerpt: &'a Excerpt,
buffer_range: Range<BD>,
- diff_base_byte_range: Option<Range<usize>>,
range: Range<MBD>,
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<Buffer>, cx: &mut Context<Self>) -> 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<Self>) -> Entity<MultiBuffer> {
- 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<MultiBufferFilterMode>) {
- 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<Self>,
) -> Vec<ExcerptId>
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<Self>,
) -> Vec<ExcerptId>
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<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut Context<Self>,
) 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<BufferDiff>, cx: &mut Context<Self>) {
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<BufferDiff>,
+ main_buffer: WeakEntity<Buffer>,
+ cx: &mut Context<Self>,
+ ) {
+ 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<BufferDiff>,
@@ -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::<Dimensions<Option<&Locator>, 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<BufferDiff>,
+ diff_change_range: Range<usize>,
+ main_buffer: WeakEntity<Buffer>,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Item = Entity<Buffer>> {
+ self.buffers.values().map(|state| state.buffer.clone())
+ }
+
pub fn all_buffers(&self) -> HashSet<Entity<Buffer>> {
- self.buffers
- .values()
- .map(|state| state.buffer.clone())
- .collect()
+ self.all_buffers_iter().collect()
+ }
+
+ pub fn all_buffer_ids_iter(&self) -> impl Iterator<Item = BufferId> {
+ self.buffers.keys().copied()
}
pub fn all_buffer_ids(&self) -> Vec<BufferId> {
- self.buffers.keys().copied().collect()
+ self.all_buffer_ids_iter().collect()
}
pub fn buffer(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
@@ -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<BufferDiff>,
+ main_buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Entity<BufferDiff>> {
@@ -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>) {
+ 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::<Dimensions<Option<&Locator>, ExcerptOffset>>(());
- let mut excerpt_edits = Vec::<Edit<ExcerptOffset>>::new();
+ let mut edits = Vec::<Edit<ExcerptOffset>>::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<BufferId, BufferState>,
diffs: &HashMap<BufferId, DiffState>,
- filter_mode: Option<MultiBufferFilterMode>,
cx: &App,
) -> Vec<Edit<MultiBufferOffset>> {
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(
@@ -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<_>>(),
+ 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<ReferenceExcerpt>,
diffs: HashMap<BufferId, Entity<BufferDiff>>,
+ inverted_diffs: HashMap<BufferId, (Entity<BufferDiff>, WeakEntity<Buffer>)>,
}
#[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<MultiBufferFilterMode>,
- all_diff_hunks_expanded: bool,
- cx: &App,
- ) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
+ fn expected_content(&self, cx: &App) -> (String, Vec<RowInfo>, HashSet<MultiBufferRow>) {
let mut text = String::new();
let mut regions = Vec::<ReferenceRegion>::new();
- let mut filtered_regions = Vec::<ReferenceRegion>::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::<String>();
- 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::<String>();
+ 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<BufferDiff>,
+ main_buffer: Entity<Buffer>,
+ 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<MultiBuffer>,
- 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<Entity<Buffer>> = Vec::new();
let mut base_texts: HashMap<BufferId, String> = 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<BufferId, Entity<BufferDiff>> = 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::<String>();
-
- 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::<String>();
+ 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::<String>();
+
+ 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::<Vec<_>>();
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
@@ -47,15 +47,23 @@ impl MultiBuffer {
self.excerpts_by_path.keys()
}
+ pub fn excerpts_for_path(&self, path: &PathKey) -> impl '_ + Iterator<Item = ExcerptId> {
+ self.excerpts_by_path
+ .get(path)
+ .map(|excerpts| excerpts.as_slice())
+ .unwrap_or(&[])
+ .iter()
+ .copied()
+ }
+
+ pub fn path_for_excerpt(&self, excerpt: ExcerptId) -> Option<PathKey> {
+ self.paths_by_excerpt.get(&excerpt).cloned()
+ }
+
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
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<Entity<Buffer>> {
@@ -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<Buffer>,
@@ -129,8 +129,8 @@ struct BufferGitState {
hunk_staging_operation_count: usize,
hunk_staging_operation_count_as_of_write: usize,
- head_text: Option<Arc<String>>,
- index_text: Option<Arc<String>>,
+ head_text: Option<Arc<str>>,
+ index_text: Option<Arc<str>>,
head_changed: bool,
index_changed: bool,
language_changed: bool,
@@ -693,7 +693,6 @@ impl GitStore {
oid: Option<git::Oid>,
buffer: Entity<Buffer>,
repo: Entity<Repository>,
- languages: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferDiff>>> {
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,
@@ -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?
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
+ let hunks = diff.snapshot(cx).hunks(&snapshot).collect::<Vec<_>>();
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::<Vec<_>>());
+ let mut hunks = uncommitted_diff.update(cx, |diff, cx| {
+ diff.snapshot(cx).hunks(&snapshot).collect::<Vec<_>>()
+ });
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::<Vec<_>>();
let actual_hunks = diff
- .hunks(&snapshot, cx)
+ .snapshot(cx)
+ .hunks(&snapshot)
.map(|hunk| (hunk.range.start.row, hunk.secondary_status))
.collect::<Vec<_>>();
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::<Vec<_>>();
+ let hunks = diff.snapshot(cx).hunks(&snapshot).collect::<Vec<_>>();
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::<Vec<_>>())
+ .read_with(cx, |diff, cx| {
+ diff.snapshot(cx).hunks(&snapshot).collect::<Vec<_>>()
+ })
.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<u32>, &str, &str, DiffHunkStatus)],
);
});
@@ -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
);
@@ -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);
@@ -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()