@@ -1,6 +1,6 @@
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 _, AsyncApp, Context, Entity, EventEmitter, TaskLabel};
use language::{
BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry,
language_settings::language_settings, word_diff_ranges,
@@ -22,7 +22,7 @@ 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>>,
}
@@ -43,10 +43,10 @@ impl std::fmt::Debug for BufferDiffSnapshot {
}
#[derive(Clone)]
-struct BufferDiffInner {
+struct BufferDiffInner<BaseText = language::BufferSnapshot> {
hunks: SumTree<InternalDiffHunk>,
pending_hunks: SumTree<PendingHunk>,
- base_text: language::BufferSnapshot,
+ base_text: BaseText,
base_text_exists: bool,
}
@@ -197,18 +197,6 @@ impl BufferDiffSnapshot {
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,
@@ -226,8 +214,9 @@ impl BufferDiffSnapshot {
}
fn new_with_base_text(
+ base_text_buffer: Entity<language::Buffer>,
buffer: text::BufferSnapshot,
- base_text: Option<Arc<String>>,
+ base_text: Option<Arc<str>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut App,
@@ -243,17 +232,22 @@ impl BufferDiffSnapshot {
);
if let Some(text) = &base_text {
- let base_text_rope = Rope::from(text.as_str());
+ let text_str: &str = &text;
+ let base_text_rope = Rope::from(text_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_buffer.update(cx, |base_text_buffer, cx| {
+ // TODO(split-diff) arguably an unnecessary allocation
+ base_text_buffer.set_text(text.clone(), cx);
+ });
base_text_exists = true;
} else {
base_text_pair = None;
- base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx));
+ base_text_buffer.update(cx, |base_text_buffer, cx| {
+ base_text_buffer.set_text("", cx);
+ });
base_text_exists = false;
};
+ base_text_snapshot = base_text_buffer.read(cx).snapshot();
let hunks = cx
.background_executor()
@@ -263,10 +257,10 @@ impl BufferDiffSnapshot {
});
async move {
- let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
+ let hunks = hunks.await;
Self {
inner: BufferDiffInner {
- base_text,
+ base_text: base_text_snapshot,
hunks,
base_text_exists,
pending_hunks: SumTree::new(&buffer),
@@ -276,12 +270,14 @@ impl BufferDiffSnapshot {
}
}
+ /// Compute a diff using an existing/unchanged base text.
pub fn new_with_base_buffer(
buffer: text::BufferSnapshot,
- base_text: Option<Arc<String>>,
- base_text_snapshot: language::BufferSnapshot,
+ base_text: Option<Arc<str>>,
+ base_text_buffer: Entity<language::Buffer>,
cx: &App,
) -> impl Future<Output = Self> + use<> {
+ let base_text_snapshot = base_text_buffer.read(cx).snapshot();
let diff_options = build_diff_options(
base_text_snapshot.file(),
base_text_snapshot.language().map(|l| l.name()),
@@ -314,7 +310,15 @@ impl BufferDiffSnapshot {
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 base_text_buffer = cx.new(|cx| language::Buffer::local("", cx));
+ Self::new_with_base_text(
+ base_text_buffer,
+ buffer,
+ Some(diff_base.as_str().into()),
+ None,
+ None,
+ cx,
+ )
}))
}
@@ -359,6 +363,43 @@ impl BufferDiffSnapshot {
.hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart)
}
+ 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
}
@@ -423,7 +464,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,
@@ -432,13 +473,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.
@@ -631,7 +673,9 @@ impl BufferDiffInner {
new_index_text.append(index_cursor.suffix());
Some(new_index_text)
}
+}
+impl BufferDiffInner {
fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
@@ -917,7 +961,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> {
@@ -1103,7 +1147,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()
}
}
@@ -1124,19 +1167,26 @@ impl BufferDiff {
pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
BufferDiff {
buffer_id: buffer.remote_id(),
- inner: BufferDiffSnapshot::empty(buffer, cx).inner,
+ inner: BufferDiffInner {
+ base_text: cx.new(|cx| language::Buffer::local("", cx)),
+ 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();
BufferDiff {
buffer_id: buffer.remote_id(),
- inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner,
+ inner: BufferDiffInner {
+ base_text: cx.new(|cx| language::Buffer::local(base_text, cx)),
+ hunks: SumTree::new(buffer),
+ pending_hunks: SumTree::new(buffer),
+ base_text_exists: false,
+ },
secondary_diff: None,
}
}
@@ -1147,9 +1197,11 @@ impl BufferDiff {
buffer: &Entity<language::Buffer>,
cx: &mut App,
) -> Self {
+ let base_buffer = cx.new(|cx| language::Buffer::local("", cx));
let mut base_text = base_text.to_owned();
text::LineEnding::normalize(&mut base_text);
let snapshot = BufferDiffSnapshot::new_with_base_text(
+ base_buffer.clone(),
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
None,
@@ -1159,7 +1211,12 @@ impl BufferDiff {
let snapshot = cx.background_executor().block(snapshot);
Self {
buffer_id: buffer.read(cx).remote_id(),
- inner: snapshot.inner,
+ inner: BufferDiffInner {
+ hunks: snapshot.inner.hunks.clone(),
+ pending_hunks: snapshot.inner.pending_hunks.clone(),
+ base_text: base_buffer,
+ base_text_exists: snapshot.inner.base_text_exists,
+ },
secondary_diff: None,
}
}
@@ -1180,7 +1237,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().len()),
+ base_text_changed_range: Some(0..self.base_text(cx).len()),
});
}
}
@@ -1193,13 +1250,19 @@ 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(),
@@ -1216,30 +1279,10 @@ impl BufferDiff {
new_index_text
}
- pub fn range_to_hunk_range(
- &self,
- range: Range<Anchor>,
- buffer: &text::BufferSnapshot,
- cx: &App,
- ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
- let first_hunk = self
- .hunks_intersecting_range(range.clone(), buffer, cx)
- .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 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>>,
@@ -1248,7 +1291,10 @@ impl BufferDiff {
) -> anyhow::Result<BufferDiffSnapshot> {
Ok(if base_text_changed || language_changed {
cx.update(|cx| {
+ // in this path we should mutate the Entity<Buffer> on `this`
+ // we want to return a BufferDiffSnapshot for which the `base_text` is a BufferSnapshot of the persistent `Entity<Buffer>`
BufferDiffSnapshot::new_with_base_text(
+ this.read(cx).inner.base_text.clone(),
buffer.clone(),
base_text,
language.clone(),
@@ -1259,10 +1305,11 @@ impl BufferDiff {
.await
} else {
this.read_with(cx, |this, cx| {
+ // FIXME we think the base text hasn't changed, so we should _not_ mutate the base text buffer or create a new buffersnapshot
BufferDiffSnapshot::new_with_base_buffer(
buffer.clone(),
base_text,
- this.base_text().clone(),
+ this.inner.base_text.clone(),
cx,
)
})?
@@ -1293,17 +1340,21 @@ 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, mut base_text_changed_range)) =
match (state.base_text_exists, new_state.base_text_exists) {
(false, false) => (true, (None, 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() =>
+ if old_snapshot.inner.base_text.version() == new_state.base_text.version()
+ && old_snapshot.inner.base_text.non_text_state_update_count()
+ == new_state.base_text.non_text_state_update_count() =>
{
- (false, new_state.compare(state, buffer))
+ debug_assert!(
+ old_snapshot.inner.base_text.remote_id() == new_state.base_text.remote_id()
+ );
+ (false, new_state.compare(&old_snapshot.inner, buffer))
}
_ => (
true,
@@ -1316,7 +1367,7 @@ impl BufferDiff {
if let Some(secondary_changed_range) = secondary_diff_change
&& let (Some(secondary_hunk_range), Some(secondary_base_range)) =
- self.range_to_hunk_range(secondary_changed_range, buffer, cx)
+ 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);
@@ -1335,7 +1386,6 @@ impl BufferDiff {
let state = &mut self.inner;
state.base_text_exists = new_state.base_text_exists;
- state.base_text = new_state.base_text;
state.hunks = new_state.hunks;
if base_text_changed || clear_pending_hunks {
if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
@@ -1366,8 +1416,8 @@ impl BufferDiff {
changed_range
}
- pub fn base_text(&self) -> &language::BufferSnapshot {
- &self.inner.base_text
+ pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
+ self.inner.base_text.read(cx).snapshot()
}
pub fn base_text_exists(&self) -> bool {
@@ -1376,7 +1426,12 @@ impl BufferDiff {
pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
BufferDiffSnapshot {
- inner: self.inner.clone(),
+ inner: BufferDiffInner {
+ hunks: self.inner.hunks.clone(),
+ pending_hunks: self.inner.pending_hunks.clone(),
+ base_text: self.inner.base_text.read(cx).snapshot(),
+ base_text_exists: self.inner.base_text_exists,
+ },
secondary_diff: self
.secondary_diff
.as_ref()
@@ -1384,56 +1439,10 @@ impl BufferDiff {
}
}
- pub fn hunks<'a>(
- &'a self,
- buffer_snapshot: &'a text::BufferSnapshot,
- cx: &'a App,
- ) -> impl 'a + Iterator<Item = DiffHunk> {
- self.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
- buffer_snapshot,
- cx,
- )
- }
-
- pub fn hunks_intersecting_range<'a>(
- &'a self,
- range: Range<text::Anchor>,
- buffer_snapshot: &'a text::BufferSnapshot,
- cx: &'a App,
- ) -> impl 'a + Iterator<Item = DiffHunk> {
- let unstaged_counterpart = self
- .secondary_diff
- .as_ref()
- .map(|diff| &diff.read(cx).inner);
- self.inner
- .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
- }
-
- pub fn hunks_intersecting_range_rev<'a>(
- &'a self,
- range: Range<text::Anchor>,
- buffer_snapshot: &'a text::BufferSnapshot,
- ) -> impl 'a + Iterator<Item = DiffHunk> {
- self.inner
- .hunks_intersecting_range_rev(range, buffer_snapshot)
- }
-
- pub fn hunks_in_row_range<'a>(
- &'a self,
- range: Range<u32>,
- buffer: &'a text::BufferSnapshot,
- cx: &'a App,
- ) -> 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, cx)
- }
-
/// Used in cases where the change set isn't derived from git.
pub fn set_base_text(
&mut self,
- base_text: Option<Arc<String>>,
+ base_text: Option<Arc<str>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
buffer: text::BufferSnapshot,
@@ -1443,6 +1452,7 @@ impl BufferDiff {
let this = cx.weak_entity();
let snapshot = BufferDiffSnapshot::new_with_base_text(
+ self.inner.base_text.clone(),
buffer.clone(),
base_text,
language,
@@ -1467,15 +1477,15 @@ impl BufferDiff {
rx
}
- pub fn base_text_string(&self) -> Option<String> {
+ pub fn base_text_string(&self, cx: &App) -> Option<String> {
self.inner
.base_text_exists
- .then(|| self.inner.base_text.text())
+ .then(|| self.inner.base_text.read(cx).text())
}
#[cfg(any(test, feature = "test-support"))]
pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
- let base_text = self.base_text_string().map(Arc::new);
+ let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
let snapshot = BufferDiffSnapshot::new_with_base_buffer(
buffer.clone(),
base_text,
@@ -1622,936 +1632,938 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
}
-#[cfg(test)]
-mod tests {
- use std::fmt::Write as _;
-
- use super::*;
- use gpui::TestAppContext;
- use pretty_assertions::{assert_eq, assert_ne};
- use rand::{Rng as _, rngs::StdRng};
- use text::{Buffer, BufferId, ReplicaId, Rope};
- use unindent::Unindent as _;
- use util::test::marked_text_ranges;
-
- #[ctor::ctor]
- fn init_logger() {
- zlog::init_test();
- }
-
- #[gpui::test]
- async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
- let diff_base = "
- one
- two
- three
- "
- .unindent();
-
- let buffer_text = "
- one
- HELLO
- three
- "
- .unindent();
-
- let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
- let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
- assert_hunks(
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer.remote_id()),
- &buffer,
- ),
- &buffer,
- &diff_base,
- &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
- );
-
- buffer.edit([(0..0, "point five\n")]);
- diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
- assert_hunks(
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer.remote_id()),
- &buffer,
- ),
- &buffer,
- &diff_base,
- &[
- (0..1, "", "point five\n", DiffHunkStatus::added_none()),
- (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
- ],
- );
-
- diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
- assert_hunks::<&str, _>(
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer.remote_id()),
- &buffer,
- ),
- &buffer,
- &diff_base,
- &[],
- );
- }
-
- #[gpui::test]
- async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
- let head_text = "
- zero
- one
- two
- three
- four
- five
- six
- seven
- eight
- nine
- "
- .unindent();
-
- let index_text = "
- zero
- one
- TWO
- three
- FOUR
- five
- six
- seven
- eight
- NINE
- "
- .unindent();
-
- let buffer_text = "
- zero
- one
- TWO
- three
- FOUR
- FIVE
- six
- SEVEN
- eight
- nine
- "
- .unindent();
-
- let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
- let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
- let mut uncommitted_diff =
- BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
- uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
-
- let expected_hunks = vec![
- (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
- (
- 4..6,
- "four\nfive\n",
- "FOUR\nFIVE\n",
- DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
- ),
- (
- 7..8,
- "seven\n",
- "SEVEN\n",
- DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
- ),
- ];
-
- assert_hunks(
- uncommitted_diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer.remote_id()),
- &buffer,
- ),
- &buffer,
- &head_text,
- &expected_hunks,
- );
- }
-
- #[gpui::test]
- async fn test_buffer_diff_range(cx: &mut TestAppContext) {
- let diff_base = Arc::new(
- "
- one
- two
- three
- four
- five
- six
- seven
- eight
- nine
- ten
- "
- .unindent(),
- );
-
- let buffer_text = "
- A
- one
- B
- two
- C
- three
- HELLO
- four
- five
- SIXTEEN
- seven
- eight
- WORLD
- nine
-
- ten
-
- "
- .unindent();
-
- let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
- let diff = cx
- .update(|cx| {
- BufferDiffSnapshot::new_with_base_text(
- buffer.snapshot(),
- Some(diff_base.clone()),
- None,
- None,
- cx,
- )
- })
- .await;
- assert_eq!(
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(buffer.remote_id()),
- &buffer
- )
- .count(),
- 8
- );
-
- assert_hunks(
- diff.hunks_intersecting_range(
- buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
- &buffer,
- ),
- &buffer,
- &diff_base,
- &[
- (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
- (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
- (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
- ],
- );
- }
-
- #[gpui::test]
- async fn test_stage_hunk(cx: &mut TestAppContext) {
- struct Example {
- name: &'static str,
- head_text: String,
- index_text: String,
- buffer_marked_text: String,
- final_index_text: String,
- }
-
- let table = [
- Example {
- name: "uncommitted hunk straddles end of unstaged hunk",
- head_text: "
- one
- two
- three
- four
- five
- "
- .unindent(),
- index_text: "
- one
- TWO_HUNDRED
- three
- FOUR_HUNDRED
- five
- "
- .unindent(),
- buffer_marked_text: "
- ZERO
- one
- two
- «THREE_HUNDRED
- FOUR_HUNDRED»
- five
- SIX
- "
- .unindent(),
- final_index_text: "
- one
- two
- THREE_HUNDRED
- FOUR_HUNDRED
- five
- "
- .unindent(),
- },
- Example {
- name: "uncommitted hunk straddles start of unstaged hunk",
- head_text: "
- one
- two
- three
- four
- five
- "
- .unindent(),
- index_text: "
- one
- TWO_HUNDRED
- three
- FOUR_HUNDRED
- five
- "
- .unindent(),
- buffer_marked_text: "
- ZERO
- one
- «TWO_HUNDRED
- THREE_HUNDRED»
- four
- five
- SIX
- "
- .unindent(),
- final_index_text: "
- one
- TWO_HUNDRED
- THREE_HUNDRED
- four
- five
- "
- .unindent(),
- },
- Example {
- name: "uncommitted hunk strictly contains unstaged hunks",
- head_text: "
- one
- two
- three
- four
- five
- six
- seven
- "
- .unindent(),
- index_text: "
- one
- TWO
- THREE
- FOUR
- FIVE
- SIX
- seven
- "
- .unindent(),
- buffer_marked_text: "
- one
- TWO
- «THREE_HUNDRED
- FOUR
- FIVE_HUNDRED»
- SIX
- seven
- "
- .unindent(),
- final_index_text: "
- one
- TWO
- THREE_HUNDRED
- FOUR
- FIVE_HUNDRED
- SIX
- seven
- "
- .unindent(),
- },
- Example {
- name: "uncommitted deletion hunk",
- head_text: "
- one
- two
- three
- four
- five
- "
- .unindent(),
- index_text: "
- one
- two
- three
- four
- five
- "
- .unindent(),
- buffer_marked_text: "
- one
- ˇfive
- "
- .unindent(),
- final_index_text: "
- one
- five
- "
- .unindent(),
- },
- Example {
- name: "one unstaged hunk that contains two uncommitted hunks",
- head_text: "
- one
- two
-
- three
- four
- "
- .unindent(),
- index_text: "
- one
- two
- three
- four
- "
- .unindent(),
- buffer_marked_text: "
- «one
-
- three // modified
- four»
- "
- .unindent(),
- final_index_text: "
- one
-
- three // modified
- four
- "
- .unindent(),
- },
- Example {
- name: "one uncommitted hunk that contains two unstaged hunks",
- head_text: "
- one
- two
- three
- four
- five
- "
- .unindent(),
- index_text: "
- ZERO
- one
- TWO
- THREE
- FOUR
- five
- "
- .unindent(),
- buffer_marked_text: "
- «one
- TWO_HUNDRED
- THREE
- FOUR_HUNDRED
- five»
- "
- .unindent(),
- final_index_text: "
- ZERO
- one
- TWO_HUNDRED
- THREE
- FOUR_HUNDRED
- five
- "
- .unindent(),
- },
- ];
-
- for example in table {
- let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
- let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
- let hunk_range =
- buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
-
- let unstaged =
- BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
- let uncommitted =
- BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
-
- let unstaged_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&buffer, cx);
- diff.set_snapshot(unstaged, &buffer, cx);
- diff
- });
-
- let uncommitted_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&buffer, cx);
- diff.set_snapshot(uncommitted, &buffer, cx);
- diff.set_secondary_diff(unstaged_diff);
- diff
- });
-
- uncommitted_diff.update(cx, |diff, cx| {
- let hunks = diff
- .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
- .collect::<Vec<_>>();
- for hunk in &hunks {
- assert_ne!(
- hunk.secondary_status,
- DiffHunkSecondaryStatus::NoSecondaryHunk
- )
- }
-
- let new_index_text = diff
- .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
- .unwrap()
- .to_string();
-
- let hunks = diff
- .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
- .collect::<Vec<_>>();
- for hunk in &hunks {
- assert_eq!(
- hunk.secondary_status,
- DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
- )
- }
-
- pretty_assertions::assert_eq!(
- new_index_text,
- example.final_index_text,
- "example: {}",
- example.name
- );
- });
- }
- }
-
- #[gpui::test]
- async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
- let head_text = "
- one
- two
- three
- "
- .unindent();
- let index_text = head_text.clone();
- let buffer_text = "
- one
- three
- "
- .unindent();
-
- let buffer = Buffer::new(
- ReplicaId::LOCAL,
- BufferId::new(1).unwrap(),
- buffer_text.clone(),
- );
- let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
- let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
- let unstaged_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&buffer, cx);
- diff.set_snapshot(unstaged, &buffer, cx);
- diff
- });
- let uncommitted_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&buffer, cx);
- diff.set_snapshot(uncommitted, &buffer, cx);
- diff.set_secondary_diff(unstaged_diff.clone());
- diff
- });
-
- uncommitted_diff.update(cx, |diff, cx| {
- let hunk = diff.hunks(&buffer, cx).next().unwrap();
-
- let new_index_text = diff
- .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
- .unwrap()
- .to_string();
- assert_eq!(new_index_text, buffer_text);
-
- let hunk = diff.hunks(&buffer, cx).next().unwrap();
- assert_eq!(
- hunk.secondary_status,
- DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
- );
-
- let index_text = diff
- .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
- .unwrap()
- .to_string();
- assert_eq!(index_text, head_text);
-
- let hunk = diff.hunks(&buffer, cx).next().unwrap();
- // optimistically unstaged (fine, could also be HasSecondaryHunk)
- assert_eq!(
- hunk.secondary_status,
- DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
- );
- });
- }
-
- #[gpui::test]
- async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
- let base_text = "
- zero
- one
- two
- three
- four
- five
- six
- seven
- eight
- nine
- "
- .unindent();
-
- let buffer_text_1 = "
- one
- three
- four
- five
- SIX
- seven
- eight
- NINE
- "
- .unindent();
-
- let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
-
- let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
- let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_1.inner.compare(&empty_diff.inner, &buffer).0.unwrap();
- assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
-
- // Edit does not affect the diff.
- buffer.edit_via_marked_text(
- &"
- one
- three
- four
- five
- «SIX.5»
- seven
- eight
- NINE
- "
- .unindent(),
- );
- let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
- assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer).0);
-
- // Edit turns a deletion hunk into a modification.
- buffer.edit_via_marked_text(
- &"
- one
- «THREE»
- four
- five
- SIX.5
- seven
- eight
- NINE
- "
- .unindent(),
- );
- let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_3.inner.compare(&diff_2.inner, &buffer).0.unwrap();
- assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
-
- // Edit turns a modification hunk into a deletion.
- buffer.edit_via_marked_text(
- &"
- one
- THREE
- four
- five«»
- seven
- eight
- NINE
- "
- .unindent(),
- );
- let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_4.inner.compare(&diff_3.inner, &buffer).0.unwrap();
- assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
-
- // Edit introduces a new insertion hunk.
- buffer.edit_via_marked_text(
- &"
- one
- THREE
- four«
- FOUR.5
- »five
- seven
- eight
- NINE
- "
- .unindent(),
- );
- let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
- let range = diff_5.inner.compare(&diff_4.inner, &buffer).0.unwrap();
- assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
-
- // Edit removes a hunk.
- buffer.edit_via_marked_text(
- &"
- one
- THREE
- four
- FOUR.5
- five
- seven
- eight
- «nine»
- "
- .unindent(),
- );
- let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
- let range = diff_6.inner.compare(&diff_5.inner, &buffer).0.unwrap();
- assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
- }
-
- #[gpui::test(iterations = 100)]
- async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
- fn gen_line(rng: &mut StdRng) -> String {
- if rng.random_bool(0.2) {
- "\n".to_owned()
- } else {
- let c = rng.random_range('A'..='Z');
- format!("{c}{c}{c}\n")
- }
- }
-
- fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
- let mut old_lines = {
- let mut old_lines = Vec::new();
- let old_lines_iter = head.lines();
- for line in old_lines_iter {
- assert!(!line.ends_with("\n"));
- old_lines.push(line.to_owned());
- }
- if old_lines.last().is_some_and(|line| line.is_empty()) {
- old_lines.pop();
- }
- old_lines.into_iter()
- };
- let mut result = String::new();
- let unchanged_count = rng.random_range(0..=old_lines.len());
- result +=
- &old_lines
- .by_ref()
- .take(unchanged_count)
- .fold(String::new(), |mut s, line| {
- writeln!(&mut s, "{line}").unwrap();
- s
- });
- while old_lines.len() > 0 {
- let deleted_count = rng.random_range(0..=old_lines.len());
- let _advance = old_lines
- .by_ref()
- .take(deleted_count)
- .map(|line| line.len() + 1)
- .sum::<usize>();
- let minimum_added = if deleted_count == 0 { 1 } else { 0 };
- let added_count = rng.random_range(minimum_added..=5);
- let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
- result += &addition;
-
- if old_lines.len() > 0 {
- let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
- if blank_lines == old_lines.len() {
- break;
- };
- let unchanged_count =
- rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
- result += &old_lines.by_ref().take(unchanged_count).fold(
- String::new(),
- |mut s, line| {
- writeln!(&mut s, "{line}").unwrap();
- s
- },
- );
- }
- }
- result
- }
-
- fn uncommitted_diff(
- working_copy: &language::BufferSnapshot,
- index_text: &Rope,
- head_text: String,
- cx: &mut TestAppContext,
- ) -> Entity<BufferDiff> {
- let inner =
- BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
- let secondary = BufferDiff {
- buffer_id: working_copy.remote_id(),
- inner: BufferDiffSnapshot::new_sync(
- working_copy.text.clone(),
- index_text.to_string(),
- cx,
- )
- .inner,
- secondary_diff: None,
- };
- let secondary = cx.new(|_| secondary);
- cx.new(|_| BufferDiff {
- buffer_id: working_copy.remote_id(),
- inner,
- secondary_diff: Some(secondary),
- })
- }
-
- let operations = std::env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(10);
-
- let rng = &mut rng;
- let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
- writeln!(&mut s, "{c}{c}{c}").unwrap();
- s
- });
- let working_copy = gen_working_copy(rng, &head_text);
- let working_copy = cx.new(|cx| {
- language::Buffer::local_normalized(
- Rope::from(working_copy.as_str()),
- text::LineEnding::default(),
- cx,
- )
- });
- let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
- let mut index_text = if rng.random() {
- Rope::from(head_text.as_str())
- } else {
- working_copy.as_rope().clone()
- };
-
- let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
- let mut hunks = diff.update(cx, |diff, cx| {
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(diff.buffer_id),
- &working_copy,
- cx,
- )
- .collect::<Vec<_>>()
- });
- if hunks.is_empty() {
- return;
- }
-
- for _ in 0..operations {
- let i = rng.random_range(0..hunks.len());
- let hunk = &mut hunks[i];
- let hunk_to_change = hunk.clone();
- let stage = match hunk.secondary_status {
- DiffHunkSecondaryStatus::HasSecondaryHunk => {
- hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
- true
- }
- DiffHunkSecondaryStatus::NoSecondaryHunk => {
- hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
- false
- }
- _ => unreachable!(),
- };
-
- index_text = diff.update(cx, |diff, cx| {
- diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
- .unwrap()
- });
-
- diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
- let found_hunks = diff.update(cx, |diff, cx| {
- diff.hunks_intersecting_range(
- Anchor::min_max_range_for_buffer(diff.buffer_id),
- &working_copy,
- cx,
- )
- .collect::<Vec<_>>()
- });
- assert_eq!(hunks.len(), found_hunks.len());
-
- for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
- assert_eq!(
- expected_hunk.buffer_range.to_point(&working_copy),
- found_hunk.buffer_range.to_point(&working_copy)
- );
- assert_eq!(
- expected_hunk.diff_base_byte_range,
- found_hunk.diff_base_byte_range
- );
- assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
- }
- hunks = found_hunks;
- }
- }
-
- #[gpui::test]
- async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
- let base_text = "
- zero
- one
- two
- three
- four
- five
- six
- seven
- eight
- "
- .unindent();
- let buffer_text = "
- zero
- ONE
- two
- NINE
- five
- seven
- "
- .unindent();
-
- // zero
- // - one
- // + ONE
- // two
- // - three
- // - four
- // + NINE
- // five
- // - six
- // seven
- // + eight
-
- let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
- let buffer_snapshot = buffer.snapshot();
- let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
- let expected_results = [
- // don't format me
- (0, 0),
- (1, 2),
- (2, 2),
- (3, 5),
- (4, 5),
- (5, 7),
- (6, 9),
- ];
- for (buffer_row, expected) in expected_results {
- assert_eq!(
- diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
- expected,
- "{buffer_row}"
- );
- }
- }
-}
+// FIXME
+// #[cfg(test)]
+// mod tests {
+// use std::fmt::Write as _;
+//
+// use super::*;
+// use gpui::TestAppContext;
+// use pretty_assertions::{assert_eq, assert_ne};
+// use rand::{Rng as _, rngs::StdRng};
+// use text::{Buffer, BufferId, ReplicaId, Rope};
+// use unindent::Unindent as _;
+// use util::test::marked_text_ranges;
+//
+// #[ctor::ctor]
+// fn init_logger() {
+// zlog::init_test();
+// }
+//
+// #[gpui::test]
+// async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
+// let diff_base = "
+// one
+// two
+// three
+// "
+// .unindent();
+//
+// let buffer_text = "
+// one
+// HELLO
+// three
+// "
+// .unindent();
+//
+// let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+// let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
+// assert_hunks(
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(buffer.remote_id()),
+// &buffer,
+// ),
+// &buffer,
+// &diff_base,
+// &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
+// );
+//
+// buffer.edit([(0..0, "point five\n")]);
+// diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
+// assert_hunks(
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(buffer.remote_id()),
+// &buffer,
+// ),
+// &buffer,
+// &diff_base,
+// &[
+// (0..1, "", "point five\n", DiffHunkStatus::added_none()),
+// (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
+// ],
+// );
+//
+// diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
+// assert_hunks::<&str, _>(
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(buffer.remote_id()),
+// &buffer,
+// ),
+// &buffer,
+// &diff_base,
+// &[],
+// );
+// }
+//
+// #[gpui::test]
+// async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
+// let head_text = "
+// zero
+// one
+// two
+// three
+// four
+// five
+// six
+// seven
+// eight
+// nine
+// "
+// .unindent();
+//
+// let index_text = "
+// zero
+// one
+// TWO
+// three
+// FOUR
+// five
+// six
+// seven
+// eight
+// NINE
+// "
+// .unindent();
+//
+// let buffer_text = "
+// zero
+// one
+// TWO
+// three
+// FOUR
+// FIVE
+// six
+// SEVEN
+// eight
+// nine
+// "
+// .unindent();
+//
+// let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+// let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
+// let mut uncommitted_diff =
+// BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
+// uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
+//
+// let expected_hunks = vec![
+// (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
+// (
+// 4..6,
+// "four\nfive\n",
+// "FOUR\nFIVE\n",
+// DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
+// ),
+// (
+// 7..8,
+// "seven\n",
+// "SEVEN\n",
+// DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
+// ),
+// ];
+//
+// assert_hunks(
+// uncommitted_diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(buffer.remote_id()),
+// &buffer,
+// ),
+// &buffer,
+// &head_text,
+// &expected_hunks,
+// );
+// }
+//
+// #[gpui::test]
+// async fn test_buffer_diff_range(cx: &mut TestAppContext) {
+// let diff_base = Arc::new(
+// "
+// one
+// two
+// three
+// four
+// five
+// six
+// seven
+// eight
+// nine
+// ten
+// "
+// .unindent(),
+// );
+//
+// let buffer_text = "
+// A
+// one
+// B
+// two
+// C
+// three
+// HELLO
+// four
+// five
+// SIXTEEN
+// seven
+// eight
+// WORLD
+// nine
+//
+// ten
+//
+// "
+// .unindent();
+//
+// let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+// let diff = cx
+// .update(|cx| {
+// BufferDiffSnapshot::new_with_base_text(
+// buffer.snapshot(),
+// Some(diff_base.clone()),
+// None,
+// None,
+// cx,
+// )
+// })
+// .await;
+// assert_eq!(
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(buffer.remote_id()),
+// &buffer
+// )
+// .count(),
+// 8
+// );
+//
+// assert_hunks(
+// diff.hunks_intersecting_range(
+// buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
+// &buffer,
+// ),
+// &buffer,
+// &diff_base,
+// &[
+// (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
+// (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
+// (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
+// ],
+// );
+// }
+//
+// #[gpui::test]
+// async fn test_stage_hunk(cx: &mut TestAppContext) {
+// struct Example {
+// name: &'static str,
+// head_text: String,
+// index_text: String,
+// buffer_marked_text: String,
+// final_index_text: String,
+// }
+//
+// let table = [
+// Example {
+// name: "uncommitted hunk straddles end of unstaged hunk",
+// head_text: "
+// one
+// two
+// three
+// four
+// five
+// "
+// .unindent(),
+// index_text: "
+// one
+// TWO_HUNDRED
+// three
+// FOUR_HUNDRED
+// five
+// "
+// .unindent(),
+// buffer_marked_text: "
+// ZERO
+// one
+// two
+// «THREE_HUNDRED
+// FOUR_HUNDRED»
+// five
+// SIX
+// "
+// .unindent(),
+// final_index_text: "
+// one
+// two
+// THREE_HUNDRED
+// FOUR_HUNDRED
+// five
+// "
+// .unindent(),
+// },
+// Example {
+// name: "uncommitted hunk straddles start of unstaged hunk",
+// head_text: "
+// one
+// two
+// three
+// four
+// five
+// "
+// .unindent(),
+// index_text: "
+// one
+// TWO_HUNDRED
+// three
+// FOUR_HUNDRED
+// five
+// "
+// .unindent(),
+// buffer_marked_text: "
+// ZERO
+// one
+// «TWO_HUNDRED
+// THREE_HUNDRED»
+// four
+// five
+// SIX
+// "
+// .unindent(),
+// final_index_text: "
+// one
+// TWO_HUNDRED
+// THREE_HUNDRED
+// four
+// five
+// "
+// .unindent(),
+// },
+// Example {
+// name: "uncommitted hunk strictly contains unstaged hunks",
+// head_text: "
+// one
+// two
+// three
+// four
+// five
+// six
+// seven
+// "
+// .unindent(),
+// index_text: "
+// one
+// TWO
+// THREE
+// FOUR
+// FIVE
+// SIX
+// seven
+// "
+// .unindent(),
+// buffer_marked_text: "
+// one
+// TWO
+// «THREE_HUNDRED
+// FOUR
+// FIVE_HUNDRED»
+// SIX
+// seven
+// "
+// .unindent(),
+// final_index_text: "
+// one
+// TWO
+// THREE_HUNDRED
+// FOUR
+// FIVE_HUNDRED
+// SIX
+// seven
+// "
+// .unindent(),
+// },
+// Example {
+// name: "uncommitted deletion hunk",
+// head_text: "
+// one
+// two
+// three
+// four
+// five
+// "
+// .unindent(),
+// index_text: "
+// one
+// two
+// three
+// four
+// five
+// "
+// .unindent(),
+// buffer_marked_text: "
+// one
+// ˇfive
+// "
+// .unindent(),
+// final_index_text: "
+// one
+// five
+// "
+// .unindent(),
+// },
+// Example {
+// name: "one unstaged hunk that contains two uncommitted hunks",
+// head_text: "
+// one
+// two
+//
+// three
+// four
+// "
+// .unindent(),
+// index_text: "
+// one
+// two
+// three
+// four
+// "
+// .unindent(),
+// buffer_marked_text: "
+// «one
+//
+// three // modified
+// four»
+// "
+// .unindent(),
+// final_index_text: "
+// one
+//
+// three // modified
+// four
+// "
+// .unindent(),
+// },
+// Example {
+// name: "one uncommitted hunk that contains two unstaged hunks",
+// head_text: "
+// one
+// two
+// three
+// four
+// five
+// "
+// .unindent(),
+// index_text: "
+// ZERO
+// one
+// TWO
+// THREE
+// FOUR
+// five
+// "
+// .unindent(),
+// buffer_marked_text: "
+// «one
+// TWO_HUNDRED
+// THREE
+// FOUR_HUNDRED
+// five»
+// "
+// .unindent(),
+// final_index_text: "
+// ZERO
+// one
+// TWO_HUNDRED
+// THREE
+// FOUR_HUNDRED
+// five
+// "
+// .unindent(),
+// },
+// ];
+//
+// for example in table {
+// let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
+// let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+// let hunk_range =
+// buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
+//
+// let unstaged =
+// BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
+// let uncommitted =
+// BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
+//
+// let unstaged_diff = cx.new(|cx| {
+// let mut diff = BufferDiff::new(&buffer, cx);
+// diff.set_snapshot(unstaged, &buffer, cx);
+// diff
+// });
+//
+// let uncommitted_diff = cx.new(|cx| {
+// let mut diff = BufferDiff::new(&buffer, cx);
+// diff.set_snapshot(uncommitted, &buffer, cx);
+// diff.set_secondary_diff(unstaged_diff);
+// diff
+// });
+//
+// uncommitted_diff.update(cx, |diff, cx| {
+// let hunks = diff
+// .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
+// .collect::<Vec<_>>();
+// for hunk in &hunks {
+// assert_ne!(
+// hunk.secondary_status,
+// DiffHunkSecondaryStatus::NoSecondaryHunk
+// )
+// }
+//
+// let new_index_text = diff
+// .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
+// .unwrap()
+// .to_string();
+//
+// let hunks = diff
+// .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
+// .collect::<Vec<_>>();
+// for hunk in &hunks {
+// assert_eq!(
+// hunk.secondary_status,
+// DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
+// )
+// }
+//
+// pretty_assertions::assert_eq!(
+// new_index_text,
+// example.final_index_text,
+// "example: {}",
+// example.name
+// );
+// });
+// }
+// }
+//
+// #[gpui::test]
+// async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
+// let head_text = "
+// one
+// two
+// three
+// "
+// .unindent();
+// let index_text = head_text.clone();
+// let buffer_text = "
+// one
+// three
+// "
+// .unindent();
+//
+// let buffer = Buffer::new(
+// ReplicaId::LOCAL,
+// BufferId::new(1).unwrap(),
+// buffer_text.clone(),
+// );
+// let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
+// let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
+// let unstaged_diff = cx.new(|cx| {
+// let mut diff = BufferDiff::new(&buffer, cx);
+// diff.set_snapshot(unstaged, &buffer, cx);
+// diff
+// });
+// let uncommitted_diff = cx.new(|cx| {
+// let mut diff = BufferDiff::new(&buffer, cx);
+// diff.set_snapshot(uncommitted, &buffer, cx);
+// diff.set_secondary_diff(unstaged_diff.clone());
+// diff
+// });
+//
+// uncommitted_diff.update(cx, |diff, cx| {
+// let hunk = diff.hunks(&buffer, cx).next().unwrap();
+//
+// let new_index_text = diff
+// .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
+// .unwrap()
+// .to_string();
+// assert_eq!(new_index_text, buffer_text);
+//
+// let hunk = diff.hunks(&buffer, cx).next().unwrap();
+// assert_eq!(
+// hunk.secondary_status,
+// DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
+// );
+//
+// let index_text = diff
+// .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
+// .unwrap()
+// .to_string();
+// assert_eq!(index_text, head_text);
+//
+// let hunk = diff.hunks(&buffer, cx).next().unwrap();
+// // optimistically unstaged (fine, could also be HasSecondaryHunk)
+// assert_eq!(
+// hunk.secondary_status,
+// DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
+// );
+// });
+// }
+//
+// #[gpui::test]
+// async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
+// let base_text = "
+// zero
+// one
+// two
+// three
+// four
+// five
+// six
+// seven
+// eight
+// nine
+// "
+// .unindent();
+//
+// let buffer_text_1 = "
+// one
+// three
+// four
+// five
+// SIX
+// seven
+// eight
+// NINE
+// "
+// .unindent();
+//
+// let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
+//
+// let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
+// let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+// let range = diff_1.inner.compare(&empty_diff.inner, &buffer).0.unwrap();
+// assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
+//
+// // Edit does not affect the diff.
+// buffer.edit_via_marked_text(
+// &"
+// one
+// three
+// four
+// five
+// «SIX.5»
+// seven
+// eight
+// NINE
+// "
+// .unindent(),
+// );
+// let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+// assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer).0);
+//
+// // Edit turns a deletion hunk into a modification.
+// buffer.edit_via_marked_text(
+// &"
+// one
+// «THREE»
+// four
+// five
+// SIX.5
+// seven
+// eight
+// NINE
+// "
+// .unindent(),
+// );
+// let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+// let range = diff_3.inner.compare(&diff_2.inner, &buffer).0.unwrap();
+// assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
+//
+// // Edit turns a modification hunk into a deletion.
+// buffer.edit_via_marked_text(
+// &"
+// one
+// THREE
+// four
+// five«»
+// seven
+// eight
+// NINE
+// "
+// .unindent(),
+// );
+// let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+// let range = diff_4.inner.compare(&diff_3.inner, &buffer).0.unwrap();
+// assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
+//
+// // Edit introduces a new insertion hunk.
+// buffer.edit_via_marked_text(
+// &"
+// one
+// THREE
+// four«
+// FOUR.5
+// »five
+// seven
+// eight
+// NINE
+// "
+// .unindent(),
+// );
+// let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
+// let range = diff_5.inner.compare(&diff_4.inner, &buffer).0.unwrap();
+// assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
+//
+// // Edit removes a hunk.
+// buffer.edit_via_marked_text(
+// &"
+// one
+// THREE
+// four
+// FOUR.5
+// five
+// seven
+// eight
+// «nine»
+// "
+// .unindent(),
+// );
+// let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
+// let range = diff_6.inner.compare(&diff_5.inner, &buffer).0.unwrap();
+// assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
+// }
+//
+// #[gpui::test(iterations = 100)]
+// async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
+// fn gen_line(rng: &mut StdRng) -> String {
+// if rng.random_bool(0.2) {
+// "\n".to_owned()
+// } else {
+// let c = rng.random_range('A'..='Z');
+// format!("{c}{c}{c}\n")
+// }
+// }
+//
+// fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
+// let mut old_lines = {
+// let mut old_lines = Vec::new();
+// let old_lines_iter = head.lines();
+// for line in old_lines_iter {
+// assert!(!line.ends_with("\n"));
+// old_lines.push(line.to_owned());
+// }
+// if old_lines.last().is_some_and(|line| line.is_empty()) {
+// old_lines.pop();
+// }
+// old_lines.into_iter()
+// };
+// let mut result = String::new();
+// let unchanged_count = rng.random_range(0..=old_lines.len());
+// result +=
+// &old_lines
+// .by_ref()
+// .take(unchanged_count)
+// .fold(String::new(), |mut s, line| {
+// writeln!(&mut s, "{line}").unwrap();
+// s
+// });
+// while old_lines.len() > 0 {
+// let deleted_count = rng.random_range(0..=old_lines.len());
+// let _advance = old_lines
+// .by_ref()
+// .take(deleted_count)
+// .map(|line| line.len() + 1)
+// .sum::<usize>();
+// let minimum_added = if deleted_count == 0 { 1 } else { 0 };
+// let added_count = rng.random_range(minimum_added..=5);
+// let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
+// result += &addition;
+//
+// if old_lines.len() > 0 {
+// let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
+// if blank_lines == old_lines.len() {
+// break;
+// };
+// let unchanged_count =
+// rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
+// result += &old_lines.by_ref().take(unchanged_count).fold(
+// String::new(),
+// |mut s, line| {
+// writeln!(&mut s, "{line}").unwrap();
+// s
+// },
+// );
+// }
+// }
+// result
+// }
+//
+// fn uncommitted_diff(
+// working_copy: &language::BufferSnapshot,
+// index_text: &Rope,
+// head_text: String,
+// cx: &mut TestAppContext,
+// ) -> Entity<BufferDiff> {
+// let inner =
+// BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
+// let secondary = BufferDiff {
+// buffer_id: working_copy.remote_id(),
+// inner: BufferDiffSnapshot::new_sync(
+// working_copy.text.clone(),
+// index_text.to_string(),
+// cx,
+// )
+// .inner,
+// secondary_diff: None,
+// };
+// let secondary = cx.new(|_| secondary);
+// cx.new(|_| BufferDiff {
+// buffer_id: working_copy.remote_id(),
+// inner,
+// secondary_diff: Some(secondary),
+// })
+// }
+//
+// let operations = std::env::var("OPERATIONS")
+// .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+// .unwrap_or(10);
+//
+// let rng = &mut rng;
+// let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
+// writeln!(&mut s, "{c}{c}{c}").unwrap();
+// s
+// });
+// let working_copy = gen_working_copy(rng, &head_text);
+// let working_copy = cx.new(|cx| {
+// language::Buffer::local_normalized(
+// Rope::from(working_copy.as_str()),
+// text::LineEnding::default(),
+// cx,
+// )
+// });
+// let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
+// let mut index_text = if rng.random() {
+// Rope::from(head_text.as_str())
+// } else {
+// working_copy.as_rope().clone()
+// };
+//
+// let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
+// let mut hunks = diff.update(cx, |diff, cx| {
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(diff.buffer_id),
+// &working_copy,
+// cx,
+// )
+// .collect::<Vec<_>>()
+// });
+// if hunks.is_empty() {
+// return;
+// }
+//
+// for _ in 0..operations {
+// let i = rng.random_range(0..hunks.len());
+// let hunk = &mut hunks[i];
+// let hunk_to_change = hunk.clone();
+// let stage = match hunk.secondary_status {
+// DiffHunkSecondaryStatus::HasSecondaryHunk => {
+// hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
+// true
+// }
+// DiffHunkSecondaryStatus::NoSecondaryHunk => {
+// hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
+// false
+// }
+// _ => unreachable!(),
+// };
+//
+// index_text = diff.update(cx, |diff, cx| {
+// diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
+// .unwrap()
+// });
+//
+// diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
+// let found_hunks = diff.update(cx, |diff, cx| {
+// diff.hunks_intersecting_range(
+// Anchor::min_max_range_for_buffer(diff.buffer_id),
+// &working_copy,
+// cx,
+// )
+// .collect::<Vec<_>>()
+// });
+// assert_eq!(hunks.len(), found_hunks.len());
+//
+// for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
+// assert_eq!(
+// expected_hunk.buffer_range.to_point(&working_copy),
+// found_hunk.buffer_range.to_point(&working_copy)
+// );
+// assert_eq!(
+// expected_hunk.diff_base_byte_range,
+// found_hunk.diff_base_byte_range
+// );
+// assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
+// }
+// hunks = found_hunks;
+// }
+// }
+//
+// #[gpui::test]
+// async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
+// let base_text = "
+// zero
+// one
+// two
+// three
+// four
+// five
+// six
+// seven
+// eight
+// "
+// .unindent();
+// let buffer_text = "
+// zero
+// ONE
+// two
+// NINE
+// five
+// seven
+// "
+// .unindent();
+//
+// // zero
+// // - one
+// // + ONE
+// // two
+// // - three
+// // - four
+// // + NINE
+// // five
+// // - six
+// // seven
+// // + eight
+//
+// let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+// let buffer_snapshot = buffer.snapshot();
+// let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
+// let expected_results = [
+// // don't format me
+// (0, 0),
+// (1, 2),
+// (2, 2),
+// (3, 5),
+// (4, 5),
+// (5, 7),
+// (6, 9),
+// ];
+// for (buffer_row, expected) in expected_results {
+// assert_eq!(
+// diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
+// expected,
+// "{buffer_row}"
+// );
+// }
+// }
+// }
+//