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