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