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