1use futures::channel::oneshot;
2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
3use gpui::{
4 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
5 TaskLabel,
6};
7use language::{Language, LanguageRegistry};
8use rope::Rope;
9use std::{
10 cmp::Ordering,
11 future::Future,
12 iter,
13 ops::Range,
14 sync::{Arc, LazyLock},
15};
16use sum_tree::SumTree;
17use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
18use util::ResultExt;
19
20pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
21
22pub struct BufferDiff {
23 pub buffer_id: BufferId,
24 inner: BufferDiffInner,
25 secondary_diff: Option<Entity<BufferDiff>>,
26}
27
28#[derive(Clone, Debug)]
29pub struct BufferDiffSnapshot {
30 inner: BufferDiffInner,
31 secondary_diff: Option<Box<BufferDiffSnapshot>>,
32}
33
34#[derive(Clone)]
35struct BufferDiffInner {
36 hunks: SumTree<InternalDiffHunk>,
37 pending_hunks: SumTree<PendingHunk>,
38 base_text: language::BufferSnapshot,
39 base_text_exists: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct DiffHunkStatus {
44 pub kind: DiffHunkStatusKind,
45 pub secondary: DiffHunkSecondaryStatus,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum DiffHunkStatusKind {
50 Added,
51 Modified,
52 Deleted,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum DiffHunkSecondaryStatus {
57 HasSecondaryHunk,
58 OverlapsWithSecondaryHunk,
59 NoSecondaryHunk,
60 SecondaryHunkAdditionPending,
61 SecondaryHunkRemovalPending,
62}
63
64/// A diff hunk resolved to rows in the buffer.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct DiffHunk {
67 /// The buffer range as points.
68 pub range: Range<Point>,
69 /// The range in the buffer to which this hunk corresponds.
70 pub buffer_range: Range<Anchor>,
71 /// The range in the buffer's diff base text to which this hunk corresponds.
72 pub diff_base_byte_range: Range<usize>,
73 pub secondary_status: DiffHunkSecondaryStatus,
74}
75
76/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
77#[derive(Debug, Clone, PartialEq, Eq)]
78struct InternalDiffHunk {
79 buffer_range: Range<Anchor>,
80 diff_base_byte_range: Range<usize>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84struct PendingHunk {
85 buffer_range: Range<Anchor>,
86 diff_base_byte_range: Range<usize>,
87 buffer_version: clock::Global,
88 new_status: DiffHunkSecondaryStatus,
89}
90
91#[derive(Debug, Clone)]
92pub struct DiffHunkSummary {
93 buffer_range: Range<Anchor>,
94}
95
96impl sum_tree::Item for InternalDiffHunk {
97 type Summary = DiffHunkSummary;
98
99 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
100 DiffHunkSummary {
101 buffer_range: self.buffer_range.clone(),
102 }
103 }
104}
105
106impl sum_tree::Item for PendingHunk {
107 type Summary = DiffHunkSummary;
108
109 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
110 DiffHunkSummary {
111 buffer_range: self.buffer_range.clone(),
112 }
113 }
114}
115
116impl sum_tree::Summary for DiffHunkSummary {
117 type Context<'a> = &'a text::BufferSnapshot;
118
119 fn zero(_cx: Self::Context<'_>) -> Self {
120 DiffHunkSummary {
121 buffer_range: Anchor::MIN..Anchor::MIN,
122 }
123 }
124
125 fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
126 self.buffer_range.start = *self
127 .buffer_range
128 .start
129 .min(&other.buffer_range.start, buffer);
130 self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
131 }
132}
133
134impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
135 fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
136 if self
137 .cmp(&cursor_location.buffer_range.start, buffer)
138 .is_lt()
139 {
140 Ordering::Less
141 } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
142 Ordering::Greater
143 } else {
144 Ordering::Equal
145 }
146 }
147}
148
149impl std::fmt::Debug for BufferDiffInner {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("BufferDiffSnapshot")
152 .field("hunks", &self.hunks)
153 .finish()
154 }
155}
156
157impl BufferDiffSnapshot {
158 fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot {
159 BufferDiffSnapshot {
160 inner: BufferDiffInner {
161 base_text: language::Buffer::build_empty_snapshot(cx),
162 hunks: SumTree::new(buffer),
163 pending_hunks: SumTree::new(buffer),
164 base_text_exists: false,
165 },
166 secondary_diff: None,
167 }
168 }
169
170 fn unchanged(
171 buffer: &text::BufferSnapshot,
172 base_text: language::BufferSnapshot,
173 ) -> BufferDiffSnapshot {
174 debug_assert_eq!(buffer.text(), base_text.text());
175 BufferDiffSnapshot {
176 inner: BufferDiffInner {
177 base_text,
178 hunks: SumTree::new(buffer),
179 pending_hunks: SumTree::new(buffer),
180 base_text_exists: false,
181 },
182 secondary_diff: None,
183 }
184 }
185
186 fn new_with_base_text(
187 buffer: text::BufferSnapshot,
188 base_text: Option<Arc<String>>,
189 language: Option<Arc<Language>>,
190 language_registry: Option<Arc<LanguageRegistry>>,
191 cx: &mut App,
192 ) -> impl Future<Output = Self> + use<> {
193 let base_text_pair;
194 let base_text_exists;
195 let base_text_snapshot;
196 if let Some(text) = &base_text {
197 let base_text_rope = Rope::from_str(text.as_str(), cx.background_executor());
198 base_text_pair = Some((text.clone(), base_text_rope.clone()));
199 let snapshot =
200 language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx);
201 base_text_snapshot = cx.background_spawn(snapshot);
202 base_text_exists = true;
203 } else {
204 base_text_pair = None;
205 base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx));
206 base_text_exists = false;
207 };
208
209 let hunks = cx
210 .background_executor()
211 .spawn_labeled(*CALCULATE_DIFF_TASK, {
212 let buffer = buffer.clone();
213 async move { compute_hunks(base_text_pair, buffer) }
214 });
215
216 async move {
217 let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
218 Self {
219 inner: BufferDiffInner {
220 base_text,
221 hunks,
222 base_text_exists,
223 pending_hunks: SumTree::new(&buffer),
224 },
225 secondary_diff: None,
226 }
227 }
228 }
229
230 pub fn new_with_base_buffer(
231 buffer: text::BufferSnapshot,
232 base_text: Option<Arc<String>>,
233 base_text_snapshot: language::BufferSnapshot,
234 cx: &App,
235 ) -> impl Future<Output = Self> + use<> {
236 let base_text_exists = base_text.is_some();
237 let base_text_pair = base_text.map(|text| {
238 debug_assert_eq!(&*text, &base_text_snapshot.text());
239 (text, base_text_snapshot.as_rope().clone())
240 });
241 cx.background_executor()
242 .spawn_labeled(*CALCULATE_DIFF_TASK, async move {
243 Self {
244 inner: BufferDiffInner {
245 base_text: base_text_snapshot,
246 pending_hunks: SumTree::new(&buffer),
247 hunks: compute_hunks(base_text_pair, buffer),
248 base_text_exists,
249 },
250 secondary_diff: None,
251 }
252 })
253 }
254
255 #[cfg(test)]
256 fn new_sync(
257 buffer: text::BufferSnapshot,
258 diff_base: String,
259 cx: &mut gpui::TestAppContext,
260 ) -> BufferDiffSnapshot {
261 cx.executor().block(cx.update(|cx| {
262 Self::new_with_base_text(buffer, Some(Arc::new(diff_base)), None, None, cx)
263 }))
264 }
265
266 pub fn is_empty(&self) -> bool {
267 self.inner.hunks.is_empty()
268 }
269
270 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
271 self.secondary_diff.as_deref()
272 }
273
274 pub fn hunks_intersecting_range<'a>(
275 &'a self,
276 range: Range<Anchor>,
277 buffer: &'a text::BufferSnapshot,
278 ) -> impl 'a + Iterator<Item = DiffHunk> {
279 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
280 self.inner
281 .hunks_intersecting_range(range, buffer, unstaged_counterpart)
282 }
283
284 pub fn hunks_intersecting_range_rev<'a>(
285 &'a self,
286 range: Range<Anchor>,
287 buffer: &'a text::BufferSnapshot,
288 ) -> impl 'a + Iterator<Item = DiffHunk> {
289 self.inner.hunks_intersecting_range_rev(range, buffer)
290 }
291
292 pub fn base_text(&self) -> &language::BufferSnapshot {
293 &self.inner.base_text
294 }
295
296 pub fn base_texts_eq(&self, other: &Self) -> bool {
297 if self.inner.base_text_exists != other.inner.base_text_exists {
298 return false;
299 }
300 let left = &self.inner.base_text;
301 let right = &other.inner.base_text;
302 let (old_id, old_empty) = (left.remote_id(), left.is_empty());
303 let (new_id, new_empty) = (right.remote_id(), right.is_empty());
304 new_id == old_id || (new_empty && old_empty)
305 }
306}
307
308impl BufferDiffInner {
309 /// Returns the new index text and new pending hunks.
310 fn stage_or_unstage_hunks_impl(
311 &mut self,
312 unstaged_diff: &Self,
313 stage: bool,
314 hunks: &[DiffHunk],
315 buffer: &text::BufferSnapshot,
316 file_exists: bool,
317 cx: &BackgroundExecutor,
318 ) -> Option<Rope> {
319 let head_text = self
320 .base_text_exists
321 .then(|| self.base_text.as_rope().clone());
322 let index_text = unstaged_diff
323 .base_text_exists
324 .then(|| unstaged_diff.base_text.as_rope().clone());
325
326 // If the file doesn't exist in either HEAD or the index, then the
327 // entire file must be either created or deleted in the index.
328 let (index_text, head_text) = match (index_text, head_text) {
329 (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
330 (index_text, head_text) => {
331 let (new_index_text, new_status) = if stage {
332 log::debug!("stage all");
333 (
334 file_exists.then(|| buffer.as_rope().clone()),
335 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
336 )
337 } else {
338 log::debug!("unstage all");
339 (
340 head_text,
341 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
342 )
343 };
344
345 let hunk = PendingHunk {
346 buffer_range: Anchor::MIN..Anchor::MAX,
347 diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
348 buffer_version: buffer.version().clone(),
349 new_status,
350 };
351 self.pending_hunks = SumTree::from_item(hunk, buffer);
352 return new_index_text;
353 }
354 };
355
356 let mut pending_hunks = SumTree::new(buffer);
357 let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
358
359 // first, merge new hunks into pending_hunks
360 for DiffHunk {
361 buffer_range,
362 diff_base_byte_range,
363 secondary_status,
364 ..
365 } in hunks.iter().cloned()
366 {
367 let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
368 pending_hunks.append(preceding_pending_hunks, buffer);
369
370 // Skip all overlapping or adjacent old pending hunks
371 while old_pending_hunks.item().is_some_and(|old_hunk| {
372 old_hunk
373 .buffer_range
374 .start
375 .cmp(&buffer_range.end, buffer)
376 .is_le()
377 }) {
378 old_pending_hunks.next();
379 }
380
381 if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
382 || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
383 {
384 continue;
385 }
386
387 pending_hunks.push(
388 PendingHunk {
389 buffer_range,
390 diff_base_byte_range,
391 buffer_version: buffer.version().clone(),
392 new_status: if stage {
393 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
394 } else {
395 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
396 },
397 },
398 buffer,
399 );
400 }
401 // append the remainder
402 pending_hunks.append(old_pending_hunks.suffix(), buffer);
403
404 let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
405 unstaged_hunk_cursor.next();
406
407 // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
408 let mut prev_unstaged_hunk_buffer_end = 0;
409 let mut prev_unstaged_hunk_base_text_end = 0;
410 let mut edits = Vec::<(Range<usize>, String)>::new();
411 let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
412 while let Some(PendingHunk {
413 buffer_range,
414 diff_base_byte_range,
415 new_status,
416 ..
417 }) = pending_hunks_iter.next()
418 {
419 // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
420 let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
421
422 if let Some(unstaged_hunk) = skipped_unstaged.last() {
423 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
424 prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
425 }
426
427 // Find where this hunk is in the index if it doesn't overlap
428 let mut buffer_offset_range = buffer_range.to_offset(buffer);
429 let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
430 let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
431
432 loop {
433 // Merge this hunk with any overlapping unstaged hunks.
434 if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
435 let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
436 if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
437 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
438 prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
439
440 index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
441 buffer_offset_range.start = buffer_offset_range
442 .start
443 .min(unstaged_hunk_offset_range.start);
444 buffer_offset_range.end =
445 buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
446
447 unstaged_hunk_cursor.next();
448 continue;
449 }
450 }
451
452 // If any unstaged hunks were merged, then subsequent pending hunks may
453 // now overlap this hunk. Merge them.
454 if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
455 let next_pending_hunk_offset_range =
456 next_pending_hunk.buffer_range.to_offset(buffer);
457 if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
458 buffer_offset_range.end = next_pending_hunk_offset_range.end;
459 pending_hunks_iter.next();
460 continue;
461 }
462 }
463
464 break;
465 }
466
467 let end_overshoot = buffer_offset_range
468 .end
469 .saturating_sub(prev_unstaged_hunk_buffer_end);
470 let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
471 let index_byte_range = index_start..index_end;
472
473 let replacement_text = match new_status {
474 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
475 log::debug!("staging hunk {:?}", buffer_offset_range);
476 buffer
477 .text_for_range(buffer_offset_range)
478 .collect::<String>()
479 }
480 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
481 log::debug!("unstaging hunk {:?}", buffer_offset_range);
482 head_text
483 .chunks_in_range(diff_base_byte_range.clone())
484 .collect::<String>()
485 }
486 _ => {
487 debug_assert!(false);
488 continue;
489 }
490 };
491
492 edits.push((index_byte_range, replacement_text));
493 }
494 drop(pending_hunks_iter);
495 drop(old_pending_hunks);
496 self.pending_hunks = pending_hunks;
497
498 #[cfg(debug_assertions)] // invariants: non-overlapping and sorted
499 {
500 for window in edits.windows(2) {
501 let (range_a, range_b) = (&window[0].0, &window[1].0);
502 debug_assert!(range_a.end < range_b.start);
503 }
504 }
505
506 let mut new_index_text = Rope::new();
507 let mut index_cursor = index_text.cursor(0);
508
509 for (old_range, replacement_text) in edits {
510 new_index_text.append(index_cursor.slice(old_range.start));
511 index_cursor.seek_forward(old_range.end);
512 new_index_text.push(&replacement_text, cx);
513 }
514 new_index_text.append(index_cursor.suffix());
515 Some(new_index_text)
516 }
517
518 fn hunks_intersecting_range<'a>(
519 &'a self,
520 range: Range<Anchor>,
521 buffer: &'a text::BufferSnapshot,
522 secondary: Option<&'a Self>,
523 ) -> impl 'a + Iterator<Item = DiffHunk> {
524 let range = range.to_offset(buffer);
525
526 let mut cursor = self
527 .hunks
528 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
529 let summary_range = summary.buffer_range.to_offset(buffer);
530 let before_start = summary_range.end < range.start;
531 let after_end = summary_range.start > range.end;
532 !before_start && !after_end
533 });
534
535 let anchor_iter = iter::from_fn(move || {
536 cursor.next();
537 cursor.item()
538 })
539 .flat_map(move |hunk| {
540 [
541 (
542 &hunk.buffer_range.start,
543 (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
544 ),
545 (
546 &hunk.buffer_range.end,
547 (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
548 ),
549 ]
550 });
551
552 let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
553 pending_hunks_cursor.next();
554
555 let mut secondary_cursor = None;
556 if let Some(secondary) = secondary.as_ref() {
557 let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
558 cursor.next();
559 secondary_cursor = Some(cursor);
560 }
561
562 let max_point = buffer.max_point();
563 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
564 iter::from_fn(move || {
565 loop {
566 let (start_point, (start_anchor, start_base)) = summaries.next()?;
567 let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
568
569 if !start_anchor.is_valid(buffer) {
570 continue;
571 }
572
573 if end_point.column > 0 && end_point < max_point {
574 end_point.row += 1;
575 end_point.column = 0;
576 end_anchor = buffer.anchor_before(end_point);
577 }
578
579 let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
580
581 let mut has_pending = false;
582 if start_anchor
583 .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
584 .is_gt()
585 {
586 pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
587 }
588
589 if let Some(pending_hunk) = pending_hunks_cursor.item() {
590 let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
591 if pending_range.end.column > 0 {
592 pending_range.end.row += 1;
593 pending_range.end.column = 0;
594 }
595
596 if pending_range == (start_point..end_point)
597 && !buffer.has_edits_since_in_range(
598 &pending_hunk.buffer_version,
599 start_anchor..end_anchor,
600 )
601 {
602 has_pending = true;
603 secondary_status = pending_hunk.new_status;
604 }
605 }
606
607 if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
608 if start_anchor
609 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
610 .is_gt()
611 {
612 secondary_cursor.seek_forward(&start_anchor, Bias::Left);
613 }
614
615 if let Some(secondary_hunk) = secondary_cursor.item() {
616 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
617 if secondary_range.end.column > 0 {
618 secondary_range.end.row += 1;
619 secondary_range.end.column = 0;
620 }
621 if secondary_range.is_empty()
622 && secondary_hunk.diff_base_byte_range.is_empty()
623 {
624 // ignore
625 } else if secondary_range == (start_point..end_point) {
626 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
627 } else if secondary_range.start <= end_point {
628 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
629 }
630 }
631 }
632
633 return Some(DiffHunk {
634 range: start_point..end_point,
635 diff_base_byte_range: start_base..end_base,
636 buffer_range: start_anchor..end_anchor,
637 secondary_status,
638 });
639 }
640 })
641 }
642
643 fn hunks_intersecting_range_rev<'a>(
644 &'a self,
645 range: Range<Anchor>,
646 buffer: &'a text::BufferSnapshot,
647 ) -> impl 'a + Iterator<Item = DiffHunk> {
648 let mut cursor = self
649 .hunks
650 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
651 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
652 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
653 !before_start && !after_end
654 });
655
656 iter::from_fn(move || {
657 cursor.prev();
658
659 let hunk = cursor.item()?;
660 let range = hunk.buffer_range.to_point(buffer);
661
662 Some(DiffHunk {
663 range,
664 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
665 buffer_range: hunk.buffer_range.clone(),
666 // The secondary status is not used by callers of this method.
667 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
668 })
669 })
670 }
671
672 fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
673 let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
674 let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
675 old_cursor.next();
676 new_cursor.next();
677 let mut start = None;
678 let mut end = None;
679
680 loop {
681 match (new_cursor.item(), old_cursor.item()) {
682 (Some(new_hunk), Some(old_hunk)) => {
683 match new_hunk
684 .buffer_range
685 .start
686 .cmp(&old_hunk.buffer_range.start, new_snapshot)
687 {
688 Ordering::Less => {
689 start.get_or_insert(new_hunk.buffer_range.start);
690 end.replace(new_hunk.buffer_range.end);
691 new_cursor.next();
692 }
693 Ordering::Equal => {
694 if new_hunk != old_hunk {
695 start.get_or_insert(new_hunk.buffer_range.start);
696 if old_hunk
697 .buffer_range
698 .end
699 .cmp(&new_hunk.buffer_range.end, new_snapshot)
700 .is_ge()
701 {
702 end.replace(old_hunk.buffer_range.end);
703 } else {
704 end.replace(new_hunk.buffer_range.end);
705 }
706 }
707
708 new_cursor.next();
709 old_cursor.next();
710 }
711 Ordering::Greater => {
712 start.get_or_insert(old_hunk.buffer_range.start);
713 end.replace(old_hunk.buffer_range.end);
714 old_cursor.next();
715 }
716 }
717 }
718 (Some(new_hunk), None) => {
719 start.get_or_insert(new_hunk.buffer_range.start);
720 end.replace(new_hunk.buffer_range.end);
721 new_cursor.next();
722 }
723 (None, Some(old_hunk)) => {
724 start.get_or_insert(old_hunk.buffer_range.start);
725 end.replace(old_hunk.buffer_range.end);
726 old_cursor.next();
727 }
728 (None, None) => break,
729 }
730 }
731
732 start.zip(end).map(|(start, end)| start..end)
733 }
734}
735
736fn compute_hunks(
737 diff_base: Option<(Arc<String>, Rope)>,
738 buffer: text::BufferSnapshot,
739) -> SumTree<InternalDiffHunk> {
740 let mut tree = SumTree::new(&buffer);
741
742 if let Some((diff_base, diff_base_rope)) = diff_base {
743 let buffer_text = buffer.as_rope().to_string();
744
745 let mut options = GitOptions::default();
746 options.context_lines(0);
747 let patch = GitPatch::from_buffers(
748 diff_base.as_bytes(),
749 None,
750 buffer_text.as_bytes(),
751 None,
752 Some(&mut options),
753 )
754 .log_err();
755
756 // A common case in Zed is that the empty buffer is represented as just a newline,
757 // but if we just compute a naive diff you get a "preserved" line in the middle,
758 // which is a bit odd.
759 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
760 tree.push(
761 InternalDiffHunk {
762 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
763 diff_base_byte_range: 0..diff_base.len() - 1,
764 },
765 &buffer,
766 );
767 return tree;
768 }
769
770 if let Some(patch) = patch {
771 let mut divergence = 0;
772 for hunk_index in 0..patch.num_hunks() {
773 let hunk = process_patch_hunk(
774 &patch,
775 hunk_index,
776 &diff_base_rope,
777 &buffer,
778 &mut divergence,
779 );
780 tree.push(hunk, &buffer);
781 }
782 }
783 } else {
784 tree.push(
785 InternalDiffHunk {
786 buffer_range: Anchor::MIN..Anchor::MAX,
787 diff_base_byte_range: 0..0,
788 },
789 &buffer,
790 );
791 }
792
793 tree
794}
795
796fn process_patch_hunk(
797 patch: &GitPatch<'_>,
798 hunk_index: usize,
799 diff_base: &Rope,
800 buffer: &text::BufferSnapshot,
801 buffer_row_divergence: &mut i64,
802) -> InternalDiffHunk {
803 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
804 assert!(line_item_count > 0);
805
806 let mut first_deletion_buffer_row: Option<u32> = None;
807 let mut buffer_row_range: Option<Range<u32>> = None;
808 let mut diff_base_byte_range: Option<Range<usize>> = None;
809 let mut first_addition_old_row: Option<u32> = None;
810
811 for line_index in 0..line_item_count {
812 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
813 let kind = line.origin_value();
814 let content_offset = line.content_offset() as isize;
815 let content_len = line.content().len() as isize;
816 match kind {
817 GitDiffLineType::Addition => {
818 if first_addition_old_row.is_none() {
819 first_addition_old_row = Some(
820 (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
821 );
822 }
823 *buffer_row_divergence += 1;
824 let row = line.new_lineno().unwrap().saturating_sub(1);
825
826 match &mut buffer_row_range {
827 Some(Range { end, .. }) => *end = row + 1,
828 None => buffer_row_range = Some(row..row + 1),
829 }
830 }
831 GitDiffLineType::Deletion => {
832 let end = content_offset + content_len;
833
834 match &mut diff_base_byte_range {
835 Some(head_byte_range) => head_byte_range.end = end as usize,
836 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
837 }
838
839 if first_deletion_buffer_row.is_none() {
840 let old_row = line.old_lineno().unwrap().saturating_sub(1);
841 let row = old_row as i64 + *buffer_row_divergence;
842 first_deletion_buffer_row = Some(row as u32);
843 }
844
845 *buffer_row_divergence -= 1;
846 }
847 _ => {}
848 }
849 }
850
851 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
852 // Pure deletion hunk without addition.
853 let row = first_deletion_buffer_row.unwrap();
854 row..row
855 });
856 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
857 // Pure addition hunk without deletion.
858 let row = first_addition_old_row.unwrap();
859 let offset = diff_base.point_to_offset(Point::new(row, 0));
860 offset..offset
861 });
862
863 let start = Point::new(buffer_row_range.start, 0);
864 let end = Point::new(buffer_row_range.end, 0);
865 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
866 InternalDiffHunk {
867 buffer_range,
868 diff_base_byte_range,
869 }
870}
871
872impl std::fmt::Debug for BufferDiff {
873 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874 f.debug_struct("BufferChangeSet")
875 .field("buffer_id", &self.buffer_id)
876 .field("snapshot", &self.inner)
877 .finish()
878 }
879}
880
881#[derive(Clone, Debug)]
882pub enum BufferDiffEvent {
883 DiffChanged {
884 changed_range: Option<Range<text::Anchor>>,
885 },
886 LanguageChanged,
887 HunksStagedOrUnstaged(Option<Rope>),
888}
889
890impl EventEmitter<BufferDiffEvent> for BufferDiff {}
891
892impl BufferDiff {
893 pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
894 BufferDiff {
895 buffer_id: buffer.remote_id(),
896 inner: BufferDiffSnapshot::empty(buffer, cx).inner,
897 secondary_diff: None,
898 }
899 }
900
901 pub fn new_unchanged(
902 buffer: &text::BufferSnapshot,
903 base_text: language::BufferSnapshot,
904 ) -> Self {
905 debug_assert_eq!(buffer.text(), base_text.text());
906 BufferDiff {
907 buffer_id: buffer.remote_id(),
908 inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner,
909 secondary_diff: None,
910 }
911 }
912
913 #[cfg(any(test, feature = "test-support"))]
914 pub fn new_with_base_text(
915 base_text: &str,
916 buffer: &Entity<language::Buffer>,
917 cx: &mut App,
918 ) -> Self {
919 let mut base_text = base_text.to_owned();
920 text::LineEnding::normalize(&mut base_text);
921 let snapshot = BufferDiffSnapshot::new_with_base_text(
922 buffer.read(cx).text_snapshot(),
923 Some(base_text.into()),
924 None,
925 None,
926 cx,
927 );
928 let snapshot = cx.background_executor().block(snapshot);
929 Self {
930 buffer_id: buffer.read(cx).remote_id(),
931 inner: snapshot.inner,
932 secondary_diff: None,
933 }
934 }
935
936 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
937 self.secondary_diff = Some(diff);
938 }
939
940 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
941 self.secondary_diff.clone()
942 }
943
944 pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
945 if self.secondary_diff.is_some() {
946 self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
947 buffer_range: Anchor::MIN..Anchor::MIN,
948 });
949 cx.emit(BufferDiffEvent::DiffChanged {
950 changed_range: Some(Anchor::MIN..Anchor::MAX),
951 });
952 }
953 }
954
955 pub fn stage_or_unstage_hunks(
956 &mut self,
957 stage: bool,
958 hunks: &[DiffHunk],
959 buffer: &text::BufferSnapshot,
960 file_exists: bool,
961 cx: &mut Context<Self>,
962 ) -> Option<Rope> {
963 let new_index_text = self.inner.stage_or_unstage_hunks_impl(
964 &self.secondary_diff.as_ref()?.read(cx).inner,
965 stage,
966 hunks,
967 buffer,
968 file_exists,
969 cx.background_executor(),
970 );
971
972 cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
973 new_index_text.clone(),
974 ));
975 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
976 let changed_range = first.buffer_range.start..last.buffer_range.end;
977 cx.emit(BufferDiffEvent::DiffChanged {
978 changed_range: Some(changed_range),
979 });
980 }
981 new_index_text
982 }
983
984 pub fn range_to_hunk_range(
985 &self,
986 range: Range<Anchor>,
987 buffer: &text::BufferSnapshot,
988 cx: &App,
989 ) -> Option<Range<Anchor>> {
990 let start = self
991 .hunks_intersecting_range(range.clone(), buffer, cx)
992 .next()?
993 .buffer_range
994 .start;
995 let end = self
996 .hunks_intersecting_range_rev(range, buffer)
997 .next()?
998 .buffer_range
999 .end;
1000 Some(start..end)
1001 }
1002
1003 pub async fn update_diff(
1004 this: Entity<BufferDiff>,
1005 buffer: text::BufferSnapshot,
1006 base_text: Option<Arc<String>>,
1007 base_text_changed: bool,
1008 language_changed: bool,
1009 language: Option<Arc<Language>>,
1010 language_registry: Option<Arc<LanguageRegistry>>,
1011 cx: &mut AsyncApp,
1012 ) -> anyhow::Result<BufferDiffSnapshot> {
1013 Ok(if base_text_changed || language_changed {
1014 cx.update(|cx| {
1015 BufferDiffSnapshot::new_with_base_text(
1016 buffer.clone(),
1017 base_text,
1018 language.clone(),
1019 language_registry.clone(),
1020 cx,
1021 )
1022 })?
1023 .await
1024 } else {
1025 this.read_with(cx, |this, cx| {
1026 BufferDiffSnapshot::new_with_base_buffer(
1027 buffer.clone(),
1028 base_text,
1029 this.base_text().clone(),
1030 cx,
1031 )
1032 })?
1033 .await
1034 })
1035 }
1036
1037 pub fn language_changed(&mut self, cx: &mut Context<Self>) {
1038 cx.emit(BufferDiffEvent::LanguageChanged);
1039 }
1040
1041 pub fn set_snapshot(
1042 &mut self,
1043 new_snapshot: BufferDiffSnapshot,
1044 buffer: &text::BufferSnapshot,
1045 cx: &mut Context<Self>,
1046 ) -> Option<Range<Anchor>> {
1047 self.set_snapshot_with_secondary(new_snapshot, buffer, None, false, cx)
1048 }
1049
1050 pub fn set_snapshot_with_secondary(
1051 &mut self,
1052 new_snapshot: BufferDiffSnapshot,
1053 buffer: &text::BufferSnapshot,
1054 secondary_diff_change: Option<Range<Anchor>>,
1055 clear_pending_hunks: bool,
1056 cx: &mut Context<Self>,
1057 ) -> Option<Range<Anchor>> {
1058 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1059
1060 let state = &mut self.inner;
1061 let new_state = new_snapshot.inner;
1062 let (base_text_changed, mut changed_range) =
1063 match (state.base_text_exists, new_state.base_text_exists) {
1064 (false, false) => (true, None),
1065 (true, true)
1066 if state.base_text.remote_id() == new_state.base_text.remote_id()
1067 && state.base_text.syntax_update_count()
1068 == new_state.base_text.syntax_update_count() =>
1069 {
1070 (false, new_state.compare(state, buffer))
1071 }
1072 _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
1073 };
1074
1075 if let Some(secondary_changed_range) = secondary_diff_change
1076 && let Some(secondary_hunk_range) =
1077 self.range_to_hunk_range(secondary_changed_range, buffer, cx)
1078 {
1079 if let Some(range) = &mut changed_range {
1080 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1081 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1082 } else {
1083 changed_range = Some(secondary_hunk_range);
1084 }
1085 }
1086
1087 let state = &mut self.inner;
1088 state.base_text_exists = new_state.base_text_exists;
1089 state.base_text = new_state.base_text;
1090 state.hunks = new_state.hunks;
1091 if base_text_changed || clear_pending_hunks {
1092 if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1093 {
1094 if let Some(range) = &mut changed_range {
1095 range.start = *range.start.min(&first.buffer_range.start, buffer);
1096 range.end = *range.end.max(&last.buffer_range.end, buffer);
1097 } else {
1098 changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1099 }
1100 }
1101 state.pending_hunks = SumTree::new(buffer);
1102 }
1103
1104 cx.emit(BufferDiffEvent::DiffChanged {
1105 changed_range: changed_range.clone(),
1106 });
1107 changed_range
1108 }
1109
1110 pub fn base_text(&self) -> &language::BufferSnapshot {
1111 &self.inner.base_text
1112 }
1113
1114 pub fn base_text_exists(&self) -> bool {
1115 self.inner.base_text_exists
1116 }
1117
1118 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1119 BufferDiffSnapshot {
1120 inner: self.inner.clone(),
1121 secondary_diff: self
1122 .secondary_diff
1123 .as_ref()
1124 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
1125 }
1126 }
1127
1128 pub fn hunks<'a>(
1129 &'a self,
1130 buffer_snapshot: &'a text::BufferSnapshot,
1131 cx: &'a App,
1132 ) -> impl 'a + Iterator<Item = DiffHunk> {
1133 self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
1134 }
1135
1136 pub fn hunks_intersecting_range<'a>(
1137 &'a self,
1138 range: Range<text::Anchor>,
1139 buffer_snapshot: &'a text::BufferSnapshot,
1140 cx: &'a App,
1141 ) -> impl 'a + Iterator<Item = DiffHunk> {
1142 let unstaged_counterpart = self
1143 .secondary_diff
1144 .as_ref()
1145 .map(|diff| &diff.read(cx).inner);
1146 self.inner
1147 .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
1148 }
1149
1150 pub fn hunks_intersecting_range_rev<'a>(
1151 &'a self,
1152 range: Range<text::Anchor>,
1153 buffer_snapshot: &'a text::BufferSnapshot,
1154 ) -> impl 'a + Iterator<Item = DiffHunk> {
1155 self.inner
1156 .hunks_intersecting_range_rev(range, buffer_snapshot)
1157 }
1158
1159 pub fn hunks_in_row_range<'a>(
1160 &'a self,
1161 range: Range<u32>,
1162 buffer: &'a text::BufferSnapshot,
1163 cx: &'a App,
1164 ) -> impl 'a + Iterator<Item = DiffHunk> {
1165 let start = buffer.anchor_before(Point::new(range.start, 0));
1166 let end = buffer.anchor_after(Point::new(range.end, 0));
1167 self.hunks_intersecting_range(start..end, buffer, cx)
1168 }
1169
1170 /// Used in cases where the change set isn't derived from git.
1171 pub fn set_base_text(
1172 &mut self,
1173 base_text: Option<Arc<String>>,
1174 language: Option<Arc<Language>>,
1175 language_registry: Option<Arc<LanguageRegistry>>,
1176 buffer: text::BufferSnapshot,
1177 cx: &mut Context<Self>,
1178 ) -> oneshot::Receiver<()> {
1179 let (tx, rx) = oneshot::channel();
1180 let this = cx.weak_entity();
1181
1182 let snapshot = BufferDiffSnapshot::new_with_base_text(
1183 buffer.clone(),
1184 base_text,
1185 language,
1186 language_registry,
1187 cx,
1188 );
1189 let complete_on_drop = util::defer(|| {
1190 tx.send(()).ok();
1191 });
1192 cx.spawn(async move |_, cx| {
1193 let snapshot = snapshot.await;
1194 let Some(this) = this.upgrade() else {
1195 return;
1196 };
1197 this.update(cx, |this, cx| {
1198 this.set_snapshot(snapshot, &buffer, cx);
1199 })
1200 .log_err();
1201 drop(complete_on_drop)
1202 })
1203 .detach();
1204 rx
1205 }
1206
1207 pub fn base_text_string(&self) -> Option<String> {
1208 self.inner
1209 .base_text_exists
1210 .then(|| self.inner.base_text.text())
1211 }
1212
1213 #[cfg(any(test, feature = "test-support"))]
1214 pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
1215 let base_text = self.base_text_string().map(Arc::new);
1216 let snapshot = BufferDiffSnapshot::new_with_base_buffer(
1217 buffer.clone(),
1218 base_text,
1219 self.inner.base_text.clone(),
1220 cx,
1221 );
1222 let snapshot = cx.background_executor().block(snapshot);
1223 self.set_snapshot(snapshot, &buffer, cx);
1224 }
1225}
1226
1227impl DiffHunk {
1228 pub fn is_created_file(&self) -> bool {
1229 self.diff_base_byte_range == (0..0) && self.buffer_range == (Anchor::MIN..Anchor::MAX)
1230 }
1231
1232 pub fn status(&self) -> DiffHunkStatus {
1233 let kind = if self.buffer_range.start == self.buffer_range.end {
1234 DiffHunkStatusKind::Deleted
1235 } else if self.diff_base_byte_range.is_empty() {
1236 DiffHunkStatusKind::Added
1237 } else {
1238 DiffHunkStatusKind::Modified
1239 };
1240 DiffHunkStatus {
1241 kind,
1242 secondary: self.secondary_status,
1243 }
1244 }
1245}
1246
1247impl DiffHunkStatus {
1248 pub fn has_secondary_hunk(&self) -> bool {
1249 matches!(
1250 self.secondary,
1251 DiffHunkSecondaryStatus::HasSecondaryHunk
1252 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1253 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
1254 )
1255 }
1256
1257 pub fn is_pending(&self) -> bool {
1258 matches!(
1259 self.secondary,
1260 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1261 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1262 )
1263 }
1264
1265 pub fn is_deleted(&self) -> bool {
1266 self.kind == DiffHunkStatusKind::Deleted
1267 }
1268
1269 pub fn is_added(&self) -> bool {
1270 self.kind == DiffHunkStatusKind::Added
1271 }
1272
1273 pub fn is_modified(&self) -> bool {
1274 self.kind == DiffHunkStatusKind::Modified
1275 }
1276
1277 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
1278 Self {
1279 kind: DiffHunkStatusKind::Added,
1280 secondary,
1281 }
1282 }
1283
1284 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
1285 Self {
1286 kind: DiffHunkStatusKind::Modified,
1287 secondary,
1288 }
1289 }
1290
1291 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
1292 Self {
1293 kind: DiffHunkStatusKind::Deleted,
1294 secondary,
1295 }
1296 }
1297
1298 pub fn deleted_none() -> Self {
1299 Self {
1300 kind: DiffHunkStatusKind::Deleted,
1301 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1302 }
1303 }
1304
1305 pub fn added_none() -> Self {
1306 Self {
1307 kind: DiffHunkStatusKind::Added,
1308 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1309 }
1310 }
1311
1312 pub fn modified_none() -> Self {
1313 Self {
1314 kind: DiffHunkStatusKind::Modified,
1315 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1316 }
1317 }
1318}
1319
1320#[cfg(any(test, feature = "test-support"))]
1321#[track_caller]
1322pub fn assert_hunks<ExpectedText, HunkIter>(
1323 diff_hunks: HunkIter,
1324 buffer: &text::BufferSnapshot,
1325 diff_base: &str,
1326 // Line range, deleted, added, status
1327 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
1328) where
1329 HunkIter: Iterator<Item = DiffHunk>,
1330 ExpectedText: AsRef<str>,
1331{
1332 let actual_hunks = diff_hunks
1333 .map(|hunk| {
1334 (
1335 hunk.range.clone(),
1336 &diff_base[hunk.diff_base_byte_range.clone()],
1337 buffer
1338 .text_for_range(hunk.range.clone())
1339 .collect::<String>(),
1340 hunk.status(),
1341 )
1342 })
1343 .collect::<Vec<_>>();
1344
1345 let expected_hunks: Vec<_> = expected_hunks
1346 .iter()
1347 .map(|(line_range, deleted_text, added_text, status)| {
1348 (
1349 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
1350 deleted_text.as_ref(),
1351 added_text.as_ref().to_string(),
1352 *status,
1353 )
1354 })
1355 .collect();
1356
1357 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362 use std::fmt::Write as _;
1363
1364 use super::*;
1365 use gpui::TestAppContext;
1366 use pretty_assertions::{assert_eq, assert_ne};
1367 use rand::{Rng as _, rngs::StdRng};
1368 use text::{Buffer, BufferId, ReplicaId, Rope};
1369 use unindent::Unindent as _;
1370 use util::test::marked_text_ranges;
1371
1372 #[ctor::ctor]
1373 fn init_logger() {
1374 zlog::init_test();
1375 }
1376
1377 #[gpui::test]
1378 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1379 let diff_base = "
1380 one
1381 two
1382 three
1383 "
1384 .unindent();
1385
1386 let buffer_text = "
1387 one
1388 HELLO
1389 three
1390 "
1391 .unindent();
1392
1393 let mut buffer = Buffer::new(
1394 ReplicaId::LOCAL,
1395 BufferId::new(1).unwrap(),
1396 buffer_text,
1397 cx.background_executor(),
1398 );
1399 let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1400 assert_hunks(
1401 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1402 &buffer,
1403 &diff_base,
1404 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
1405 );
1406
1407 buffer.edit([(0..0, "point five\n")], cx.background_executor());
1408 diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1409 assert_hunks(
1410 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1411 &buffer,
1412 &diff_base,
1413 &[
1414 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
1415 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
1416 ],
1417 );
1418
1419 diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
1420 assert_hunks::<&str, _>(
1421 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1422 &buffer,
1423 &diff_base,
1424 &[],
1425 );
1426 }
1427
1428 #[gpui::test]
1429 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1430 let head_text = "
1431 zero
1432 one
1433 two
1434 three
1435 four
1436 five
1437 six
1438 seven
1439 eight
1440 nine
1441 "
1442 .unindent();
1443
1444 let index_text = "
1445 zero
1446 one
1447 TWO
1448 three
1449 FOUR
1450 five
1451 six
1452 seven
1453 eight
1454 NINE
1455 "
1456 .unindent();
1457
1458 let buffer_text = "
1459 zero
1460 one
1461 TWO
1462 three
1463 FOUR
1464 FIVE
1465 six
1466 SEVEN
1467 eight
1468 nine
1469 "
1470 .unindent();
1471
1472 let buffer = Buffer::new(
1473 ReplicaId::LOCAL,
1474 BufferId::new(1).unwrap(),
1475 buffer_text,
1476 cx.background_executor(),
1477 );
1478 let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1479 let mut uncommitted_diff =
1480 BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1481 uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
1482
1483 let expected_hunks = vec![
1484 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
1485 (
1486 4..6,
1487 "four\nfive\n",
1488 "FOUR\nFIVE\n",
1489 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1490 ),
1491 (
1492 7..8,
1493 "seven\n",
1494 "SEVEN\n",
1495 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1496 ),
1497 ];
1498
1499 assert_hunks(
1500 uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1501 &buffer,
1502 &head_text,
1503 &expected_hunks,
1504 );
1505 }
1506
1507 #[gpui::test]
1508 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1509 let diff_base = Arc::new(
1510 "
1511 one
1512 two
1513 three
1514 four
1515 five
1516 six
1517 seven
1518 eight
1519 nine
1520 ten
1521 "
1522 .unindent(),
1523 );
1524
1525 let buffer_text = "
1526 A
1527 one
1528 B
1529 two
1530 C
1531 three
1532 HELLO
1533 four
1534 five
1535 SIXTEEN
1536 seven
1537 eight
1538 WORLD
1539 nine
1540
1541 ten
1542
1543 "
1544 .unindent();
1545
1546 let buffer = Buffer::new(
1547 ReplicaId::LOCAL,
1548 BufferId::new(1).unwrap(),
1549 buffer_text,
1550 cx.background_executor(),
1551 );
1552 let diff = cx
1553 .update(|cx| {
1554 BufferDiffSnapshot::new_with_base_text(
1555 buffer.snapshot(),
1556 Some(diff_base.clone()),
1557 None,
1558 None,
1559 cx,
1560 )
1561 })
1562 .await;
1563 assert_eq!(
1564 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer)
1565 .count(),
1566 8
1567 );
1568
1569 assert_hunks(
1570 diff.hunks_intersecting_range(
1571 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1572 &buffer,
1573 ),
1574 &buffer,
1575 &diff_base,
1576 &[
1577 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
1578 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
1579 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
1580 ],
1581 );
1582 }
1583
1584 #[gpui::test]
1585 async fn test_stage_hunk(cx: &mut TestAppContext) {
1586 struct Example {
1587 name: &'static str,
1588 head_text: String,
1589 index_text: String,
1590 buffer_marked_text: String,
1591 final_index_text: String,
1592 }
1593
1594 let table = [
1595 Example {
1596 name: "uncommitted hunk straddles end of unstaged hunk",
1597 head_text: "
1598 one
1599 two
1600 three
1601 four
1602 five
1603 "
1604 .unindent(),
1605 index_text: "
1606 one
1607 TWO_HUNDRED
1608 three
1609 FOUR_HUNDRED
1610 five
1611 "
1612 .unindent(),
1613 buffer_marked_text: "
1614 ZERO
1615 one
1616 two
1617 «THREE_HUNDRED
1618 FOUR_HUNDRED»
1619 five
1620 SIX
1621 "
1622 .unindent(),
1623 final_index_text: "
1624 one
1625 two
1626 THREE_HUNDRED
1627 FOUR_HUNDRED
1628 five
1629 "
1630 .unindent(),
1631 },
1632 Example {
1633 name: "uncommitted hunk straddles start of unstaged hunk",
1634 head_text: "
1635 one
1636 two
1637 three
1638 four
1639 five
1640 "
1641 .unindent(),
1642 index_text: "
1643 one
1644 TWO_HUNDRED
1645 three
1646 FOUR_HUNDRED
1647 five
1648 "
1649 .unindent(),
1650 buffer_marked_text: "
1651 ZERO
1652 one
1653 «TWO_HUNDRED
1654 THREE_HUNDRED»
1655 four
1656 five
1657 SIX
1658 "
1659 .unindent(),
1660 final_index_text: "
1661 one
1662 TWO_HUNDRED
1663 THREE_HUNDRED
1664 four
1665 five
1666 "
1667 .unindent(),
1668 },
1669 Example {
1670 name: "uncommitted hunk strictly contains unstaged hunks",
1671 head_text: "
1672 one
1673 two
1674 three
1675 four
1676 five
1677 six
1678 seven
1679 "
1680 .unindent(),
1681 index_text: "
1682 one
1683 TWO
1684 THREE
1685 FOUR
1686 FIVE
1687 SIX
1688 seven
1689 "
1690 .unindent(),
1691 buffer_marked_text: "
1692 one
1693 TWO
1694 «THREE_HUNDRED
1695 FOUR
1696 FIVE_HUNDRED»
1697 SIX
1698 seven
1699 "
1700 .unindent(),
1701 final_index_text: "
1702 one
1703 TWO
1704 THREE_HUNDRED
1705 FOUR
1706 FIVE_HUNDRED
1707 SIX
1708 seven
1709 "
1710 .unindent(),
1711 },
1712 Example {
1713 name: "uncommitted deletion hunk",
1714 head_text: "
1715 one
1716 two
1717 three
1718 four
1719 five
1720 "
1721 .unindent(),
1722 index_text: "
1723 one
1724 two
1725 three
1726 four
1727 five
1728 "
1729 .unindent(),
1730 buffer_marked_text: "
1731 one
1732 ˇfive
1733 "
1734 .unindent(),
1735 final_index_text: "
1736 one
1737 five
1738 "
1739 .unindent(),
1740 },
1741 Example {
1742 name: "one unstaged hunk that contains two uncommitted hunks",
1743 head_text: "
1744 one
1745 two
1746
1747 three
1748 four
1749 "
1750 .unindent(),
1751 index_text: "
1752 one
1753 two
1754 three
1755 four
1756 "
1757 .unindent(),
1758 buffer_marked_text: "
1759 «one
1760
1761 three // modified
1762 four»
1763 "
1764 .unindent(),
1765 final_index_text: "
1766 one
1767
1768 three // modified
1769 four
1770 "
1771 .unindent(),
1772 },
1773 Example {
1774 name: "one uncommitted hunk that contains two unstaged hunks",
1775 head_text: "
1776 one
1777 two
1778 three
1779 four
1780 five
1781 "
1782 .unindent(),
1783 index_text: "
1784 ZERO
1785 one
1786 TWO
1787 THREE
1788 FOUR
1789 five
1790 "
1791 .unindent(),
1792 buffer_marked_text: "
1793 «one
1794 TWO_HUNDRED
1795 THREE
1796 FOUR_HUNDRED
1797 five»
1798 "
1799 .unindent(),
1800 final_index_text: "
1801 ZERO
1802 one
1803 TWO_HUNDRED
1804 THREE
1805 FOUR_HUNDRED
1806 five
1807 "
1808 .unindent(),
1809 },
1810 ];
1811
1812 for example in table {
1813 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
1814 let buffer = Buffer::new(
1815 ReplicaId::LOCAL,
1816 BufferId::new(1).unwrap(),
1817 buffer_text,
1818 cx.background_executor(),
1819 );
1820 let hunk_range =
1821 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
1822
1823 let unstaged =
1824 BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
1825 let uncommitted =
1826 BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
1827
1828 let unstaged_diff = cx.new(|cx| {
1829 let mut diff = BufferDiff::new(&buffer, cx);
1830 diff.set_snapshot(unstaged, &buffer, cx);
1831 diff
1832 });
1833
1834 let uncommitted_diff = cx.new(|cx| {
1835 let mut diff = BufferDiff::new(&buffer, cx);
1836 diff.set_snapshot(uncommitted, &buffer, cx);
1837 diff.set_secondary_diff(unstaged_diff);
1838 diff
1839 });
1840
1841 uncommitted_diff.update(cx, |diff, cx| {
1842 let hunks = diff
1843 .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
1844 .collect::<Vec<_>>();
1845 for hunk in &hunks {
1846 assert_ne!(
1847 hunk.secondary_status,
1848 DiffHunkSecondaryStatus::NoSecondaryHunk
1849 )
1850 }
1851
1852 let new_index_text = diff
1853 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
1854 .unwrap()
1855 .to_string();
1856
1857 let hunks = diff
1858 .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
1859 .collect::<Vec<_>>();
1860 for hunk in &hunks {
1861 assert_eq!(
1862 hunk.secondary_status,
1863 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1864 )
1865 }
1866
1867 pretty_assertions::assert_eq!(
1868 new_index_text,
1869 example.final_index_text,
1870 "example: {}",
1871 example.name
1872 );
1873 });
1874 }
1875 }
1876
1877 #[gpui::test]
1878 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
1879 let head_text = "
1880 one
1881 two
1882 three
1883 "
1884 .unindent();
1885 let index_text = head_text.clone();
1886 let buffer_text = "
1887 one
1888 three
1889 "
1890 .unindent();
1891
1892 let buffer = Buffer::new(
1893 ReplicaId::LOCAL,
1894 BufferId::new(1).unwrap(),
1895 buffer_text.clone(),
1896 cx.background_executor(),
1897 );
1898 let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1899 let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1900 let unstaged_diff = cx.new(|cx| {
1901 let mut diff = BufferDiff::new(&buffer, cx);
1902 diff.set_snapshot(unstaged, &buffer, cx);
1903 diff
1904 });
1905 let uncommitted_diff = cx.new(|cx| {
1906 let mut diff = BufferDiff::new(&buffer, cx);
1907 diff.set_snapshot(uncommitted, &buffer, cx);
1908 diff.set_secondary_diff(unstaged_diff.clone());
1909 diff
1910 });
1911
1912 uncommitted_diff.update(cx, |diff, cx| {
1913 let hunk = diff.hunks(&buffer, cx).next().unwrap();
1914
1915 let new_index_text = diff
1916 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
1917 .unwrap()
1918 .to_string();
1919 assert_eq!(new_index_text, buffer_text);
1920
1921 let hunk = diff.hunks(&buffer, cx).next().unwrap();
1922 assert_eq!(
1923 hunk.secondary_status,
1924 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1925 );
1926
1927 let index_text = diff
1928 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
1929 .unwrap()
1930 .to_string();
1931 assert_eq!(index_text, head_text);
1932
1933 let hunk = diff.hunks(&buffer, cx).next().unwrap();
1934 // optimistically unstaged (fine, could also be HasSecondaryHunk)
1935 assert_eq!(
1936 hunk.secondary_status,
1937 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1938 );
1939 });
1940 }
1941
1942 #[gpui::test]
1943 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
1944 let base_text = "
1945 zero
1946 one
1947 two
1948 three
1949 four
1950 five
1951 six
1952 seven
1953 eight
1954 nine
1955 "
1956 .unindent();
1957
1958 let buffer_text_1 = "
1959 one
1960 three
1961 four
1962 five
1963 SIX
1964 seven
1965 eight
1966 NINE
1967 "
1968 .unindent();
1969
1970 let mut buffer = Buffer::new(
1971 ReplicaId::LOCAL,
1972 BufferId::new(1).unwrap(),
1973 buffer_text_1,
1974 cx.background_executor(),
1975 );
1976
1977 let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
1978 let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1979 let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
1980 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
1981
1982 // Edit does not affect the diff.
1983 buffer.edit_via_marked_text(
1984 &"
1985 one
1986 three
1987 four
1988 five
1989 «SIX.5»
1990 seven
1991 eight
1992 NINE
1993 "
1994 .unindent(),
1995 cx.background_executor(),
1996 );
1997 let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1998 assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
1999
2000 // Edit turns a deletion hunk into a modification.
2001 buffer.edit_via_marked_text(
2002 &"
2003 one
2004 «THREE»
2005 four
2006 five
2007 SIX.5
2008 seven
2009 eight
2010 NINE
2011 "
2012 .unindent(),
2013 cx.background_executor(),
2014 );
2015 let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2016 let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap();
2017 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2018
2019 // Edit turns a modification hunk into a deletion.
2020 buffer.edit_via_marked_text(
2021 &"
2022 one
2023 THREE
2024 four
2025 five«»
2026 seven
2027 eight
2028 NINE
2029 "
2030 .unindent(),
2031 cx.background_executor(),
2032 );
2033 let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2034 let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap();
2035 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2036
2037 // Edit introduces a new insertion hunk.
2038 buffer.edit_via_marked_text(
2039 &"
2040 one
2041 THREE
2042 four«
2043 FOUR.5
2044 »five
2045 seven
2046 eight
2047 NINE
2048 "
2049 .unindent(),
2050 cx.background_executor(),
2051 );
2052 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2053 let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap();
2054 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2055
2056 // Edit removes a hunk.
2057 buffer.edit_via_marked_text(
2058 &"
2059 one
2060 THREE
2061 four
2062 FOUR.5
2063 five
2064 seven
2065 eight
2066 «nine»
2067 "
2068 .unindent(),
2069 cx.background_executor(),
2070 );
2071 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
2072 let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap();
2073 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2074 }
2075
2076 #[gpui::test(iterations = 100)]
2077 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
2078 fn gen_line(rng: &mut StdRng) -> String {
2079 if rng.random_bool(0.2) {
2080 "\n".to_owned()
2081 } else {
2082 let c = rng.random_range('A'..='Z');
2083 format!("{c}{c}{c}\n")
2084 }
2085 }
2086
2087 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
2088 let mut old_lines = {
2089 let mut old_lines = Vec::new();
2090 let old_lines_iter = head.lines();
2091 for line in old_lines_iter {
2092 assert!(!line.ends_with("\n"));
2093 old_lines.push(line.to_owned());
2094 }
2095 if old_lines.last().is_some_and(|line| line.is_empty()) {
2096 old_lines.pop();
2097 }
2098 old_lines.into_iter()
2099 };
2100 let mut result = String::new();
2101 let unchanged_count = rng.random_range(0..=old_lines.len());
2102 result +=
2103 &old_lines
2104 .by_ref()
2105 .take(unchanged_count)
2106 .fold(String::new(), |mut s, line| {
2107 writeln!(&mut s, "{line}").unwrap();
2108 s
2109 });
2110 while old_lines.len() > 0 {
2111 let deleted_count = rng.random_range(0..=old_lines.len());
2112 let _advance = old_lines
2113 .by_ref()
2114 .take(deleted_count)
2115 .map(|line| line.len() + 1)
2116 .sum::<usize>();
2117 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
2118 let added_count = rng.random_range(minimum_added..=5);
2119 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
2120 result += &addition;
2121
2122 if old_lines.len() > 0 {
2123 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
2124 if blank_lines == old_lines.len() {
2125 break;
2126 };
2127 let unchanged_count =
2128 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
2129 result += &old_lines.by_ref().take(unchanged_count).fold(
2130 String::new(),
2131 |mut s, line| {
2132 writeln!(&mut s, "{line}").unwrap();
2133 s
2134 },
2135 );
2136 }
2137 }
2138 result
2139 }
2140
2141 fn uncommitted_diff(
2142 working_copy: &language::BufferSnapshot,
2143 index_text: &Rope,
2144 head_text: String,
2145 cx: &mut TestAppContext,
2146 ) -> Entity<BufferDiff> {
2147 let inner =
2148 BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
2149 let secondary = BufferDiff {
2150 buffer_id: working_copy.remote_id(),
2151 inner: BufferDiffSnapshot::new_sync(
2152 working_copy.text.clone(),
2153 index_text.to_string(),
2154 cx,
2155 )
2156 .inner,
2157 secondary_diff: None,
2158 };
2159 let secondary = cx.new(|_| secondary);
2160 cx.new(|_| BufferDiff {
2161 buffer_id: working_copy.remote_id(),
2162 inner,
2163 secondary_diff: Some(secondary),
2164 })
2165 }
2166
2167 let operations = std::env::var("OPERATIONS")
2168 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2169 .unwrap_or(10);
2170
2171 let rng = &mut rng;
2172 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
2173 writeln!(&mut s, "{c}{c}{c}").unwrap();
2174 s
2175 });
2176 let working_copy = gen_working_copy(rng, &head_text);
2177 let working_copy = cx.new(|cx| {
2178 language::Buffer::local_normalized(
2179 Rope::from_str(working_copy.as_str(), cx.background_executor()),
2180 text::LineEnding::default(),
2181 cx,
2182 )
2183 });
2184 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
2185 let mut index_text = if rng.random() {
2186 Rope::from_str(head_text.as_str(), cx.background_executor())
2187 } else {
2188 working_copy.as_rope().clone()
2189 };
2190
2191 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2192 let mut hunks = diff.update(cx, |diff, cx| {
2193 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
2194 .collect::<Vec<_>>()
2195 });
2196 if hunks.is_empty() {
2197 return;
2198 }
2199
2200 for _ in 0..operations {
2201 let i = rng.random_range(0..hunks.len());
2202 let hunk = &mut hunks[i];
2203 let hunk_to_change = hunk.clone();
2204 let stage = match hunk.secondary_status {
2205 DiffHunkSecondaryStatus::HasSecondaryHunk => {
2206 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
2207 true
2208 }
2209 DiffHunkSecondaryStatus::NoSecondaryHunk => {
2210 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
2211 false
2212 }
2213 _ => unreachable!(),
2214 };
2215
2216 index_text = diff.update(cx, |diff, cx| {
2217 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
2218 .unwrap()
2219 });
2220
2221 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2222 let found_hunks = diff.update(cx, |diff, cx| {
2223 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
2224 .collect::<Vec<_>>()
2225 });
2226 assert_eq!(hunks.len(), found_hunks.len());
2227
2228 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
2229 assert_eq!(
2230 expected_hunk.buffer_range.to_point(&working_copy),
2231 found_hunk.buffer_range.to_point(&working_copy)
2232 );
2233 assert_eq!(
2234 expected_hunk.diff_base_byte_range,
2235 found_hunk.diff_base_byte_range
2236 );
2237 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
2238 }
2239 hunks = found_hunks;
2240 }
2241 }
2242}