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