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