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