1use futures::{channel::oneshot, future::OptionFuture};
2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
3use gpui::{App, AsyncApp, Context, Entity, EventEmitter};
4use language::{Language, LanguageRegistry};
5use rope::Rope;
6use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
7use sum_tree::SumTree;
8use text::ToOffset as _;
9use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
10use util::ResultExt;
11
12pub struct BufferDiff {
13 pub buffer_id: BufferId,
14 inner: BufferDiffInner,
15 secondary_diff: Option<Entity<BufferDiff>>,
16}
17
18#[derive(Clone, Debug)]
19pub struct BufferDiffSnapshot {
20 inner: BufferDiffInner,
21 secondary_diff: Option<Box<BufferDiffSnapshot>>,
22 pub is_single_insertion: bool,
23}
24
25#[derive(Clone)]
26struct BufferDiffInner {
27 hunks: SumTree<InternalDiffHunk>,
28 base_text: Option<language::BufferSnapshot>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum DiffHunkStatus {
33 Added(DiffHunkSecondaryStatus),
34 Modified(DiffHunkSecondaryStatus),
35 Removed(DiffHunkSecondaryStatus),
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum DiffHunkSecondaryStatus {
40 HasSecondaryHunk,
41 OverlapsWithSecondaryHunk,
42 None,
43}
44
45/// A diff hunk resolved to rows in the buffer.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct DiffHunk {
48 /// The buffer range, expressed in terms of rows.
49 pub row_range: Range<u32>,
50 /// The range in the buffer to which this hunk corresponds.
51 pub buffer_range: Range<Anchor>,
52 /// The range in the buffer's diff base text to which this hunk corresponds.
53 pub diff_base_byte_range: Range<usize>,
54 pub secondary_status: DiffHunkSecondaryStatus,
55 pub secondary_diff_base_byte_range: Option<Range<usize>>,
56}
57
58/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
59#[derive(Debug, Clone, PartialEq, Eq)]
60struct InternalDiffHunk {
61 buffer_range: Range<Anchor>,
62 diff_base_byte_range: Range<usize>,
63}
64
65impl sum_tree::Item for InternalDiffHunk {
66 type Summary = DiffHunkSummary;
67
68 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
69 DiffHunkSummary {
70 buffer_range: self.buffer_range.clone(),
71 }
72 }
73}
74
75#[derive(Debug, Default, Clone)]
76pub struct DiffHunkSummary {
77 buffer_range: Range<Anchor>,
78}
79
80impl sum_tree::Summary for DiffHunkSummary {
81 type Context = text::BufferSnapshot;
82
83 fn zero(_cx: &Self::Context) -> Self {
84 Default::default()
85 }
86
87 fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
88 self.buffer_range.start = self
89 .buffer_range
90 .start
91 .min(&other.buffer_range.start, buffer);
92 self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
93 }
94}
95
96impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor {
97 fn cmp(
98 &self,
99 cursor_location: &DiffHunkSummary,
100 buffer: &text::BufferSnapshot,
101 ) -> cmp::Ordering {
102 self.cmp(&cursor_location.buffer_range.end, buffer)
103 }
104}
105
106impl std::fmt::Debug for BufferDiffInner {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("BufferDiffSnapshot")
109 .field("hunks", &self.hunks)
110 .finish()
111 }
112}
113
114impl BufferDiffSnapshot {
115 pub fn is_empty(&self) -> bool {
116 self.inner.hunks.is_empty()
117 }
118
119 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
120 self.secondary_diff.as_deref()
121 }
122
123 pub fn hunks_intersecting_range<'a>(
124 &'a self,
125 range: Range<Anchor>,
126 buffer: &'a text::BufferSnapshot,
127 ) -> impl 'a + Iterator<Item = DiffHunk> {
128 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
129 self.inner
130 .hunks_intersecting_range(range, buffer, unstaged_counterpart)
131 }
132
133 pub fn hunks_intersecting_range_rev<'a>(
134 &'a self,
135 range: Range<Anchor>,
136 buffer: &'a text::BufferSnapshot,
137 ) -> impl 'a + Iterator<Item = DiffHunk> {
138 self.inner.hunks_intersecting_range_rev(range, buffer)
139 }
140
141 pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
142 self.inner.base_text.as_ref()
143 }
144
145 pub fn base_texts_eq(&self, other: &Self) -> bool {
146 match (other.base_text(), self.base_text()) {
147 (None, None) => true,
148 (None, Some(_)) => false,
149 (Some(_), None) => false,
150 (Some(old), Some(new)) => {
151 let (old_id, old_empty) = (old.remote_id(), old.is_empty());
152 let (new_id, new_empty) = (new.remote_id(), new.is_empty());
153 new_id == old_id || (new_empty && old_empty)
154 }
155 }
156 }
157
158 fn buffer_range_to_unchanged_diff_base_range(
159 &self,
160 buffer_range: Range<Anchor>,
161 buffer: &text::BufferSnapshot,
162 ) -> Option<Range<usize>> {
163 let mut hunks = self.inner.hunks.iter();
164 let mut start = 0;
165 let mut pos = buffer.anchor_before(0);
166 while let Some(hunk) = hunks.next() {
167 assert!(buffer_range.start.cmp(&pos, buffer).is_ge());
168 assert!(hunk.buffer_range.start.cmp(&pos, buffer).is_ge());
169 if hunk
170 .buffer_range
171 .start
172 .cmp(&buffer_range.end, buffer)
173 .is_ge()
174 {
175 // target buffer range is contained in the unchanged stretch leading up to this next hunk,
176 // so do a final adjustment based on that
177 break;
178 }
179
180 // if the target buffer range intersects this hunk at all, no dice
181 if buffer_range
182 .start
183 .cmp(&hunk.buffer_range.end, buffer)
184 .is_lt()
185 {
186 return None;
187 }
188
189 start += hunk.buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
190 start += hunk.diff_base_byte_range.end - hunk.diff_base_byte_range.start;
191 pos = hunk.buffer_range.end;
192 }
193 start += buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
194 let end = start + buffer_range.end.to_offset(buffer) - buffer_range.start.to_offset(buffer);
195 Some(start..end)
196 }
197
198 pub fn secondary_edits_for_stage_or_unstage(
199 &self,
200 stage: bool,
201 hunks: impl Iterator<Item = (Range<usize>, Option<Range<usize>>, Range<Anchor>)>,
202 buffer: &text::BufferSnapshot,
203 ) -> Vec<(Range<usize>, String)> {
204 let Some(secondary_diff) = self.secondary_diff() else {
205 log::debug!("no secondary diff");
206 return Vec::new();
207 };
208 let index_base = secondary_diff.base_text().map_or_else(
209 || Rope::from(""),
210 |snapshot| snapshot.text.as_rope().clone(),
211 );
212 let head_base = self.base_text().map_or_else(
213 || Rope::from(""),
214 |snapshot| snapshot.text.as_rope().clone(),
215 );
216 log::debug!("original: {:?}", index_base.to_string());
217 let mut edits = Vec::new();
218 for (diff_base_byte_range, secondary_diff_base_byte_range, buffer_range) in hunks {
219 let (index_byte_range, replacement_text) = if stage {
220 log::debug!("staging");
221 let mut replacement_text = String::new();
222 let Some(index_byte_range) = secondary_diff_base_byte_range.clone() else {
223 log::debug!("not a stageable hunk");
224 continue;
225 };
226 log::debug!("using {:?}", index_byte_range);
227 for chunk in buffer.text_for_range(buffer_range.clone()) {
228 replacement_text.push_str(chunk);
229 }
230 (index_byte_range, replacement_text)
231 } else {
232 log::debug!("unstaging");
233 let mut replacement_text = String::new();
234 let Some(index_byte_range) = secondary_diff
235 .buffer_range_to_unchanged_diff_base_range(buffer_range.clone(), &buffer)
236 else {
237 log::debug!("not an unstageable hunk");
238 continue;
239 };
240 for chunk in head_base.chunks_in_range(diff_base_byte_range.clone()) {
241 replacement_text.push_str(chunk);
242 }
243 (index_byte_range, replacement_text)
244 };
245 edits.push((index_byte_range, replacement_text));
246 }
247 log::debug!("edits: {edits:?}");
248 edits
249 }
250}
251
252impl BufferDiffInner {
253 fn hunks_intersecting_range<'a>(
254 &'a self,
255 range: Range<Anchor>,
256 buffer: &'a text::BufferSnapshot,
257 secondary: Option<&'a Self>,
258 ) -> impl 'a + Iterator<Item = DiffHunk> {
259 let range = range.to_offset(buffer);
260
261 let mut cursor = self
262 .hunks
263 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
264 let summary_range = summary.buffer_range.to_offset(buffer);
265 let before_start = summary_range.end < range.start;
266 let after_end = summary_range.start > range.end;
267 !before_start && !after_end
268 });
269
270 let anchor_iter = iter::from_fn(move || {
271 cursor.next(buffer);
272 cursor.item()
273 })
274 .flat_map(move |hunk| {
275 [
276 (
277 &hunk.buffer_range.start,
278 (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
279 ),
280 (
281 &hunk.buffer_range.end,
282 (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
283 ),
284 ]
285 });
286
287 let mut secondary_cursor = secondary.as_ref().map(|diff| {
288 let mut cursor = diff.hunks.cursor::<DiffHunkSummary>(buffer);
289 cursor.next(buffer);
290 cursor
291 });
292
293 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
294 iter::from_fn(move || loop {
295 let (start_point, (start_anchor, start_base)) = summaries.next()?;
296 let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
297
298 if !start_anchor.is_valid(buffer) {
299 continue;
300 }
301
302 if end_point.column > 0 {
303 end_point.row += 1;
304 end_point.column = 0;
305 end_anchor = buffer.anchor_before(end_point);
306 }
307
308 let mut secondary_status = DiffHunkSecondaryStatus::None;
309 let mut secondary_diff_base_byte_range = None;
310 if let Some(secondary_cursor) = secondary_cursor.as_mut() {
311 if start_anchor
312 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
313 .is_gt()
314 {
315 secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer);
316 }
317
318 if let Some(secondary_hunk) = secondary_cursor.item() {
319 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
320 if secondary_range.end.column > 0 {
321 secondary_range.end.row += 1;
322 secondary_range.end.column = 0;
323 }
324 if secondary_range == (start_point..end_point) {
325 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
326 secondary_diff_base_byte_range =
327 Some(secondary_hunk.diff_base_byte_range.clone());
328 } else if secondary_range.start <= end_point {
329 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
330 }
331 }
332 }
333
334 return Some(DiffHunk {
335 row_range: start_point.row..end_point.row,
336 diff_base_byte_range: start_base..end_base,
337 buffer_range: start_anchor..end_anchor,
338 secondary_status,
339 secondary_diff_base_byte_range,
340 });
341 })
342 }
343
344 fn hunks_intersecting_range_rev<'a>(
345 &'a self,
346 range: Range<Anchor>,
347 buffer: &'a text::BufferSnapshot,
348 ) -> impl 'a + Iterator<Item = DiffHunk> {
349 let mut cursor = self
350 .hunks
351 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
352 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
353 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
354 !before_start && !after_end
355 });
356
357 iter::from_fn(move || {
358 cursor.prev(buffer);
359
360 let hunk = cursor.item()?;
361 let range = hunk.buffer_range.to_point(buffer);
362 let end_row = if range.end.column > 0 {
363 range.end.row + 1
364 } else {
365 range.end.row
366 };
367
368 Some(DiffHunk {
369 row_range: range.start.row..end_row,
370 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
371 buffer_range: hunk.buffer_range.clone(),
372 // The secondary status is not used by callers of this method.
373 secondary_status: DiffHunkSecondaryStatus::None,
374 secondary_diff_base_byte_range: None,
375 })
376 })
377 }
378
379 fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
380 let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
381 let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
382 old_cursor.next(new_snapshot);
383 new_cursor.next(new_snapshot);
384 let mut start = None;
385 let mut end = None;
386
387 loop {
388 match (new_cursor.item(), old_cursor.item()) {
389 (Some(new_hunk), Some(old_hunk)) => {
390 match new_hunk
391 .buffer_range
392 .start
393 .cmp(&old_hunk.buffer_range.start, new_snapshot)
394 {
395 cmp::Ordering::Less => {
396 start.get_or_insert(new_hunk.buffer_range.start);
397 end.replace(new_hunk.buffer_range.end);
398 new_cursor.next(new_snapshot);
399 }
400 cmp::Ordering::Equal => {
401 if new_hunk != old_hunk {
402 start.get_or_insert(new_hunk.buffer_range.start);
403 if old_hunk
404 .buffer_range
405 .end
406 .cmp(&new_hunk.buffer_range.end, new_snapshot)
407 .is_ge()
408 {
409 end.replace(old_hunk.buffer_range.end);
410 } else {
411 end.replace(new_hunk.buffer_range.end);
412 }
413 }
414
415 new_cursor.next(new_snapshot);
416 old_cursor.next(new_snapshot);
417 }
418 cmp::Ordering::Greater => {
419 start.get_or_insert(old_hunk.buffer_range.start);
420 end.replace(old_hunk.buffer_range.end);
421 old_cursor.next(new_snapshot);
422 }
423 }
424 }
425 (Some(new_hunk), None) => {
426 start.get_or_insert(new_hunk.buffer_range.start);
427 end.replace(new_hunk.buffer_range.end);
428 new_cursor.next(new_snapshot);
429 }
430 (None, Some(old_hunk)) => {
431 start.get_or_insert(old_hunk.buffer_range.start);
432 end.replace(old_hunk.buffer_range.end);
433 old_cursor.next(new_snapshot);
434 }
435 (None, None) => break,
436 }
437 }
438
439 start.zip(end).map(|(start, end)| start..end)
440 }
441}
442
443fn compute_hunks(
444 diff_base: Option<(Arc<String>, Rope)>,
445 buffer: text::BufferSnapshot,
446) -> SumTree<InternalDiffHunk> {
447 let mut tree = SumTree::new(&buffer);
448
449 if let Some((diff_base, diff_base_rope)) = diff_base {
450 let buffer_text = buffer.as_rope().to_string();
451
452 let mut options = GitOptions::default();
453 options.context_lines(0);
454 let patch = GitPatch::from_buffers(
455 diff_base.as_bytes(),
456 None,
457 buffer_text.as_bytes(),
458 None,
459 Some(&mut options),
460 )
461 .log_err();
462
463 // A common case in Zed is that the empty buffer is represented as just a newline,
464 // but if we just compute a naive diff you get a "preserved" line in the middle,
465 // which is a bit odd.
466 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
467 tree.push(
468 InternalDiffHunk {
469 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
470 diff_base_byte_range: 0..diff_base.len() - 1,
471 },
472 &buffer,
473 );
474 return tree;
475 }
476
477 if let Some(patch) = patch {
478 let mut divergence = 0;
479 for hunk_index in 0..patch.num_hunks() {
480 let hunk = process_patch_hunk(
481 &patch,
482 hunk_index,
483 &diff_base_rope,
484 &buffer,
485 &mut divergence,
486 );
487 tree.push(hunk, &buffer);
488 }
489 }
490 }
491
492 tree
493}
494
495fn process_patch_hunk(
496 patch: &GitPatch<'_>,
497 hunk_index: usize,
498 diff_base: &Rope,
499 buffer: &text::BufferSnapshot,
500 buffer_row_divergence: &mut i64,
501) -> InternalDiffHunk {
502 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
503 assert!(line_item_count > 0);
504
505 let mut first_deletion_buffer_row: Option<u32> = None;
506 let mut buffer_row_range: Option<Range<u32>> = None;
507 let mut diff_base_byte_range: Option<Range<usize>> = None;
508 let mut first_addition_old_row: Option<u32> = None;
509
510 for line_index in 0..line_item_count {
511 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
512 let kind = line.origin_value();
513 let content_offset = line.content_offset() as isize;
514 let content_len = line.content().len() as isize;
515 match kind {
516 GitDiffLineType::Addition => {
517 if first_addition_old_row.is_none() {
518 first_addition_old_row = Some(
519 (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
520 );
521 }
522 *buffer_row_divergence += 1;
523 let row = line.new_lineno().unwrap().saturating_sub(1);
524
525 match &mut buffer_row_range {
526 Some(Range { end, .. }) => *end = row + 1,
527 None => buffer_row_range = Some(row..row + 1),
528 }
529 }
530 GitDiffLineType::Deletion => {
531 let end = content_offset + content_len;
532
533 match &mut diff_base_byte_range {
534 Some(head_byte_range) => head_byte_range.end = end as usize,
535 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
536 }
537
538 if first_deletion_buffer_row.is_none() {
539 let old_row = line.old_lineno().unwrap().saturating_sub(1);
540 let row = old_row as i64 + *buffer_row_divergence;
541 first_deletion_buffer_row = Some(row as u32);
542 }
543
544 *buffer_row_divergence -= 1;
545 }
546 _ => {}
547 }
548 }
549
550 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
551 // Pure deletion hunk without addition.
552 let row = first_deletion_buffer_row.unwrap();
553 row..row
554 });
555 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
556 // Pure addition hunk without deletion.
557 let row = first_addition_old_row.unwrap();
558 let offset = diff_base.point_to_offset(Point::new(row, 0));
559 offset..offset
560 });
561
562 let start = Point::new(buffer_row_range.start, 0);
563 let end = Point::new(buffer_row_range.end, 0);
564 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
565 InternalDiffHunk {
566 buffer_range,
567 diff_base_byte_range,
568 }
569}
570
571impl std::fmt::Debug for BufferDiff {
572 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573 f.debug_struct("BufferChangeSet")
574 .field("buffer_id", &self.buffer_id)
575 .field("snapshot", &self.inner)
576 .finish()
577 }
578}
579
580pub enum BufferDiffEvent {
581 DiffChanged {
582 changed_range: Option<Range<text::Anchor>>,
583 },
584 LanguageChanged,
585}
586
587impl EventEmitter<BufferDiffEvent> for BufferDiff {}
588
589impl BufferDiff {
590 #[cfg(test)]
591 fn build_sync(
592 buffer: text::BufferSnapshot,
593 diff_base: String,
594 cx: &mut gpui::TestAppContext,
595 ) -> BufferDiffInner {
596 let snapshot =
597 cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
598 cx.executor().block(snapshot)
599 }
600
601 fn build(
602 buffer: text::BufferSnapshot,
603 diff_base: Option<Arc<String>>,
604 language: Option<Arc<Language>>,
605 language_registry: Option<Arc<LanguageRegistry>>,
606 cx: &mut App,
607 ) -> impl Future<Output = BufferDiffInner> {
608 let diff_base =
609 diff_base.map(|diff_base| (diff_base.clone(), Rope::from(diff_base.as_str())));
610 let base_text_snapshot = diff_base.as_ref().map(|(_, diff_base)| {
611 language::Buffer::build_snapshot(
612 diff_base.clone(),
613 language.clone(),
614 language_registry.clone(),
615 cx,
616 )
617 });
618 let base_text_snapshot = cx
619 .background_executor()
620 .spawn(OptionFuture::from(base_text_snapshot));
621
622 let hunks = cx.background_executor().spawn({
623 let buffer = buffer.clone();
624 async move { compute_hunks(diff_base, buffer) }
625 });
626
627 async move {
628 let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
629 BufferDiffInner { base_text, hunks }
630 }
631 }
632
633 fn build_with_base_buffer(
634 buffer: text::BufferSnapshot,
635 diff_base: Option<Arc<String>>,
636 diff_base_buffer: Option<language::BufferSnapshot>,
637 cx: &App,
638 ) -> impl Future<Output = BufferDiffInner> {
639 let diff_base = diff_base.clone().zip(
640 diff_base_buffer
641 .clone()
642 .map(|buffer| buffer.as_rope().clone()),
643 );
644 cx.background_executor().spawn(async move {
645 BufferDiffInner {
646 hunks: compute_hunks(diff_base, buffer),
647 base_text: diff_base_buffer,
648 }
649 })
650 }
651
652 fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner {
653 BufferDiffInner {
654 hunks: SumTree::new(buffer),
655 base_text: None,
656 }
657 }
658
659 pub fn build_with_single_insertion(
660 insertion_present_in_secondary_diff: bool,
661 buffer: language::BufferSnapshot,
662 cx: &mut App,
663 ) -> BufferDiffSnapshot {
664 let base_text = language::Buffer::build_empty_snapshot(cx);
665 let hunks = SumTree::from_item(
666 InternalDiffHunk {
667 buffer_range: Anchor::MIN..Anchor::MAX,
668 diff_base_byte_range: 0..0,
669 },
670 &base_text,
671 );
672 BufferDiffSnapshot {
673 inner: BufferDiffInner {
674 hunks: hunks.clone(),
675 base_text: Some(base_text.clone()),
676 },
677 secondary_diff: Some(Box::new(BufferDiffSnapshot {
678 inner: BufferDiffInner {
679 hunks: if insertion_present_in_secondary_diff {
680 hunks
681 } else {
682 SumTree::new(&buffer.text)
683 },
684 base_text: Some(if insertion_present_in_secondary_diff {
685 base_text
686 } else {
687 buffer
688 }),
689 },
690 secondary_diff: None,
691 is_single_insertion: true,
692 })),
693 is_single_insertion: true,
694 }
695 }
696
697 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
698 self.secondary_diff = Some(diff);
699 }
700
701 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
702 Some(self.secondary_diff.as_ref()?.clone())
703 }
704
705 pub fn range_to_hunk_range(
706 &self,
707 range: Range<Anchor>,
708 buffer: &text::BufferSnapshot,
709 cx: &App,
710 ) -> Option<Range<Anchor>> {
711 let start = self
712 .hunks_intersecting_range(range.clone(), &buffer, cx)
713 .next()?
714 .buffer_range
715 .start;
716 let end = self
717 .hunks_intersecting_range_rev(range.clone(), &buffer)
718 .next()?
719 .buffer_range
720 .end;
721 Some(start..end)
722 }
723
724 #[allow(clippy::too_many_arguments)]
725 pub async fn update_diff(
726 this: Entity<BufferDiff>,
727 buffer: text::BufferSnapshot,
728 base_text: Option<Arc<String>>,
729 base_text_changed: bool,
730 language_changed: bool,
731 language: Option<Arc<Language>>,
732 language_registry: Option<Arc<LanguageRegistry>>,
733 cx: &mut AsyncApp,
734 ) -> anyhow::Result<Option<Range<Anchor>>> {
735 let snapshot = if base_text_changed || language_changed {
736 cx.update(|cx| {
737 Self::build(
738 buffer.clone(),
739 base_text,
740 language.clone(),
741 language_registry.clone(),
742 cx,
743 )
744 })?
745 .await
746 } else {
747 this.read_with(cx, |this, cx| {
748 Self::build_with_base_buffer(
749 buffer.clone(),
750 base_text,
751 this.base_text().cloned(),
752 cx,
753 )
754 })?
755 .await
756 };
757
758 this.update(cx, |this, _| this.set_state(snapshot, &buffer))
759 }
760
761 pub fn update_diff_from(
762 &mut self,
763 buffer: &text::BufferSnapshot,
764 other: &Entity<Self>,
765 cx: &mut Context<Self>,
766 ) -> Option<Range<Anchor>> {
767 let other = other.read(cx).inner.clone();
768 self.set_state(other, buffer)
769 }
770
771 fn set_state(
772 &mut self,
773 inner: BufferDiffInner,
774 buffer: &text::BufferSnapshot,
775 ) -> Option<Range<Anchor>> {
776 let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) {
777 (None, None) => None,
778 (Some(old), Some(new)) if old.remote_id() == new.remote_id() => {
779 inner.compare(&self.inner, buffer)
780 }
781 _ => Some(text::Anchor::MIN..text::Anchor::MAX),
782 };
783 self.inner = inner;
784 changed_range
785 }
786
787 pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
788 self.inner.base_text.as_ref()
789 }
790
791 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
792 BufferDiffSnapshot {
793 inner: self.inner.clone(),
794 secondary_diff: self
795 .secondary_diff
796 .as_ref()
797 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
798 is_single_insertion: false,
799 }
800 }
801
802 pub fn hunks_intersecting_range<'a>(
803 &'a self,
804 range: Range<text::Anchor>,
805 buffer_snapshot: &'a text::BufferSnapshot,
806 cx: &'a App,
807 ) -> impl 'a + Iterator<Item = DiffHunk> {
808 let unstaged_counterpart = self
809 .secondary_diff
810 .as_ref()
811 .map(|diff| &diff.read(cx).inner);
812 self.inner
813 .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
814 }
815
816 pub fn hunks_intersecting_range_rev<'a>(
817 &'a self,
818 range: Range<text::Anchor>,
819 buffer_snapshot: &'a text::BufferSnapshot,
820 ) -> impl 'a + Iterator<Item = DiffHunk> {
821 self.inner
822 .hunks_intersecting_range_rev(range, buffer_snapshot)
823 }
824
825 pub fn hunks_in_row_range<'a>(
826 &'a self,
827 range: Range<u32>,
828 buffer: &'a text::BufferSnapshot,
829 cx: &'a App,
830 ) -> impl 'a + Iterator<Item = DiffHunk> {
831 let start = buffer.anchor_before(Point::new(range.start, 0));
832 let end = buffer.anchor_after(Point::new(range.end, 0));
833 self.hunks_intersecting_range(start..end, buffer, cx)
834 }
835
836 /// Used in cases where the change set isn't derived from git.
837 pub fn set_base_text(
838 &mut self,
839 base_buffer: Entity<language::Buffer>,
840 buffer: text::BufferSnapshot,
841 cx: &mut Context<Self>,
842 ) -> oneshot::Receiver<()> {
843 let (tx, rx) = oneshot::channel();
844 let this = cx.weak_entity();
845 let base_buffer = base_buffer.read(cx);
846 let language_registry = base_buffer.language_registry();
847 let base_buffer = base_buffer.snapshot();
848 let base_text = Arc::new(base_buffer.text());
849
850 let snapshot = BufferDiff::build(
851 buffer.clone(),
852 Some(base_text),
853 base_buffer.language().cloned(),
854 language_registry,
855 cx,
856 );
857 let complete_on_drop = util::defer(|| {
858 tx.send(()).ok();
859 });
860 cx.spawn(|_, mut cx| async move {
861 let snapshot = snapshot.await;
862 let Some(this) = this.upgrade() else {
863 return;
864 };
865 this.update(&mut cx, |this, _| {
866 this.set_state(snapshot, &buffer);
867 })
868 .log_err();
869 drop(complete_on_drop)
870 })
871 .detach();
872 rx
873 }
874
875 #[cfg(any(test, feature = "test-support"))]
876 pub fn base_text_string(&self) -> Option<String> {
877 self.inner.base_text.as_ref().map(|buffer| buffer.text())
878 }
879
880 pub fn new(buffer: &text::BufferSnapshot) -> Self {
881 BufferDiff {
882 buffer_id: buffer.remote_id(),
883 inner: BufferDiff::build_empty(buffer),
884 secondary_diff: None,
885 }
886 }
887
888 #[cfg(any(test, feature = "test-support"))]
889 pub fn new_with_base_text(
890 base_text: &str,
891 buffer: &Entity<language::Buffer>,
892 cx: &mut App,
893 ) -> Self {
894 let mut base_text = base_text.to_owned();
895 text::LineEnding::normalize(&mut base_text);
896 let snapshot = BufferDiff::build(
897 buffer.read(cx).text_snapshot(),
898 Some(base_text.into()),
899 None,
900 None,
901 cx,
902 );
903 let snapshot = cx.background_executor().block(snapshot);
904 BufferDiff {
905 buffer_id: buffer.read(cx).remote_id(),
906 inner: snapshot,
907 secondary_diff: None,
908 }
909 }
910
911 #[cfg(any(test, feature = "test-support"))]
912 pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
913 let base_text = self
914 .inner
915 .base_text
916 .as_ref()
917 .map(|base_text| base_text.text());
918 let snapshot = BufferDiff::build_with_base_buffer(
919 buffer.clone(),
920 base_text.clone().map(Arc::new),
921 self.inner.base_text.clone(),
922 cx,
923 );
924 let snapshot = cx.background_executor().block(snapshot);
925 let changed_range = self.set_state(snapshot, &buffer);
926 cx.emit(BufferDiffEvent::DiffChanged { changed_range });
927 }
928}
929
930impl DiffHunk {
931 pub fn status(&self) -> DiffHunkStatus {
932 if self.buffer_range.start == self.buffer_range.end {
933 DiffHunkStatus::Removed(self.secondary_status)
934 } else if self.diff_base_byte_range.is_empty() {
935 DiffHunkStatus::Added(self.secondary_status)
936 } else {
937 DiffHunkStatus::Modified(self.secondary_status)
938 }
939 }
940}
941
942impl DiffHunkStatus {
943 pub fn is_removed(&self) -> bool {
944 matches!(self, DiffHunkStatus::Removed(_))
945 }
946
947 #[cfg(any(test, feature = "test-support"))]
948 pub fn removed() -> Self {
949 DiffHunkStatus::Removed(DiffHunkSecondaryStatus::None)
950 }
951
952 #[cfg(any(test, feature = "test-support"))]
953 pub fn added() -> Self {
954 DiffHunkStatus::Added(DiffHunkSecondaryStatus::None)
955 }
956
957 #[cfg(any(test, feature = "test-support"))]
958 pub fn modified() -> Self {
959 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None)
960 }
961}
962
963/// Range (crossing new lines), old, new
964#[cfg(any(test, feature = "test-support"))]
965#[track_caller]
966pub fn assert_hunks<Iter>(
967 diff_hunks: Iter,
968 buffer: &text::BufferSnapshot,
969 diff_base: &str,
970 expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
971) where
972 Iter: Iterator<Item = DiffHunk>,
973{
974 let actual_hunks = diff_hunks
975 .map(|hunk| {
976 (
977 hunk.row_range.clone(),
978 &diff_base[hunk.diff_base_byte_range.clone()],
979 buffer
980 .text_for_range(
981 Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
982 )
983 .collect::<String>(),
984 hunk.status(),
985 )
986 })
987 .collect::<Vec<_>>();
988
989 let expected_hunks: Vec<_> = expected_hunks
990 .iter()
991 .map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
992 .collect();
993
994 assert_eq!(actual_hunks, expected_hunks);
995}
996
997#[cfg(test)]
998mod tests {
999 use std::fmt::Write as _;
1000
1001 use super::*;
1002 use gpui::{AppContext as _, TestAppContext};
1003 use rand::{rngs::StdRng, Rng as _};
1004 use text::{Buffer, BufferId, Rope};
1005 use unindent::Unindent as _;
1006
1007 #[ctor::ctor]
1008 fn init_logger() {
1009 if std::env::var("RUST_LOG").is_ok() {
1010 env_logger::init();
1011 }
1012 }
1013
1014 #[gpui::test]
1015 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1016 let diff_base = "
1017 one
1018 two
1019 three
1020 "
1021 .unindent();
1022
1023 let buffer_text = "
1024 one
1025 HELLO
1026 three
1027 "
1028 .unindent();
1029
1030 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1031 let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
1032 assert_hunks(
1033 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1034 &buffer,
1035 &diff_base,
1036 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified())],
1037 );
1038
1039 buffer.edit([(0..0, "point five\n")]);
1040 diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
1041 assert_hunks(
1042 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1043 &buffer,
1044 &diff_base,
1045 &[
1046 (0..1, "", "point five\n", DiffHunkStatus::added()),
1047 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified()),
1048 ],
1049 );
1050
1051 diff = BufferDiff::build_empty(&buffer);
1052 assert_hunks(
1053 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1054 &buffer,
1055 &diff_base,
1056 &[],
1057 );
1058 }
1059
1060 #[gpui::test]
1061 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1062 let head_text = "
1063 zero
1064 one
1065 two
1066 three
1067 four
1068 five
1069 six
1070 seven
1071 eight
1072 nine
1073 "
1074 .unindent();
1075
1076 let index_text = "
1077 zero
1078 one
1079 TWO
1080 three
1081 FOUR
1082 five
1083 six
1084 seven
1085 eight
1086 NINE
1087 "
1088 .unindent();
1089
1090 let buffer_text = "
1091 zero
1092 one
1093 TWO
1094 three
1095 FOUR
1096 FIVE
1097 six
1098 SEVEN
1099 eight
1100 nine
1101 "
1102 .unindent();
1103
1104 let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1105 let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx);
1106
1107 let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
1108
1109 let expected_hunks = vec![
1110 (
1111 2..3,
1112 "two\n",
1113 "TWO\n",
1114 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None),
1115 ),
1116 (
1117 4..6,
1118 "four\nfive\n",
1119 "FOUR\nFIVE\n",
1120 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1121 ),
1122 (
1123 7..8,
1124 "seven\n",
1125 "SEVEN\n",
1126 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1127 ),
1128 ];
1129
1130 assert_hunks(
1131 uncommitted_diff.hunks_intersecting_range(
1132 Anchor::MIN..Anchor::MAX,
1133 &buffer,
1134 Some(&unstaged_diff),
1135 ),
1136 &buffer,
1137 &head_text,
1138 &expected_hunks,
1139 );
1140 }
1141
1142 #[gpui::test]
1143 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1144 let diff_base = Arc::new(
1145 "
1146 one
1147 two
1148 three
1149 four
1150 five
1151 six
1152 seven
1153 eight
1154 nine
1155 ten
1156 "
1157 .unindent(),
1158 );
1159
1160 let buffer_text = "
1161 A
1162 one
1163 B
1164 two
1165 C
1166 three
1167 HELLO
1168 four
1169 five
1170 SIXTEEN
1171 seven
1172 eight
1173 WORLD
1174 nine
1175
1176 ten
1177
1178 "
1179 .unindent();
1180
1181 let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1182 let diff = cx
1183 .update(|cx| {
1184 BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx)
1185 })
1186 .await;
1187 assert_eq!(
1188 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None)
1189 .count(),
1190 8
1191 );
1192
1193 assert_hunks(
1194 diff.hunks_intersecting_range(
1195 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1196 &buffer,
1197 None,
1198 ),
1199 &buffer,
1200 &diff_base,
1201 &[
1202 (6..7, "", "HELLO\n", DiffHunkStatus::added()),
1203 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified()),
1204 (12..13, "", "WORLD\n", DiffHunkStatus::added()),
1205 ],
1206 );
1207 }
1208
1209 #[gpui::test]
1210 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
1211 let base_text = "
1212 zero
1213 one
1214 two
1215 three
1216 four
1217 five
1218 six
1219 seven
1220 eight
1221 nine
1222 "
1223 .unindent();
1224
1225 let buffer_text_1 = "
1226 one
1227 three
1228 four
1229 five
1230 SIX
1231 seven
1232 eight
1233 NINE
1234 "
1235 .unindent();
1236
1237 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
1238
1239 let empty_diff = BufferDiff::build_empty(&buffer);
1240 let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1241 let range = diff_1.compare(&empty_diff, &buffer).unwrap();
1242 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
1243
1244 // Edit does not affect the diff.
1245 buffer.edit_via_marked_text(
1246 &"
1247 one
1248 three
1249 four
1250 five
1251 «SIX.5»
1252 seven
1253 eight
1254 NINE
1255 "
1256 .unindent(),
1257 );
1258 let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1259 assert_eq!(None, diff_2.compare(&diff_1, &buffer));
1260
1261 // Edit turns a deletion hunk into a modification.
1262 buffer.edit_via_marked_text(
1263 &"
1264 one
1265 «THREE»
1266 four
1267 five
1268 SIX.5
1269 seven
1270 eight
1271 NINE
1272 "
1273 .unindent(),
1274 );
1275 let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1276 let range = diff_3.compare(&diff_2, &buffer).unwrap();
1277 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
1278
1279 // Edit turns a modification hunk into a deletion.
1280 buffer.edit_via_marked_text(
1281 &"
1282 one
1283 THREE
1284 four
1285 five«»
1286 seven
1287 eight
1288 NINE
1289 "
1290 .unindent(),
1291 );
1292 let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1293 let range = diff_4.compare(&diff_3, &buffer).unwrap();
1294 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
1295
1296 // Edit introduces a new insertion hunk.
1297 buffer.edit_via_marked_text(
1298 &"
1299 one
1300 THREE
1301 four«
1302 FOUR.5
1303 »five
1304 seven
1305 eight
1306 NINE
1307 "
1308 .unindent(),
1309 );
1310 let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx);
1311 let range = diff_5.compare(&diff_4, &buffer).unwrap();
1312 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
1313
1314 // Edit removes a hunk.
1315 buffer.edit_via_marked_text(
1316 &"
1317 one
1318 THREE
1319 four
1320 FOUR.5
1321 five
1322 seven
1323 eight
1324 «nine»
1325 "
1326 .unindent(),
1327 );
1328 let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx);
1329 let range = diff_6.compare(&diff_5, &buffer).unwrap();
1330 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
1331 }
1332
1333 #[gpui::test(iterations = 100)]
1334 async fn test_secondary_edits_for_stage_unstage(cx: &mut TestAppContext, mut rng: StdRng) {
1335 fn gen_line(rng: &mut StdRng) -> String {
1336 if rng.gen_bool(0.2) {
1337 "\n".to_owned()
1338 } else {
1339 let c = rng.gen_range('A'..='Z');
1340 format!("{c}{c}{c}\n")
1341 }
1342 }
1343
1344 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
1345 let mut old_lines = {
1346 let mut old_lines = Vec::new();
1347 let mut old_lines_iter = head.lines();
1348 while let Some(line) = old_lines_iter.next() {
1349 assert!(!line.ends_with("\n"));
1350 old_lines.push(line.to_owned());
1351 }
1352 if old_lines.last().is_some_and(|line| line.is_empty()) {
1353 old_lines.pop();
1354 }
1355 old_lines.into_iter()
1356 };
1357 let mut result = String::new();
1358 let unchanged_count = rng.gen_range(0..=old_lines.len());
1359 result +=
1360 &old_lines
1361 .by_ref()
1362 .take(unchanged_count)
1363 .fold(String::new(), |mut s, line| {
1364 writeln!(&mut s, "{line}").unwrap();
1365 s
1366 });
1367 while old_lines.len() > 0 {
1368 let deleted_count = rng.gen_range(0..=old_lines.len());
1369 let _advance = old_lines
1370 .by_ref()
1371 .take(deleted_count)
1372 .map(|line| line.len() + 1)
1373 .sum::<usize>();
1374 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
1375 let added_count = rng.gen_range(minimum_added..=5);
1376 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
1377 result += &addition;
1378
1379 if old_lines.len() > 0 {
1380 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
1381 if blank_lines == old_lines.len() {
1382 break;
1383 };
1384 let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
1385 result += &old_lines.by_ref().take(unchanged_count).fold(
1386 String::new(),
1387 |mut s, line| {
1388 writeln!(&mut s, "{line}").unwrap();
1389 s
1390 },
1391 );
1392 }
1393 }
1394 result
1395 }
1396
1397 fn uncommitted_diff(
1398 working_copy: &language::BufferSnapshot,
1399 index_text: &Entity<language::Buffer>,
1400 head_text: String,
1401 cx: &mut TestAppContext,
1402 ) -> BufferDiff {
1403 let inner = BufferDiff::build_sync(working_copy.text.clone(), head_text, cx);
1404 let secondary = BufferDiff {
1405 buffer_id: working_copy.remote_id(),
1406 inner: BufferDiff::build_sync(
1407 working_copy.text.clone(),
1408 index_text.read_with(cx, |index_text, _| index_text.text()),
1409 cx,
1410 ),
1411 secondary_diff: None,
1412 };
1413 let secondary = cx.new(|_| secondary);
1414 BufferDiff {
1415 buffer_id: working_copy.remote_id(),
1416 inner,
1417 secondary_diff: Some(secondary),
1418 }
1419 }
1420
1421 let operations = std::env::var("OPERATIONS")
1422 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1423 .unwrap_or(10);
1424
1425 let rng = &mut rng;
1426 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
1427 writeln!(&mut s, "{c}{c}{c}").unwrap();
1428 s
1429 });
1430 let working_copy = gen_working_copy(rng, &head_text);
1431 let working_copy = cx.new(|cx| {
1432 language::Buffer::local_normalized(
1433 Rope::from(working_copy.as_str()),
1434 text::LineEnding::default(),
1435 cx,
1436 )
1437 });
1438 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
1439 let index_text = cx.new(|cx| {
1440 language::Buffer::local_normalized(
1441 if rng.gen() {
1442 Rope::from(head_text.as_str())
1443 } else {
1444 working_copy.as_rope().clone()
1445 },
1446 text::LineEnding::default(),
1447 cx,
1448 )
1449 });
1450
1451 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1452 let mut hunks = cx.update(|cx| {
1453 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1454 .collect::<Vec<_>>()
1455 });
1456 if hunks.len() == 0 {
1457 return;
1458 }
1459
1460 for _ in 0..operations {
1461 let i = rng.gen_range(0..hunks.len());
1462 let hunk = &mut hunks[i];
1463 let hunk_fields = (
1464 hunk.diff_base_byte_range.clone(),
1465 hunk.secondary_diff_base_byte_range.clone(),
1466 hunk.buffer_range.clone(),
1467 );
1468 let stage = match (
1469 hunk.secondary_status,
1470 hunk.secondary_diff_base_byte_range.clone(),
1471 ) {
1472 (DiffHunkSecondaryStatus::HasSecondaryHunk, Some(_)) => {
1473 hunk.secondary_status = DiffHunkSecondaryStatus::None;
1474 hunk.secondary_diff_base_byte_range = None;
1475 true
1476 }
1477 (DiffHunkSecondaryStatus::None, None) => {
1478 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1479 // We don't look at this, just notice whether it's Some or not.
1480 hunk.secondary_diff_base_byte_range = Some(17..17);
1481 false
1482 }
1483 _ => unreachable!(),
1484 };
1485
1486 let snapshot = cx.update(|cx| diff.snapshot(cx));
1487 let edits = snapshot.secondary_edits_for_stage_or_unstage(
1488 stage,
1489 [hunk_fields].into_iter(),
1490 &working_copy,
1491 );
1492 index_text.update(cx, |index_text, cx| {
1493 index_text.edit(edits, None, cx);
1494 });
1495
1496 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1497 let found_hunks = cx.update(|cx| {
1498 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1499 .collect::<Vec<_>>()
1500 });
1501 assert_eq!(hunks.len(), found_hunks.len());
1502 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
1503 assert_eq!(
1504 expected_hunk.buffer_range.to_point(&working_copy),
1505 found_hunk.buffer_range.to_point(&working_copy)
1506 );
1507 assert_eq!(
1508 expected_hunk.diff_base_byte_range,
1509 found_hunk.diff_base_byte_range
1510 );
1511 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
1512 assert_eq!(
1513 expected_hunk.secondary_diff_base_byte_range.is_some(),
1514 found_hunk.secondary_diff_base_byte_range.is_some()
1515 )
1516 }
1517 hunks = found_hunks;
1518 }
1519 }
1520}