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