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