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