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 stage_or_unstage_all_hunks(
1163 &mut self,
1164 stage: bool,
1165 buffer: &text::BufferSnapshot,
1166 file_exists: bool,
1167 cx: &mut Context<Self>,
1168 ) {
1169 let hunks = self
1170 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
1171 .collect::<Vec<_>>();
1172 let Some(secondary) = self.secondary_diff.as_ref() else {
1173 return;
1174 };
1175 self.inner.stage_or_unstage_hunks_impl(
1176 &secondary.read(cx).inner,
1177 stage,
1178 &hunks,
1179 buffer,
1180 file_exists,
1181 );
1182 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1183 let changed_range = first.buffer_range.start..last.buffer_range.end;
1184 cx.emit(BufferDiffEvent::DiffChanged {
1185 changed_range: Some(changed_range),
1186 });
1187 }
1188 }
1189
1190 pub fn range_to_hunk_range(
1191 &self,
1192 range: Range<Anchor>,
1193 buffer: &text::BufferSnapshot,
1194 cx: &App,
1195 ) -> Option<Range<Anchor>> {
1196 let start = self
1197 .hunks_intersecting_range(range.clone(), buffer, cx)
1198 .next()?
1199 .buffer_range
1200 .start;
1201 let end = self
1202 .hunks_intersecting_range_rev(range, buffer)
1203 .next()?
1204 .buffer_range
1205 .end;
1206 Some(start..end)
1207 }
1208
1209 pub async fn update_diff(
1210 this: Entity<BufferDiff>,
1211 buffer: text::BufferSnapshot,
1212 base_text: Option<Arc<String>>,
1213 base_text_changed: bool,
1214 language_changed: bool,
1215 language: Option<Arc<Language>>,
1216 language_registry: Option<Arc<LanguageRegistry>>,
1217 cx: &mut AsyncApp,
1218 ) -> anyhow::Result<BufferDiffSnapshot> {
1219 Ok(if base_text_changed || language_changed {
1220 cx.update(|cx| {
1221 BufferDiffSnapshot::new_with_base_text(
1222 buffer.clone(),
1223 base_text,
1224 language.clone(),
1225 language_registry.clone(),
1226 cx,
1227 )
1228 })?
1229 .await
1230 } else {
1231 this.read_with(cx, |this, cx| {
1232 BufferDiffSnapshot::new_with_base_buffer(
1233 buffer.clone(),
1234 base_text,
1235 this.base_text().clone(),
1236 cx,
1237 )
1238 })?
1239 .await
1240 })
1241 }
1242
1243 pub fn language_changed(&mut self, cx: &mut Context<Self>) {
1244 cx.emit(BufferDiffEvent::LanguageChanged);
1245 }
1246
1247 pub fn set_snapshot(
1248 &mut self,
1249 new_snapshot: BufferDiffSnapshot,
1250 buffer: &text::BufferSnapshot,
1251 cx: &mut Context<Self>,
1252 ) -> Option<Range<Anchor>> {
1253 self.set_snapshot_with_secondary(new_snapshot, buffer, None, false, cx)
1254 }
1255
1256 pub fn set_snapshot_with_secondary(
1257 &mut self,
1258 new_snapshot: BufferDiffSnapshot,
1259 buffer: &text::BufferSnapshot,
1260 secondary_diff_change: Option<Range<Anchor>>,
1261 clear_pending_hunks: bool,
1262 cx: &mut Context<Self>,
1263 ) -> Option<Range<Anchor>> {
1264 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1265
1266 let state = &mut self.inner;
1267 let new_state = new_snapshot.inner;
1268 let (base_text_changed, mut changed_range) =
1269 match (state.base_text_exists, new_state.base_text_exists) {
1270 (false, false) => (true, None),
1271 (true, true)
1272 if state.base_text.remote_id() == new_state.base_text.remote_id()
1273 && state.base_text.syntax_update_count()
1274 == new_state.base_text.syntax_update_count() =>
1275 {
1276 (false, new_state.compare(state, buffer))
1277 }
1278 _ => (
1279 true,
1280 Some(text::Anchor::min_max_range_for_buffer(self.buffer_id)),
1281 ),
1282 };
1283
1284 if let Some(secondary_changed_range) = secondary_diff_change
1285 && let Some(secondary_hunk_range) =
1286 self.range_to_hunk_range(secondary_changed_range, buffer, cx)
1287 {
1288 if let Some(range) = &mut changed_range {
1289 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1290 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1291 } else {
1292 changed_range = Some(secondary_hunk_range);
1293 }
1294 }
1295
1296 let state = &mut self.inner;
1297 state.base_text_exists = new_state.base_text_exists;
1298 state.base_text = new_state.base_text;
1299 state.hunks = new_state.hunks;
1300 if base_text_changed || clear_pending_hunks {
1301 if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1302 {
1303 if let Some(range) = &mut changed_range {
1304 range.start = *range.start.min(&first.buffer_range.start, buffer);
1305 range.end = *range.end.max(&last.buffer_range.end, buffer);
1306 } else {
1307 changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1308 }
1309 }
1310 state.pending_hunks = SumTree::new(buffer);
1311 }
1312
1313 cx.emit(BufferDiffEvent::DiffChanged {
1314 changed_range: changed_range.clone(),
1315 });
1316 changed_range
1317 }
1318
1319 pub fn base_text(&self) -> &language::BufferSnapshot {
1320 &self.inner.base_text
1321 }
1322
1323 pub fn base_text_exists(&self) -> bool {
1324 self.inner.base_text_exists
1325 }
1326
1327 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1328 BufferDiffSnapshot {
1329 inner: self.inner.clone(),
1330 secondary_diff: self
1331 .secondary_diff
1332 .as_ref()
1333 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
1334 }
1335 }
1336
1337 pub fn hunks<'a>(
1338 &'a self,
1339 buffer_snapshot: &'a text::BufferSnapshot,
1340 cx: &'a App,
1341 ) -> impl 'a + Iterator<Item = DiffHunk> {
1342 self.hunks_intersecting_range(
1343 Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
1344 buffer_snapshot,
1345 cx,
1346 )
1347 }
1348
1349 pub fn hunks_intersecting_range<'a>(
1350 &'a self,
1351 range: Range<text::Anchor>,
1352 buffer_snapshot: &'a text::BufferSnapshot,
1353 cx: &'a App,
1354 ) -> impl 'a + Iterator<Item = DiffHunk> {
1355 let unstaged_counterpart = self
1356 .secondary_diff
1357 .as_ref()
1358 .map(|diff| &diff.read(cx).inner);
1359 self.inner
1360 .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
1361 }
1362
1363 pub fn hunks_intersecting_range_rev<'a>(
1364 &'a self,
1365 range: Range<text::Anchor>,
1366 buffer_snapshot: &'a text::BufferSnapshot,
1367 ) -> impl 'a + Iterator<Item = DiffHunk> {
1368 self.inner
1369 .hunks_intersecting_range_rev(range, buffer_snapshot)
1370 }
1371
1372 pub fn hunks_in_row_range<'a>(
1373 &'a self,
1374 range: Range<u32>,
1375 buffer: &'a text::BufferSnapshot,
1376 cx: &'a App,
1377 ) -> impl 'a + Iterator<Item = DiffHunk> {
1378 let start = buffer.anchor_before(Point::new(range.start, 0));
1379 let end = buffer.anchor_after(Point::new(range.end, 0));
1380 self.hunks_intersecting_range(start..end, buffer, cx)
1381 }
1382
1383 /// Used in cases where the change set isn't derived from git.
1384 pub fn set_base_text(
1385 &mut self,
1386 base_text: Option<Arc<String>>,
1387 language: Option<Arc<Language>>,
1388 language_registry: Option<Arc<LanguageRegistry>>,
1389 buffer: text::BufferSnapshot,
1390 cx: &mut Context<Self>,
1391 ) -> oneshot::Receiver<()> {
1392 let (tx, rx) = oneshot::channel();
1393 let this = cx.weak_entity();
1394
1395 let snapshot = BufferDiffSnapshot::new_with_base_text(
1396 buffer.clone(),
1397 base_text,
1398 language,
1399 language_registry,
1400 cx,
1401 );
1402 let complete_on_drop = util::defer(|| {
1403 tx.send(()).ok();
1404 });
1405 cx.spawn(async move |_, cx| {
1406 let snapshot = snapshot.await;
1407 let Some(this) = this.upgrade() else {
1408 return;
1409 };
1410 this.update(cx, |this, cx| {
1411 this.set_snapshot(snapshot, &buffer, cx);
1412 })
1413 .log_err();
1414 drop(complete_on_drop)
1415 })
1416 .detach();
1417 rx
1418 }
1419
1420 pub fn base_text_string(&self) -> Option<String> {
1421 self.inner
1422 .base_text_exists
1423 .then(|| self.inner.base_text.text())
1424 }
1425
1426 #[cfg(any(test, feature = "test-support"))]
1427 pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
1428 let base_text = self.base_text_string().map(Arc::new);
1429 let snapshot = BufferDiffSnapshot::new_with_base_buffer(
1430 buffer.clone(),
1431 base_text,
1432 self.inner.base_text.clone(),
1433 cx,
1434 );
1435 let snapshot = cx.background_executor().block(snapshot);
1436 self.set_snapshot(snapshot, &buffer, cx);
1437 }
1438}
1439
1440impl DiffHunk {
1441 pub fn is_created_file(&self) -> bool {
1442 self.diff_base_byte_range == (0..0)
1443 && self.buffer_range.start.is_min()
1444 && self.buffer_range.end.is_max()
1445 }
1446
1447 pub fn status(&self) -> DiffHunkStatus {
1448 let kind = if self.buffer_range.start == self.buffer_range.end {
1449 DiffHunkStatusKind::Deleted
1450 } else if self.diff_base_byte_range.is_empty() {
1451 DiffHunkStatusKind::Added
1452 } else {
1453 DiffHunkStatusKind::Modified
1454 };
1455 DiffHunkStatus {
1456 kind,
1457 secondary: self.secondary_status,
1458 }
1459 }
1460}
1461
1462impl DiffHunkStatus {
1463 pub fn has_secondary_hunk(&self) -> bool {
1464 matches!(
1465 self.secondary,
1466 DiffHunkSecondaryStatus::HasSecondaryHunk
1467 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1468 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
1469 )
1470 }
1471
1472 pub fn is_pending(&self) -> bool {
1473 matches!(
1474 self.secondary,
1475 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1476 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1477 )
1478 }
1479
1480 pub fn is_deleted(&self) -> bool {
1481 self.kind == DiffHunkStatusKind::Deleted
1482 }
1483
1484 pub fn is_added(&self) -> bool {
1485 self.kind == DiffHunkStatusKind::Added
1486 }
1487
1488 pub fn is_modified(&self) -> bool {
1489 self.kind == DiffHunkStatusKind::Modified
1490 }
1491
1492 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
1493 Self {
1494 kind: DiffHunkStatusKind::Added,
1495 secondary,
1496 }
1497 }
1498
1499 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
1500 Self {
1501 kind: DiffHunkStatusKind::Modified,
1502 secondary,
1503 }
1504 }
1505
1506 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
1507 Self {
1508 kind: DiffHunkStatusKind::Deleted,
1509 secondary,
1510 }
1511 }
1512
1513 pub fn deleted_none() -> Self {
1514 Self {
1515 kind: DiffHunkStatusKind::Deleted,
1516 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1517 }
1518 }
1519
1520 pub fn added_none() -> Self {
1521 Self {
1522 kind: DiffHunkStatusKind::Added,
1523 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1524 }
1525 }
1526
1527 pub fn modified_none() -> Self {
1528 Self {
1529 kind: DiffHunkStatusKind::Modified,
1530 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1531 }
1532 }
1533}
1534
1535#[cfg(any(test, feature = "test-support"))]
1536#[track_caller]
1537pub fn assert_hunks<ExpectedText, HunkIter>(
1538 diff_hunks: HunkIter,
1539 buffer: &text::BufferSnapshot,
1540 diff_base: &str,
1541 // Line range, deleted, added, status
1542 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
1543) where
1544 HunkIter: Iterator<Item = DiffHunk>,
1545 ExpectedText: AsRef<str>,
1546{
1547 let actual_hunks = diff_hunks
1548 .map(|hunk| {
1549 (
1550 hunk.range.clone(),
1551 &diff_base[hunk.diff_base_byte_range.clone()],
1552 buffer
1553 .text_for_range(hunk.range.clone())
1554 .collect::<String>(),
1555 hunk.status(),
1556 )
1557 })
1558 .collect::<Vec<_>>();
1559
1560 let expected_hunks: Vec<_> = expected_hunks
1561 .iter()
1562 .map(|(line_range, deleted_text, added_text, status)| {
1563 (
1564 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
1565 deleted_text.as_ref(),
1566 added_text.as_ref().to_string(),
1567 *status,
1568 )
1569 })
1570 .collect();
1571
1572 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
1573}
1574
1575#[cfg(test)]
1576mod tests {
1577 use std::fmt::Write as _;
1578
1579 use super::*;
1580 use gpui::TestAppContext;
1581 use pretty_assertions::{assert_eq, assert_ne};
1582 use rand::{Rng as _, rngs::StdRng};
1583 use text::{Buffer, BufferId, ReplicaId, Rope};
1584 use unindent::Unindent as _;
1585 use util::test::marked_text_ranges;
1586
1587 #[ctor::ctor]
1588 fn init_logger() {
1589 zlog::init_test();
1590 }
1591
1592 #[gpui::test]
1593 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1594 let diff_base = "
1595 one
1596 two
1597 three
1598 "
1599 .unindent();
1600
1601 let buffer_text = "
1602 one
1603 HELLO
1604 three
1605 "
1606 .unindent();
1607
1608 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1609 let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1610 assert_hunks(
1611 diff.hunks_intersecting_range(
1612 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1613 &buffer,
1614 ),
1615 &buffer,
1616 &diff_base,
1617 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
1618 );
1619
1620 buffer.edit([(0..0, "point five\n")]);
1621 diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1622 assert_hunks(
1623 diff.hunks_intersecting_range(
1624 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1625 &buffer,
1626 ),
1627 &buffer,
1628 &diff_base,
1629 &[
1630 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
1631 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
1632 ],
1633 );
1634
1635 diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
1636 assert_hunks::<&str, _>(
1637 diff.hunks_intersecting_range(
1638 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1639 &buffer,
1640 ),
1641 &buffer,
1642 &diff_base,
1643 &[],
1644 );
1645 }
1646
1647 #[gpui::test]
1648 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1649 let head_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 index_text = "
1664 zero
1665 one
1666 TWO
1667 three
1668 FOUR
1669 five
1670 six
1671 seven
1672 eight
1673 NINE
1674 "
1675 .unindent();
1676
1677 let buffer_text = "
1678 zero
1679 one
1680 TWO
1681 three
1682 FOUR
1683 FIVE
1684 six
1685 SEVEN
1686 eight
1687 nine
1688 "
1689 .unindent();
1690
1691 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1692 let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1693 let mut uncommitted_diff =
1694 BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1695 uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
1696
1697 let expected_hunks = vec![
1698 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
1699 (
1700 4..6,
1701 "four\nfive\n",
1702 "FOUR\nFIVE\n",
1703 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1704 ),
1705 (
1706 7..8,
1707 "seven\n",
1708 "SEVEN\n",
1709 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1710 ),
1711 ];
1712
1713 assert_hunks(
1714 uncommitted_diff.hunks_intersecting_range(
1715 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1716 &buffer,
1717 ),
1718 &buffer,
1719 &head_text,
1720 &expected_hunks,
1721 );
1722 }
1723
1724 #[gpui::test]
1725 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1726 let diff_base = Arc::new(
1727 "
1728 one
1729 two
1730 three
1731 four
1732 five
1733 six
1734 seven
1735 eight
1736 nine
1737 ten
1738 "
1739 .unindent(),
1740 );
1741
1742 let buffer_text = "
1743 A
1744 one
1745 B
1746 two
1747 C
1748 three
1749 HELLO
1750 four
1751 five
1752 SIXTEEN
1753 seven
1754 eight
1755 WORLD
1756 nine
1757
1758 ten
1759
1760 "
1761 .unindent();
1762
1763 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1764 let diff = cx
1765 .update(|cx| {
1766 BufferDiffSnapshot::new_with_base_text(
1767 buffer.snapshot(),
1768 Some(diff_base.clone()),
1769 None,
1770 None,
1771 cx,
1772 )
1773 })
1774 .await;
1775 assert_eq!(
1776 diff.hunks_intersecting_range(
1777 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1778 &buffer
1779 )
1780 .count(),
1781 8
1782 );
1783
1784 assert_hunks(
1785 diff.hunks_intersecting_range(
1786 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1787 &buffer,
1788 ),
1789 &buffer,
1790 &diff_base,
1791 &[
1792 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
1793 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
1794 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
1795 ],
1796 );
1797 }
1798
1799 #[gpui::test]
1800 async fn test_stage_hunk(cx: &mut TestAppContext) {
1801 struct Example {
1802 name: &'static str,
1803 head_text: String,
1804 index_text: String,
1805 buffer_marked_text: String,
1806 final_index_text: String,
1807 }
1808
1809 let table = [
1810 Example {
1811 name: "uncommitted hunk straddles end of unstaged hunk",
1812 head_text: "
1813 one
1814 two
1815 three
1816 four
1817 five
1818 "
1819 .unindent(),
1820 index_text: "
1821 one
1822 TWO_HUNDRED
1823 three
1824 FOUR_HUNDRED
1825 five
1826 "
1827 .unindent(),
1828 buffer_marked_text: "
1829 ZERO
1830 one
1831 two
1832 «THREE_HUNDRED
1833 FOUR_HUNDRED»
1834 five
1835 SIX
1836 "
1837 .unindent(),
1838 final_index_text: "
1839 one
1840 two
1841 THREE_HUNDRED
1842 FOUR_HUNDRED
1843 five
1844 "
1845 .unindent(),
1846 },
1847 Example {
1848 name: "uncommitted hunk straddles start of unstaged hunk",
1849 head_text: "
1850 one
1851 two
1852 three
1853 four
1854 five
1855 "
1856 .unindent(),
1857 index_text: "
1858 one
1859 TWO_HUNDRED
1860 three
1861 FOUR_HUNDRED
1862 five
1863 "
1864 .unindent(),
1865 buffer_marked_text: "
1866 ZERO
1867 one
1868 «TWO_HUNDRED
1869 THREE_HUNDRED»
1870 four
1871 five
1872 SIX
1873 "
1874 .unindent(),
1875 final_index_text: "
1876 one
1877 TWO_HUNDRED
1878 THREE_HUNDRED
1879 four
1880 five
1881 "
1882 .unindent(),
1883 },
1884 Example {
1885 name: "uncommitted hunk strictly contains unstaged hunks",
1886 head_text: "
1887 one
1888 two
1889 three
1890 four
1891 five
1892 six
1893 seven
1894 "
1895 .unindent(),
1896 index_text: "
1897 one
1898 TWO
1899 THREE
1900 FOUR
1901 FIVE
1902 SIX
1903 seven
1904 "
1905 .unindent(),
1906 buffer_marked_text: "
1907 one
1908 TWO
1909 «THREE_HUNDRED
1910 FOUR
1911 FIVE_HUNDRED»
1912 SIX
1913 seven
1914 "
1915 .unindent(),
1916 final_index_text: "
1917 one
1918 TWO
1919 THREE_HUNDRED
1920 FOUR
1921 FIVE_HUNDRED
1922 SIX
1923 seven
1924 "
1925 .unindent(),
1926 },
1927 Example {
1928 name: "uncommitted deletion hunk",
1929 head_text: "
1930 one
1931 two
1932 three
1933 four
1934 five
1935 "
1936 .unindent(),
1937 index_text: "
1938 one
1939 two
1940 three
1941 four
1942 five
1943 "
1944 .unindent(),
1945 buffer_marked_text: "
1946 one
1947 ˇfive
1948 "
1949 .unindent(),
1950 final_index_text: "
1951 one
1952 five
1953 "
1954 .unindent(),
1955 },
1956 Example {
1957 name: "one unstaged hunk that contains two uncommitted hunks",
1958 head_text: "
1959 one
1960 two
1961
1962 three
1963 four
1964 "
1965 .unindent(),
1966 index_text: "
1967 one
1968 two
1969 three
1970 four
1971 "
1972 .unindent(),
1973 buffer_marked_text: "
1974 «one
1975
1976 three // modified
1977 four»
1978 "
1979 .unindent(),
1980 final_index_text: "
1981 one
1982
1983 three // modified
1984 four
1985 "
1986 .unindent(),
1987 },
1988 Example {
1989 name: "one uncommitted hunk that contains two unstaged hunks",
1990 head_text: "
1991 one
1992 two
1993 three
1994 four
1995 five
1996 "
1997 .unindent(),
1998 index_text: "
1999 ZERO
2000 one
2001 TWO
2002 THREE
2003 FOUR
2004 five
2005 "
2006 .unindent(),
2007 buffer_marked_text: "
2008 «one
2009 TWO_HUNDRED
2010 THREE
2011 FOUR_HUNDRED
2012 five»
2013 "
2014 .unindent(),
2015 final_index_text: "
2016 ZERO
2017 one
2018 TWO_HUNDRED
2019 THREE
2020 FOUR_HUNDRED
2021 five
2022 "
2023 .unindent(),
2024 },
2025 ];
2026
2027 for example in table {
2028 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2029 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2030 let hunk_range =
2031 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2032
2033 let unstaged =
2034 BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
2035 let uncommitted =
2036 BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
2037
2038 let unstaged_diff = cx.new(|cx| {
2039 let mut diff = BufferDiff::new(&buffer, cx);
2040 diff.set_snapshot(unstaged, &buffer, cx);
2041 diff
2042 });
2043
2044 let uncommitted_diff = cx.new(|cx| {
2045 let mut diff = BufferDiff::new(&buffer, cx);
2046 diff.set_snapshot(uncommitted, &buffer, cx);
2047 diff.set_secondary_diff(unstaged_diff);
2048 diff
2049 });
2050
2051 uncommitted_diff.update(cx, |diff, cx| {
2052 let hunks = diff
2053 .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
2054 .collect::<Vec<_>>();
2055 for hunk in &hunks {
2056 assert_ne!(
2057 hunk.secondary_status,
2058 DiffHunkSecondaryStatus::NoSecondaryHunk
2059 )
2060 }
2061
2062 let new_index_text = diff
2063 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2064 .unwrap()
2065 .to_string();
2066
2067 let hunks = diff
2068 .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
2069 .collect::<Vec<_>>();
2070 for hunk in &hunks {
2071 assert_eq!(
2072 hunk.secondary_status,
2073 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2074 )
2075 }
2076
2077 pretty_assertions::assert_eq!(
2078 new_index_text,
2079 example.final_index_text,
2080 "example: {}",
2081 example.name
2082 );
2083 });
2084 }
2085 }
2086
2087 #[gpui::test]
2088 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2089 let head_text = "
2090 one
2091 two
2092 three
2093 "
2094 .unindent();
2095 let index_text = head_text.clone();
2096 let buffer_text = "
2097 one
2098 three
2099 "
2100 .unindent();
2101
2102 let buffer = Buffer::new(
2103 ReplicaId::LOCAL,
2104 BufferId::new(1).unwrap(),
2105 buffer_text.clone(),
2106 );
2107 let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
2108 let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
2109 let unstaged_diff = cx.new(|cx| {
2110 let mut diff = BufferDiff::new(&buffer, cx);
2111 diff.set_snapshot(unstaged, &buffer, cx);
2112 diff
2113 });
2114 let uncommitted_diff = cx.new(|cx| {
2115 let mut diff = BufferDiff::new(&buffer, cx);
2116 diff.set_snapshot(uncommitted, &buffer, cx);
2117 diff.set_secondary_diff(unstaged_diff.clone());
2118 diff
2119 });
2120
2121 uncommitted_diff.update(cx, |diff, cx| {
2122 let hunk = diff.hunks(&buffer, cx).next().unwrap();
2123
2124 let new_index_text = diff
2125 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
2126 .unwrap()
2127 .to_string();
2128 assert_eq!(new_index_text, buffer_text);
2129
2130 let hunk = diff.hunks(&buffer, cx).next().unwrap();
2131 assert_eq!(
2132 hunk.secondary_status,
2133 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2134 );
2135
2136 let index_text = diff
2137 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
2138 .unwrap()
2139 .to_string();
2140 assert_eq!(index_text, head_text);
2141
2142 let hunk = diff.hunks(&buffer, cx).next().unwrap();
2143 // optimistically unstaged (fine, could also be HasSecondaryHunk)
2144 assert_eq!(
2145 hunk.secondary_status,
2146 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2147 );
2148 });
2149 }
2150
2151 #[gpui::test]
2152 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
2153 let base_text = "
2154 zero
2155 one
2156 two
2157 three
2158 four
2159 five
2160 six
2161 seven
2162 eight
2163 nine
2164 "
2165 .unindent();
2166
2167 let buffer_text_1 = "
2168 one
2169 three
2170 four
2171 five
2172 SIX
2173 seven
2174 eight
2175 NINE
2176 "
2177 .unindent();
2178
2179 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
2180
2181 let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
2182 let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2183 let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
2184 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
2185
2186 // Edit does affects the diff because it recalculates word diffs.
2187 buffer.edit_via_marked_text(
2188 &"
2189 one
2190 three
2191 four
2192 five
2193 «SIX.5»
2194 seven
2195 eight
2196 NINE
2197 "
2198 .unindent(),
2199 );
2200 let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2201 assert_eq!(
2202 Point::new(4, 0)..Point::new(5, 0),
2203 diff_2
2204 .inner
2205 .compare(&diff_1.inner, &buffer)
2206 .unwrap()
2207 .to_point(&buffer)
2208 );
2209
2210 // Edit turns a deletion hunk into a modification.
2211 buffer.edit_via_marked_text(
2212 &"
2213 one
2214 «THREE»
2215 four
2216 five
2217 SIX.5
2218 seven
2219 eight
2220 NINE
2221 "
2222 .unindent(),
2223 );
2224 let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2225 let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap();
2226 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2227
2228 // Edit turns a modification hunk into a deletion.
2229 buffer.edit_via_marked_text(
2230 &"
2231 one
2232 THREE
2233 four
2234 five«»
2235 seven
2236 eight
2237 NINE
2238 "
2239 .unindent(),
2240 );
2241 let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2242 let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap();
2243 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2244
2245 // Edit introduces a new insertion hunk.
2246 buffer.edit_via_marked_text(
2247 &"
2248 one
2249 THREE
2250 four«
2251 FOUR.5
2252 »five
2253 seven
2254 eight
2255 NINE
2256 "
2257 .unindent(),
2258 );
2259 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2260 let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap();
2261 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2262
2263 // Edit removes a hunk.
2264 buffer.edit_via_marked_text(
2265 &"
2266 one
2267 THREE
2268 four
2269 FOUR.5
2270 five
2271 seven
2272 eight
2273 «nine»
2274 "
2275 .unindent(),
2276 );
2277 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
2278 let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap();
2279 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2280 }
2281
2282 #[gpui::test(iterations = 100)]
2283 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
2284 fn gen_line(rng: &mut StdRng) -> String {
2285 if rng.random_bool(0.2) {
2286 "\n".to_owned()
2287 } else {
2288 let c = rng.random_range('A'..='Z');
2289 format!("{c}{c}{c}\n")
2290 }
2291 }
2292
2293 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
2294 let mut old_lines = {
2295 let mut old_lines = Vec::new();
2296 let old_lines_iter = head.lines();
2297 for line in old_lines_iter {
2298 assert!(!line.ends_with("\n"));
2299 old_lines.push(line.to_owned());
2300 }
2301 if old_lines.last().is_some_and(|line| line.is_empty()) {
2302 old_lines.pop();
2303 }
2304 old_lines.into_iter()
2305 };
2306 let mut result = String::new();
2307 let unchanged_count = rng.random_range(0..=old_lines.len());
2308 result +=
2309 &old_lines
2310 .by_ref()
2311 .take(unchanged_count)
2312 .fold(String::new(), |mut s, line| {
2313 writeln!(&mut s, "{line}").unwrap();
2314 s
2315 });
2316 while old_lines.len() > 0 {
2317 let deleted_count = rng.random_range(0..=old_lines.len());
2318 let _advance = old_lines
2319 .by_ref()
2320 .take(deleted_count)
2321 .map(|line| line.len() + 1)
2322 .sum::<usize>();
2323 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
2324 let added_count = rng.random_range(minimum_added..=5);
2325 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
2326 result += &addition;
2327
2328 if old_lines.len() > 0 {
2329 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
2330 if blank_lines == old_lines.len() {
2331 break;
2332 };
2333 let unchanged_count =
2334 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
2335 result += &old_lines.by_ref().take(unchanged_count).fold(
2336 String::new(),
2337 |mut s, line| {
2338 writeln!(&mut s, "{line}").unwrap();
2339 s
2340 },
2341 );
2342 }
2343 }
2344 result
2345 }
2346
2347 fn uncommitted_diff(
2348 working_copy: &language::BufferSnapshot,
2349 index_text: &Rope,
2350 head_text: String,
2351 cx: &mut TestAppContext,
2352 ) -> Entity<BufferDiff> {
2353 let inner =
2354 BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
2355 let secondary = BufferDiff {
2356 buffer_id: working_copy.remote_id(),
2357 inner: BufferDiffSnapshot::new_sync(
2358 working_copy.text.clone(),
2359 index_text.to_string(),
2360 cx,
2361 )
2362 .inner,
2363 secondary_diff: None,
2364 };
2365 let secondary = cx.new(|_| secondary);
2366 cx.new(|_| BufferDiff {
2367 buffer_id: working_copy.remote_id(),
2368 inner,
2369 secondary_diff: Some(secondary),
2370 })
2371 }
2372
2373 let operations = std::env::var("OPERATIONS")
2374 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2375 .unwrap_or(10);
2376
2377 let rng = &mut rng;
2378 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
2379 writeln!(&mut s, "{c}{c}{c}").unwrap();
2380 s
2381 });
2382 let working_copy = gen_working_copy(rng, &head_text);
2383 let working_copy = cx.new(|cx| {
2384 language::Buffer::local_normalized(
2385 Rope::from(working_copy.as_str()),
2386 text::LineEnding::default(),
2387 cx,
2388 )
2389 });
2390 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
2391 let mut index_text = if rng.random() {
2392 Rope::from(head_text.as_str())
2393 } else {
2394 working_copy.as_rope().clone()
2395 };
2396
2397 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2398 let mut hunks = diff.update(cx, |diff, cx| {
2399 diff.hunks_intersecting_range(
2400 Anchor::min_max_range_for_buffer(diff.buffer_id),
2401 &working_copy,
2402 cx,
2403 )
2404 .collect::<Vec<_>>()
2405 });
2406 if hunks.is_empty() {
2407 return;
2408 }
2409
2410 for _ in 0..operations {
2411 let i = rng.random_range(0..hunks.len());
2412 let hunk = &mut hunks[i];
2413 let hunk_to_change = hunk.clone();
2414 let stage = match hunk.secondary_status {
2415 DiffHunkSecondaryStatus::HasSecondaryHunk => {
2416 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
2417 true
2418 }
2419 DiffHunkSecondaryStatus::NoSecondaryHunk => {
2420 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
2421 false
2422 }
2423 _ => unreachable!(),
2424 };
2425
2426 index_text = diff.update(cx, |diff, cx| {
2427 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
2428 .unwrap()
2429 });
2430
2431 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2432 let found_hunks = diff.update(cx, |diff, cx| {
2433 diff.hunks_intersecting_range(
2434 Anchor::min_max_range_for_buffer(diff.buffer_id),
2435 &working_copy,
2436 cx,
2437 )
2438 .collect::<Vec<_>>()
2439 });
2440 assert_eq!(hunks.len(), found_hunks.len());
2441
2442 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
2443 assert_eq!(
2444 expected_hunk.buffer_range.to_point(&working_copy),
2445 found_hunk.buffer_range.to_point(&working_copy)
2446 );
2447 assert_eq!(
2448 expected_hunk.diff_base_byte_range,
2449 found_hunk.diff_base_byte_range
2450 );
2451 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
2452 }
2453 hunks = found_hunks;
2454 }
2455 }
2456
2457 #[gpui::test]
2458 async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
2459 let base_text = "
2460 zero
2461 one
2462 two
2463 three
2464 four
2465 five
2466 six
2467 seven
2468 eight
2469 "
2470 .unindent();
2471 let buffer_text = "
2472 zero
2473 ONE
2474 two
2475 NINE
2476 five
2477 seven
2478 "
2479 .unindent();
2480
2481 // zero
2482 // - one
2483 // + ONE
2484 // two
2485 // - three
2486 // - four
2487 // + NINE
2488 // five
2489 // - six
2490 // seven
2491 // + eight
2492
2493 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2494 let buffer_snapshot = buffer.snapshot();
2495 let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
2496 let expected_results = [
2497 // don't format me
2498 (0, 0),
2499 (1, 2),
2500 (2, 2),
2501 (3, 5),
2502 (4, 5),
2503 (5, 7),
2504 (6, 9),
2505 ];
2506 for (buffer_row, expected) in expected_results {
2507 assert_eq!(
2508 diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
2509 expected,
2510 "{buffer_row}"
2511 );
2512 }
2513 }
2514}