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 Capability, Diff, 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, RangeInclusive},
14 sync::Arc,
15};
16use sum_tree::SumTree;
17use text::{
18 Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _,
19};
20use util::ResultExt;
21
22pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
23
24pub struct BufferDiff {
25 pub buffer_id: BufferId,
26 inner: BufferDiffInner<Entity<language::Buffer>>,
27 secondary_diff: Option<Entity<BufferDiff>>,
28}
29
30#[derive(Clone)]
31pub struct BufferDiffSnapshot {
32 inner: BufferDiffInner<language::BufferSnapshot>,
33 secondary_diff: Option<Arc<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)]
46pub struct BufferDiffUpdate {
47 inner: BufferDiffInner<Arc<str>>,
48 buffer_snapshot: text::BufferSnapshot,
49 base_text_edits: Option<Diff>,
50 base_text_changed: bool,
51}
52
53#[derive(Clone)]
54struct BufferDiffInner<BaseText> {
55 hunks: SumTree<InternalDiffHunk>,
56 pending_hunks: SumTree<PendingHunk>,
57 base_text: BaseText,
58 base_text_exists: bool,
59 buffer_snapshot: text::BufferSnapshot,
60}
61
62impl<BaseText> BufferDiffInner<BaseText> {
63 fn buffer_version(&self) -> &clock::Global {
64 self.buffer_snapshot.version()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct DiffHunkStatus {
70 pub kind: DiffHunkStatusKind,
71 pub secondary: DiffHunkSecondaryStatus,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum DiffHunkStatusKind {
76 Added,
77 Modified,
78 Deleted,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82/// Diff of Working Copy vs Index
83/// aka 'is this hunk staged or not'
84pub enum DiffHunkSecondaryStatus {
85 /// Unstaged
86 HasSecondaryHunk,
87 /// Partially staged
88 OverlapsWithSecondaryHunk,
89 /// Staged
90 NoSecondaryHunk,
91 /// We are unstaging
92 SecondaryHunkAdditionPending,
93 /// We are stagind
94 SecondaryHunkRemovalPending,
95}
96
97/// A diff hunk resolved to rows in the buffer.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct DiffHunk {
100 /// The buffer range as points.
101 pub range: Range<Point>,
102 /// The range in the buffer to which this hunk corresponds.
103 pub buffer_range: Range<Anchor>,
104 /// The range in the buffer's diff base text to which this hunk corresponds.
105 pub diff_base_byte_range: Range<usize>,
106 pub secondary_status: DiffHunkSecondaryStatus,
107 // Anchors representing the word diff locations in the active buffer
108 pub buffer_word_diffs: Vec<Range<Anchor>>,
109 // Offsets relative to the start of the deleted diff that represent word diff locations
110 pub base_word_diffs: Vec<Range<usize>>,
111}
112
113/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
114#[derive(Debug, Clone, PartialEq, Eq)]
115struct InternalDiffHunk {
116 buffer_range: Range<Anchor>,
117 diff_base_byte_range: Range<usize>,
118 base_word_diffs: Vec<Range<usize>>,
119 buffer_word_diffs: Vec<Range<Anchor>>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123struct PendingHunk {
124 buffer_range: Range<Anchor>,
125 diff_base_byte_range: Range<usize>,
126 buffer_version: clock::Global,
127 new_status: DiffHunkSecondaryStatus,
128}
129
130#[derive(Debug, Clone)]
131pub struct DiffHunkSummary {
132 buffer_range: Range<Anchor>,
133 diff_base_byte_range: Range<usize>,
134}
135
136impl sum_tree::Item for InternalDiffHunk {
137 type Summary = DiffHunkSummary;
138
139 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
140 DiffHunkSummary {
141 buffer_range: self.buffer_range.clone(),
142 diff_base_byte_range: self.diff_base_byte_range.clone(),
143 }
144 }
145}
146
147impl sum_tree::Item for PendingHunk {
148 type Summary = DiffHunkSummary;
149
150 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
151 DiffHunkSummary {
152 buffer_range: self.buffer_range.clone(),
153 diff_base_byte_range: self.diff_base_byte_range.clone(),
154 }
155 }
156}
157
158impl sum_tree::Summary for DiffHunkSummary {
159 type Context<'a> = &'a text::BufferSnapshot;
160
161 fn zero(_cx: Self::Context<'_>) -> Self {
162 DiffHunkSummary {
163 buffer_range: Anchor::MIN..Anchor::MIN,
164 diff_base_byte_range: 0..0,
165 }
166 }
167
168 fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
169 self.buffer_range.start = *self
170 .buffer_range
171 .start
172 .min(&other.buffer_range.start, buffer);
173 self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
174
175 self.diff_base_byte_range.start = self
176 .diff_base_byte_range
177 .start
178 .min(other.diff_base_byte_range.start);
179 self.diff_base_byte_range.end = self
180 .diff_base_byte_range
181 .end
182 .max(other.diff_base_byte_range.end);
183 }
184}
185
186impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
187 fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
188 if self
189 .cmp(&cursor_location.buffer_range.start, buffer)
190 .is_lt()
191 {
192 Ordering::Less
193 } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
194 Ordering::Greater
195 } else {
196 Ordering::Equal
197 }
198 }
199}
200
201impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for usize {
202 fn cmp(&self, cursor_location: &DiffHunkSummary, _cx: &text::BufferSnapshot) -> Ordering {
203 if *self < cursor_location.diff_base_byte_range.start {
204 Ordering::Less
205 } else if *self > cursor_location.diff_base_byte_range.end {
206 Ordering::Greater
207 } else {
208 Ordering::Equal
209 }
210 }
211}
212
213impl std::fmt::Debug for BufferDiffInner<language::BufferSnapshot> {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("BufferDiffSnapshot")
216 .field("hunks", &self.hunks)
217 .field("remote_id", &self.base_text.remote_id())
218 .finish()
219 }
220}
221
222impl BufferDiffSnapshot {
223 #[cfg(test)]
224 fn new_sync(
225 buffer: &text::BufferSnapshot,
226 diff_base: String,
227 cx: &mut gpui::TestAppContext,
228 ) -> BufferDiffSnapshot {
229 let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, buffer, cx));
230 buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx))
231 }
232
233 pub fn is_empty(&self) -> bool {
234 self.inner.hunks.is_empty()
235 }
236
237 pub fn base_text_string(&self) -> Option<String> {
238 self.inner
239 .base_text_exists
240 .then(|| self.inner.base_text.text())
241 }
242
243 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
244 self.secondary_diff.as_deref()
245 }
246
247 pub fn buffer_version(&self) -> &clock::Global {
248 self.inner.buffer_version()
249 }
250
251 fn original_buffer_snapshot(&self) -> &text::BufferSnapshot {
252 &self.inner.buffer_snapshot
253 }
254
255 #[ztracing::instrument(skip_all)]
256 pub fn hunks_intersecting_range<'a>(
257 &'a self,
258 range: Range<Anchor>,
259 buffer: &'a text::BufferSnapshot,
260 ) -> impl 'a + Iterator<Item = DiffHunk> {
261 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
262 self.inner
263 .hunks_intersecting_range(range, buffer, unstaged_counterpart)
264 }
265
266 pub fn hunks_intersecting_range_rev<'a>(
267 &'a self,
268 range: Range<Anchor>,
269 buffer: &'a text::BufferSnapshot,
270 ) -> impl 'a + Iterator<Item = DiffHunk> {
271 let filter = move |summary: &DiffHunkSummary| {
272 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
273 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
274 !before_start && !after_end
275 };
276 self.inner.hunks_intersecting_range_rev_impl(filter, buffer)
277 }
278
279 pub fn hunks_intersecting_base_text_range<'a>(
280 &'a self,
281 range: Range<usize>,
282 main_buffer: &'a text::BufferSnapshot,
283 ) -> impl 'a + Iterator<Item = DiffHunk> {
284 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
285 let filter = move |summary: &DiffHunkSummary| {
286 let before_start = summary.diff_base_byte_range.end < range.start;
287 let after_end = summary.diff_base_byte_range.start > range.end;
288 !before_start && !after_end
289 };
290 self.inner
291 .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart)
292 }
293
294 pub fn hunks_intersecting_base_text_range_rev<'a>(
295 &'a self,
296 range: Range<usize>,
297 main_buffer: &'a text::BufferSnapshot,
298 ) -> impl 'a + Iterator<Item = DiffHunk> {
299 let filter = move |summary: &DiffHunkSummary| {
300 let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt();
301 let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt();
302 !before_start && !after_end
303 };
304 self.inner
305 .hunks_intersecting_range_rev_impl(filter, main_buffer)
306 }
307
308 pub fn hunks<'a>(
309 &'a self,
310 buffer_snapshot: &'a text::BufferSnapshot,
311 ) -> impl 'a + Iterator<Item = DiffHunk> {
312 self.hunks_intersecting_range(
313 Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
314 buffer_snapshot,
315 )
316 }
317
318 pub fn hunks_in_row_range<'a>(
319 &'a self,
320 range: Range<u32>,
321 buffer: &'a text::BufferSnapshot,
322 ) -> impl 'a + Iterator<Item = DiffHunk> {
323 let start = buffer.anchor_before(Point::new(range.start, 0));
324 let end = buffer.anchor_after(Point::new(range.end, 0));
325 self.hunks_intersecting_range(start..end, buffer)
326 }
327
328 pub fn range_to_hunk_range(
329 &self,
330 range: Range<Anchor>,
331 buffer: &text::BufferSnapshot,
332 ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
333 let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next();
334 let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next();
335 let range = first_hunk
336 .as_ref()
337 .zip(last_hunk.as_ref())
338 .map(|(first, last)| first.buffer_range.start..last.buffer_range.end);
339 let base_text_range = first_hunk
340 .zip(last_hunk)
341 .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end);
342 (range, base_text_range)
343 }
344
345 pub fn base_text(&self) -> &language::BufferSnapshot {
346 &self.inner.base_text
347 }
348
349 /// If this function returns `true`, the base texts are equal. If this
350 /// function returns `false`, they might be equal, but might not. This
351 /// result is used to avoid recalculating diffs in situations where we know
352 /// nothing has changed.
353 pub fn base_texts_definitely_eq(&self, other: &Self) -> bool {
354 if self.inner.base_text_exists != other.inner.base_text_exists {
355 return false;
356 }
357 let left = &self.inner.base_text;
358 let right = &other.inner.base_text;
359 let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty());
360 let (new_id, new_version, new_empty) =
361 (right.remote_id(), right.version(), right.is_empty());
362 (new_id == old_id && new_version == old_version) || (new_empty && old_empty)
363 }
364
365 /// Returns the last hunk whose start is less than or equal to the given position.
366 fn hunk_before_base_text_offset<'a>(
367 &self,
368 target: usize,
369 cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
370 ) -> Option<&'a InternalDiffHunk> {
371 cursor.seek_forward(&target, Bias::Left);
372 if cursor
373 .item()
374 .is_none_or(|hunk| target < hunk.diff_base_byte_range.start)
375 {
376 cursor.prev();
377 }
378 cursor
379 .item()
380 .filter(|hunk| target >= hunk.diff_base_byte_range.start)
381 }
382
383 fn hunk_before_buffer_anchor<'a>(
384 &self,
385 target: Anchor,
386 cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
387 buffer: &text::BufferSnapshot,
388 ) -> Option<&'a InternalDiffHunk> {
389 cursor.seek_forward(&target, Bias::Left);
390 if cursor
391 .item()
392 .is_none_or(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_lt())
393 {
394 cursor.prev();
395 }
396 cursor
397 .item()
398 .filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge())
399 }
400
401 /// Returns a patch mapping the provided main buffer snapshot to the base text of this diff.
402 ///
403 /// The returned patch is guaranteed to be accurate for all main buffer points in the provided range,
404 /// but not necessarily for points outside that range.
405 pub fn patch_for_buffer_range<'a>(
406 &'a self,
407 range: RangeInclusive<Point>,
408 buffer: &'a text::BufferSnapshot,
409 ) -> Patch<Point> {
410 if !self.inner.base_text_exists {
411 return Patch::new(vec![Edit {
412 old: Point::zero()..buffer.max_point(),
413 new: Point::zero()..Point::zero(),
414 }]);
415 }
416
417 let mut edits_since_diff = Patch::new(
418 buffer
419 .edits_since::<Point>(&self.inner.buffer_snapshot.version)
420 .collect::<Vec<_>>(),
421 );
422 edits_since_diff.invert();
423
424 let mut start_point = edits_since_diff.old_to_new(*range.start());
425 if let Some(first_edit) = edits_since_diff.edits().first() {
426 start_point = start_point.min(first_edit.new.start);
427 }
428
429 let original_snapshot = self.original_buffer_snapshot();
430 let base_text = self.base_text();
431
432 let mut cursor = self.inner.hunks.cursor(original_snapshot);
433 self.hunk_before_buffer_anchor(
434 original_snapshot.anchor_before(start_point),
435 &mut cursor,
436 original_snapshot,
437 );
438 if cursor.item().is_none() {
439 cursor.next();
440 }
441
442 let mut prefix_edit = cursor.prev_item().map(|prev_hunk| Edit {
443 old: Point::zero()..prev_hunk.buffer_range.end.to_point(original_snapshot),
444 new: Point::zero()..prev_hunk.diff_base_byte_range.end.to_point(base_text),
445 });
446
447 let mut range_end = edits_since_diff.old_to_new(*range.end());
448 if let Some(last_edit) = edits_since_diff.edits().last() {
449 range_end = range_end.max(last_edit.new.end);
450 }
451 let range_end = original_snapshot.anchor_before(range_end);
452
453 let hunk_iter = std::iter::from_fn(move || {
454 if let Some(edit) = prefix_edit.take() {
455 return Some(edit);
456 }
457 let hunk = cursor.item()?;
458 if hunk
459 .buffer_range
460 .start
461 .cmp(&range_end, original_snapshot)
462 .is_gt()
463 {
464 return None;
465 }
466 let edit = Edit {
467 old: hunk.buffer_range.to_point(original_snapshot),
468 new: hunk.diff_base_byte_range.to_point(base_text),
469 };
470 cursor.next();
471 Some(edit)
472 });
473
474 edits_since_diff.compose(hunk_iter)
475 }
476
477 #[cfg(test)]
478 pub(crate) fn patch_for_buffer_range_naive<'a>(
479 &'a self,
480 buffer: &'a text::BufferSnapshot,
481 ) -> Patch<Point> {
482 let original_snapshot = self.original_buffer_snapshot();
483
484 let edits_since: Vec<Edit<Point>> = buffer
485 .edits_since::<Point>(original_snapshot.version())
486 .collect();
487 let mut inverted_edits_since = Patch::new(edits_since);
488 inverted_edits_since.invert();
489
490 inverted_edits_since.compose(
491 self.inner
492 .hunks
493 .iter()
494 .map(|hunk| {
495 let old_start = hunk.buffer_range.start.to_point(original_snapshot);
496 let old_end = hunk.buffer_range.end.to_point(original_snapshot);
497 let new_start = self
498 .base_text()
499 .offset_to_point(hunk.diff_base_byte_range.start);
500 let new_end = self
501 .base_text()
502 .offset_to_point(hunk.diff_base_byte_range.end);
503 Edit {
504 old: old_start..old_end,
505 new: new_start..new_end,
506 }
507 })
508 .chain(
509 if !self.inner.base_text_exists && self.inner.hunks.is_empty() {
510 Some(Edit {
511 old: Point::zero()..original_snapshot.max_point(),
512 new: Point::zero()..Point::zero(),
513 })
514 } else {
515 None
516 },
517 ),
518 )
519 }
520
521 /// Returns a patch mapping the base text of this diff to the provided main buffer snapshot.
522 ///
523 /// The returned patch is guaranteed to be accurate for all base text points in the provided range,
524 /// but not necessarily for points outside that range.
525 pub fn patch_for_base_text_range<'a>(
526 &'a self,
527 range: RangeInclusive<Point>,
528 buffer: &'a text::BufferSnapshot,
529 ) -> Patch<Point> {
530 if !self.inner.base_text_exists {
531 return Patch::new(vec![Edit {
532 old: Point::zero()..Point::zero(),
533 new: Point::zero()..buffer.max_point(),
534 }]);
535 }
536
537 let edits_since_diff = buffer
538 .edits_since::<Point>(&self.inner.buffer_snapshot.version)
539 .collect::<Vec<_>>();
540
541 let mut hunk_patch = Vec::new();
542 let mut cursor = self.inner.hunks.cursor(self.original_buffer_snapshot());
543 let hunk_before = self
544 .hunk_before_base_text_offset(range.start().to_offset(self.base_text()), &mut cursor);
545
546 if let Some(hunk) = hunk_before
547 && let Some(first_edit) = edits_since_diff.first()
548 && hunk
549 .buffer_range
550 .start
551 .to_point(self.original_buffer_snapshot())
552 > first_edit.old.start
553 {
554 cursor.reset();
555 self.hunk_before_buffer_anchor(
556 self.original_buffer_snapshot()
557 .anchor_before(first_edit.old.start),
558 &mut cursor,
559 self.original_buffer_snapshot(),
560 );
561 }
562 if cursor.item().is_none() {
563 cursor.next();
564 }
565 if let Some(prev_hunk) = cursor.prev_item() {
566 hunk_patch.push(Edit {
567 old: Point::zero()
568 ..prev_hunk
569 .diff_base_byte_range
570 .end
571 .to_point(self.base_text()),
572 new: Point::zero()
573 ..prev_hunk
574 .buffer_range
575 .end
576 .to_point(self.original_buffer_snapshot()),
577 })
578 }
579 let range_end = range.end().to_offset(self.base_text());
580 while let Some(hunk) = cursor.item()
581 && (hunk.diff_base_byte_range.start <= range_end
582 || edits_since_diff.last().is_some_and(|last_edit| {
583 hunk.buffer_range
584 .start
585 .to_point(self.original_buffer_snapshot())
586 <= last_edit.old.end
587 }))
588 {
589 hunk_patch.push(Edit {
590 old: hunk.diff_base_byte_range.to_point(self.base_text()),
591 new: hunk.buffer_range.to_point(self.original_buffer_snapshot()),
592 });
593 cursor.next();
594 }
595
596 Patch::new(hunk_patch).compose(edits_since_diff)
597 }
598
599 #[cfg(test)]
600 pub(crate) fn patch_for_base_text_range_naive<'a>(
601 &'a self,
602 buffer: &'a text::BufferSnapshot,
603 ) -> Patch<Point> {
604 let original_snapshot = self.original_buffer_snapshot();
605
606 let mut hunk_edits: Vec<Edit<Point>> = Vec::new();
607 for hunk in self.inner.hunks.iter() {
608 let old_start = self
609 .base_text()
610 .offset_to_point(hunk.diff_base_byte_range.start);
611 let old_end = self
612 .base_text()
613 .offset_to_point(hunk.diff_base_byte_range.end);
614 let new_start = hunk.buffer_range.start.to_point(original_snapshot);
615 let new_end = hunk.buffer_range.end.to_point(original_snapshot);
616 hunk_edits.push(Edit {
617 old: old_start..old_end,
618 new: new_start..new_end,
619 });
620 }
621 if !self.inner.base_text_exists && hunk_edits.is_empty() {
622 hunk_edits.push(Edit {
623 old: Point::zero()..Point::zero(),
624 new: Point::zero()..original_snapshot.max_point(),
625 })
626 }
627 let hunk_patch = Patch::new(hunk_edits);
628
629 hunk_patch.compose(buffer.edits_since::<Point>(original_snapshot.version()))
630 }
631
632 pub fn buffer_point_to_base_text_range(
633 &self,
634 point: Point,
635 buffer: &text::BufferSnapshot,
636 ) -> Range<Point> {
637 let patch = self.patch_for_buffer_range(point..=point, buffer);
638 let edit = patch.edit_for_old_position(point);
639 edit.new
640 }
641
642 pub fn base_text_point_to_buffer_range(
643 &self,
644 point: Point,
645 buffer: &text::BufferSnapshot,
646 ) -> Range<Point> {
647 let patch = self.patch_for_base_text_range(point..=point, buffer);
648 let edit = patch.edit_for_old_position(point);
649 edit.new
650 }
651
652 pub fn buffer_point_to_base_text_point(
653 &self,
654 point: Point,
655 buffer: &text::BufferSnapshot,
656 ) -> Point {
657 let patch = self.patch_for_buffer_range(point..=point, buffer);
658 let edit = patch.edit_for_old_position(point);
659 if point == edit.old.end {
660 edit.new.end
661 } else {
662 edit.new.start
663 }
664 }
665
666 pub fn base_text_point_to_buffer_point(
667 &self,
668 point: Point,
669 buffer: &text::BufferSnapshot,
670 ) -> Point {
671 let patch = self.patch_for_base_text_range(point..=point, buffer);
672 let edit = patch.edit_for_old_position(point);
673 if point == edit.old.end {
674 edit.new.end
675 } else {
676 edit.new.start
677 }
678 }
679}
680
681impl BufferDiffInner<Entity<language::Buffer>> {
682 /// Returns the new index text and new pending hunks.
683 fn stage_or_unstage_hunks_impl(
684 &mut self,
685 unstaged_diff: &Self,
686 stage: bool,
687 hunks: &[DiffHunk],
688 buffer: &text::BufferSnapshot,
689 file_exists: bool,
690 cx: &mut Context<BufferDiff>,
691 ) -> Option<Rope> {
692 let head_text = self
693 .base_text_exists
694 .then(|| self.base_text.read(cx).as_rope().clone());
695 let index_text = unstaged_diff
696 .base_text_exists
697 .then(|| unstaged_diff.base_text.read(cx).as_rope().clone());
698
699 // If the file doesn't exist in either HEAD or the index, then the
700 // entire file must be either created or deleted in the index.
701 let (index_text, head_text) = match (index_text, head_text) {
702 (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
703 (index_text, head_text) => {
704 let (new_index_text, new_status) = if stage {
705 log::debug!("stage all");
706 (
707 file_exists.then(|| buffer.as_rope().clone()),
708 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
709 )
710 } else {
711 log::debug!("unstage all");
712 (
713 head_text,
714 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
715 )
716 };
717
718 let hunk = PendingHunk {
719 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
720 diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
721 buffer_version: buffer.version().clone(),
722 new_status,
723 };
724 self.pending_hunks = SumTree::from_item(hunk, buffer);
725 return new_index_text;
726 }
727 };
728
729 let mut pending_hunks = SumTree::new(buffer);
730 let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
731
732 // first, merge new hunks into pending_hunks
733 for DiffHunk {
734 buffer_range,
735 diff_base_byte_range,
736 secondary_status,
737 ..
738 } in hunks.iter().cloned()
739 {
740 let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
741 pending_hunks.append(preceding_pending_hunks, buffer);
742
743 // Skip all overlapping or adjacent old pending hunks
744 while old_pending_hunks.item().is_some_and(|old_hunk| {
745 old_hunk
746 .buffer_range
747 .start
748 .cmp(&buffer_range.end, buffer)
749 .is_le()
750 }) {
751 old_pending_hunks.next();
752 }
753
754 if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
755 || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
756 {
757 continue;
758 }
759
760 pending_hunks.push(
761 PendingHunk {
762 buffer_range,
763 diff_base_byte_range,
764 buffer_version: buffer.version().clone(),
765 new_status: if stage {
766 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
767 } else {
768 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
769 },
770 },
771 buffer,
772 );
773 }
774 // append the remainder
775 pending_hunks.append(old_pending_hunks.suffix(), buffer);
776
777 let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
778 unstaged_hunk_cursor.next();
779
780 // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
781 let mut prev_unstaged_hunk_buffer_end = 0;
782 let mut prev_unstaged_hunk_base_text_end = 0;
783 let mut edits = Vec::<(Range<usize>, String)>::new();
784 let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
785 while let Some(PendingHunk {
786 buffer_range,
787 diff_base_byte_range,
788 new_status,
789 ..
790 }) = pending_hunks_iter.next()
791 {
792 // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
793 let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
794
795 if let Some(unstaged_hunk) = skipped_unstaged.last() {
796 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
797 prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
798 }
799
800 // Find where this hunk is in the index if it doesn't overlap
801 let mut buffer_offset_range = buffer_range.to_offset(buffer);
802 let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
803 let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
804
805 loop {
806 // Merge this hunk with any overlapping unstaged hunks.
807 if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
808 let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
809 if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
810 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
811 prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
812
813 index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
814 buffer_offset_range.start = buffer_offset_range
815 .start
816 .min(unstaged_hunk_offset_range.start);
817 buffer_offset_range.end =
818 buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
819
820 unstaged_hunk_cursor.next();
821 continue;
822 }
823 }
824
825 // If any unstaged hunks were merged, then subsequent pending hunks may
826 // now overlap this hunk. Merge them.
827 if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
828 let next_pending_hunk_offset_range =
829 next_pending_hunk.buffer_range.to_offset(buffer);
830 if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
831 buffer_offset_range.end = buffer_offset_range
832 .end
833 .max(next_pending_hunk_offset_range.end);
834 pending_hunks_iter.next();
835 continue;
836 }
837 }
838
839 break;
840 }
841
842 let end_overshoot = buffer_offset_range
843 .end
844 .saturating_sub(prev_unstaged_hunk_buffer_end);
845 let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
846 let index_byte_range = index_start..index_end;
847
848 let replacement_text = match new_status {
849 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
850 log::debug!("staging hunk {:?}", buffer_offset_range);
851 buffer
852 .text_for_range(buffer_offset_range)
853 .collect::<String>()
854 }
855 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
856 log::debug!("unstaging hunk {:?}", buffer_offset_range);
857 head_text
858 .chunks_in_range(diff_base_byte_range.clone())
859 .collect::<String>()
860 }
861 _ => {
862 debug_assert!(false);
863 continue;
864 }
865 };
866
867 edits.push((index_byte_range, replacement_text));
868 }
869 drop(pending_hunks_iter);
870 drop(old_pending_hunks);
871 self.pending_hunks = pending_hunks;
872
873 #[cfg(debug_assertions)] // invariants: non-overlapping and sorted
874 {
875 for window in edits.windows(2) {
876 let (range_a, range_b) = (&window[0].0, &window[1].0);
877 debug_assert!(range_a.end < range_b.start);
878 }
879 }
880
881 let mut new_index_text = Rope::new();
882 let mut index_cursor = index_text.cursor(0);
883
884 for (old_range, replacement_text) in edits {
885 new_index_text.append(index_cursor.slice(old_range.start));
886 index_cursor.seek_forward(old_range.end);
887 new_index_text.push(&replacement_text);
888 }
889 new_index_text.append(index_cursor.suffix());
890 Some(new_index_text)
891 }
892}
893
894impl BufferDiffInner<language::BufferSnapshot> {
895 fn hunks_intersecting_range<'a>(
896 &'a self,
897 range: Range<Anchor>,
898 buffer: &'a text::BufferSnapshot,
899 secondary: Option<&'a Self>,
900 ) -> impl 'a + Iterator<Item = DiffHunk> {
901 let range = range.to_offset(buffer);
902 let filter = move |summary: &DiffHunkSummary| {
903 let summary_range = summary.buffer_range.to_offset(buffer);
904 let before_start = summary_range.end < range.start;
905 let after_end = summary_range.start > range.end;
906 !before_start && !after_end
907 };
908 self.hunks_intersecting_range_impl(filter, buffer, secondary)
909 }
910
911 fn hunks_intersecting_range_impl<'a>(
912 &'a self,
913 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
914 buffer: &'a text::BufferSnapshot,
915 secondary: Option<&'a Self>,
916 ) -> impl 'a + Iterator<Item = DiffHunk> {
917 let anchor_iter = self
918 .hunks
919 .filter::<_, DiffHunkSummary>(buffer, filter)
920 .flat_map(move |hunk| {
921 [
922 (
923 &hunk.buffer_range.start,
924 (
925 hunk.buffer_range.start,
926 hunk.diff_base_byte_range.start,
927 hunk,
928 ),
929 ),
930 (
931 &hunk.buffer_range.end,
932 (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
933 ),
934 ]
935 });
936
937 let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
938 pending_hunks_cursor.next();
939
940 let mut secondary_cursor = None;
941 if let Some(secondary) = secondary.as_ref() {
942 let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
943 cursor.next();
944 secondary_cursor = Some(cursor);
945 }
946
947 let max_point = buffer.max_point();
948 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
949 iter::from_fn(move || {
950 loop {
951 let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
952 let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
953
954 let base_word_diffs = hunk.base_word_diffs.clone();
955 let buffer_word_diffs = hunk.buffer_word_diffs.clone();
956
957 if !start_anchor.is_valid(buffer) {
958 continue;
959 }
960
961 if end_point.column > 0 && end_point < max_point {
962 end_point.row += 1;
963 end_point.column = 0;
964 end_anchor = buffer.anchor_before(end_point);
965 }
966
967 let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
968
969 let mut has_pending = false;
970 if start_anchor
971 .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
972 .is_gt()
973 {
974 pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
975 }
976
977 if let Some(pending_hunk) = pending_hunks_cursor.item() {
978 let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
979 if pending_range.end.column > 0 {
980 pending_range.end.row += 1;
981 pending_range.end.column = 0;
982 }
983
984 if pending_range == (start_point..end_point)
985 && !buffer.has_edits_since_in_range(
986 &pending_hunk.buffer_version,
987 start_anchor..end_anchor,
988 )
989 {
990 has_pending = true;
991 secondary_status = pending_hunk.new_status;
992 }
993 }
994
995 if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
996 if start_anchor
997 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
998 .is_gt()
999 {
1000 secondary_cursor.seek_forward(&start_anchor, Bias::Left);
1001 }
1002
1003 if let Some(secondary_hunk) = secondary_cursor.item() {
1004 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
1005 if secondary_range.end.column > 0 {
1006 secondary_range.end.row += 1;
1007 secondary_range.end.column = 0;
1008 }
1009 if secondary_range.is_empty()
1010 && secondary_hunk.diff_base_byte_range.is_empty()
1011 {
1012 // ignore
1013 } else if secondary_range == (start_point..end_point) {
1014 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1015 } else if secondary_range.start <= end_point {
1016 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
1017 }
1018 }
1019 }
1020
1021 return Some(DiffHunk {
1022 range: start_point..end_point,
1023 diff_base_byte_range: start_base..end_base,
1024 buffer_range: start_anchor..end_anchor,
1025 base_word_diffs,
1026 buffer_word_diffs,
1027 secondary_status,
1028 });
1029 }
1030 })
1031 }
1032
1033 fn hunks_intersecting_range_rev_impl<'a>(
1034 &'a self,
1035 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
1036 buffer: &'a text::BufferSnapshot,
1037 ) -> impl 'a + Iterator<Item = DiffHunk> {
1038 let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
1039
1040 iter::from_fn(move || {
1041 cursor.prev();
1042
1043 let hunk = cursor.item()?;
1044 let range = hunk.buffer_range.to_point(buffer);
1045
1046 Some(DiffHunk {
1047 range,
1048 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1049 buffer_range: hunk.buffer_range.clone(),
1050 // The secondary status is not used by callers of this method.
1051 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
1052 base_word_diffs: hunk.base_word_diffs.clone(),
1053 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
1054 })
1055 })
1056 }
1057}
1058
1059fn build_diff_options(
1060 file: Option<&Arc<dyn File>>,
1061 language: Option<LanguageName>,
1062 language_scope: Option<language::LanguageScope>,
1063 cx: &App,
1064) -> Option<DiffOptions> {
1065 #[cfg(any(test, feature = "test-support"))]
1066 {
1067 if !cx.has_global::<settings::SettingsStore>() {
1068 return Some(DiffOptions {
1069 language_scope,
1070 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1071 ..Default::default()
1072 });
1073 }
1074 }
1075
1076 language_settings(language, file, cx)
1077 .word_diff_enabled
1078 .then_some(DiffOptions {
1079 language_scope,
1080 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1081 ..Default::default()
1082 })
1083}
1084
1085fn compute_hunks(
1086 diff_base: Option<(Arc<str>, Rope)>,
1087 buffer: &text::BufferSnapshot,
1088 diff_options: Option<DiffOptions>,
1089) -> SumTree<InternalDiffHunk> {
1090 let mut tree = SumTree::new(buffer);
1091
1092 if let Some((diff_base, diff_base_rope)) = diff_base {
1093 let buffer_text = buffer.as_rope().to_string();
1094
1095 let mut options = GitOptions::default();
1096 options.context_lines(0);
1097 let patch = GitPatch::from_buffers(
1098 diff_base.as_bytes(),
1099 None,
1100 buffer_text.as_bytes(),
1101 None,
1102 Some(&mut options),
1103 )
1104 .log_err();
1105
1106 // A common case in Zed is that the empty buffer is represented as just a newline,
1107 // but if we just compute a naive diff you get a "preserved" line in the middle,
1108 // which is a bit odd.
1109 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
1110 tree.push(
1111 InternalDiffHunk {
1112 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
1113 diff_base_byte_range: 0..diff_base.len() - 1,
1114 base_word_diffs: Vec::default(),
1115 buffer_word_diffs: Vec::default(),
1116 },
1117 buffer,
1118 );
1119 return tree;
1120 }
1121
1122 if let Some(patch) = patch {
1123 let mut divergence = 0;
1124 for hunk_index in 0..patch.num_hunks() {
1125 let hunk = process_patch_hunk(
1126 &patch,
1127 hunk_index,
1128 &diff_base_rope,
1129 buffer,
1130 &mut divergence,
1131 diff_options.as_ref(),
1132 );
1133 tree.push(hunk, buffer);
1134 }
1135 }
1136 } else {
1137 tree.push(
1138 InternalDiffHunk {
1139 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
1140 diff_base_byte_range: 0..0,
1141 base_word_diffs: Vec::default(),
1142 buffer_word_diffs: Vec::default(),
1143 },
1144 buffer,
1145 );
1146 }
1147
1148 tree
1149}
1150
1151fn compare_hunks(
1152 new_hunks: &SumTree<InternalDiffHunk>,
1153 old_hunks: &SumTree<InternalDiffHunk>,
1154 old_snapshot: &text::BufferSnapshot,
1155 new_snapshot: &text::BufferSnapshot,
1156 old_base_text: &text::BufferSnapshot,
1157 new_base_text: &text::BufferSnapshot,
1158) -> DiffChanged {
1159 let mut new_cursor = new_hunks.cursor::<()>(new_snapshot);
1160 let mut old_cursor = old_hunks.cursor::<()>(new_snapshot);
1161 old_cursor.next();
1162 new_cursor.next();
1163 let mut start = None;
1164 let mut end = None;
1165 let mut base_text_start: Option<Anchor> = None;
1166 let mut base_text_end: Option<Anchor> = None;
1167
1168 let mut last_unchanged_new_hunk_end: Option<text::Anchor> = None;
1169 let mut has_changes = false;
1170 let mut extended_end_candidate: Option<text::Anchor> = None;
1171
1172 loop {
1173 match (new_cursor.item(), old_cursor.item()) {
1174 (Some(new_hunk), Some(old_hunk)) => {
1175 match new_hunk
1176 .buffer_range
1177 .start
1178 .cmp(&old_hunk.buffer_range.start, new_snapshot)
1179 {
1180 Ordering::Less => {
1181 has_changes = true;
1182 extended_end_candidate = None;
1183 start.get_or_insert(new_hunk.buffer_range.start);
1184 base_text_start.get_or_insert(
1185 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1186 );
1187 end.replace(new_hunk.buffer_range.end);
1188 let new_diff_range_end =
1189 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1190 if base_text_end.is_none_or(|base_text_end| {
1191 new_diff_range_end
1192 .cmp(&base_text_end, &new_base_text)
1193 .is_gt()
1194 }) {
1195 base_text_end = Some(new_diff_range_end)
1196 }
1197 new_cursor.next();
1198 }
1199 Ordering::Equal => {
1200 if new_hunk != old_hunk {
1201 has_changes = true;
1202 extended_end_candidate = None;
1203 start.get_or_insert(new_hunk.buffer_range.start);
1204 base_text_start.get_or_insert(
1205 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1206 );
1207 if old_hunk
1208 .buffer_range
1209 .end
1210 .cmp(&new_hunk.buffer_range.end, new_snapshot)
1211 .is_ge()
1212 {
1213 end.replace(old_hunk.buffer_range.end);
1214 } else {
1215 end.replace(new_hunk.buffer_range.end);
1216 }
1217
1218 let old_hunk_diff_base_range_end =
1219 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1220 let new_hunk_diff_base_range_end =
1221 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1222
1223 base_text_end.replace(
1224 *old_hunk_diff_base_range_end
1225 .max(&new_hunk_diff_base_range_end, new_base_text),
1226 );
1227 } else {
1228 if !has_changes {
1229 last_unchanged_new_hunk_end = Some(new_hunk.buffer_range.end);
1230 } else if extended_end_candidate.is_none() {
1231 extended_end_candidate = Some(new_hunk.buffer_range.start);
1232 }
1233 }
1234
1235 new_cursor.next();
1236 old_cursor.next();
1237 }
1238 Ordering::Greater => {
1239 has_changes = true;
1240 extended_end_candidate = None;
1241 start.get_or_insert(old_hunk.buffer_range.start);
1242 base_text_start.get_or_insert(
1243 old_base_text.anchor_after(old_hunk.diff_base_byte_range.start),
1244 );
1245 end.replace(old_hunk.buffer_range.end);
1246 let old_diff_range_end =
1247 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1248 if base_text_end.is_none_or(|base_text_end| {
1249 old_diff_range_end
1250 .cmp(&base_text_end, new_base_text)
1251 .is_gt()
1252 }) {
1253 base_text_end = Some(old_diff_range_end)
1254 }
1255 old_cursor.next();
1256 }
1257 }
1258 }
1259 (Some(new_hunk), None) => {
1260 has_changes = true;
1261 extended_end_candidate = None;
1262 start.get_or_insert(new_hunk.buffer_range.start);
1263 base_text_start
1264 .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start));
1265 if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le())
1266 {
1267 end.replace(new_hunk.buffer_range.end);
1268 }
1269 let new_base_text_end =
1270 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1271 if base_text_end.is_none_or(|base_text_end| {
1272 new_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1273 }) {
1274 base_text_end = Some(new_base_text_end)
1275 }
1276 new_cursor.next();
1277 }
1278 (None, Some(old_hunk)) => {
1279 has_changes = true;
1280 extended_end_candidate = None;
1281 start.get_or_insert(old_hunk.buffer_range.start);
1282 base_text_start
1283 .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start));
1284 if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le())
1285 {
1286 end.replace(old_hunk.buffer_range.end);
1287 }
1288 let old_base_text_end =
1289 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1290 if base_text_end.is_none_or(|base_text_end| {
1291 old_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1292 }) {
1293 base_text_end = Some(old_base_text_end);
1294 }
1295 old_cursor.next();
1296 }
1297 (None, None) => break,
1298 }
1299 }
1300
1301 let changed_range = start.zip(end).map(|(start, end)| start..end);
1302 let base_text_changed_range = base_text_start
1303 .zip(base_text_end)
1304 .map(|(start, end)| (start..end).to_offset(new_base_text));
1305
1306 let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() {
1307 let extended_start = *last_unchanged_new_hunk_end
1308 .unwrap_or(text::Anchor::min_for_buffer(new_snapshot.remote_id()))
1309 .min(&changed_range.start, new_snapshot);
1310 let extended_start = new_snapshot
1311 .anchored_edits_since_in_range::<usize>(
1312 &old_snapshot.version(),
1313 extended_start..changed_range.start,
1314 )
1315 .map(|(_, anchors)| anchors.start)
1316 .min_by(|a, b| a.cmp(b, new_snapshot))
1317 .unwrap_or(changed_range.start);
1318
1319 let extended_end = *extended_end_candidate
1320 .unwrap_or(text::Anchor::max_for_buffer(new_snapshot.remote_id()))
1321 .max(&changed_range.end, new_snapshot);
1322 let extended_end = new_snapshot
1323 .anchored_edits_since_in_range::<usize>(
1324 &old_snapshot.version(),
1325 changed_range.end..extended_end,
1326 )
1327 .map(|(_, anchors)| anchors.end)
1328 .max_by(|a, b| a.cmp(b, new_snapshot))
1329 .unwrap_or(changed_range.end);
1330
1331 Some(extended_start..extended_end)
1332 } else {
1333 None
1334 };
1335
1336 DiffChanged {
1337 changed_range,
1338 base_text_changed_range,
1339 extended_range,
1340 }
1341}
1342
1343fn process_patch_hunk(
1344 patch: &GitPatch<'_>,
1345 hunk_index: usize,
1346 diff_base: &Rope,
1347 buffer: &text::BufferSnapshot,
1348 buffer_row_divergence: &mut i64,
1349 diff_options: Option<&DiffOptions>,
1350) -> InternalDiffHunk {
1351 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
1352 assert!(line_item_count > 0);
1353
1354 let mut first_deletion_buffer_row: Option<u32> = None;
1355 let mut buffer_row_range: Option<Range<u32>> = None;
1356 let mut diff_base_byte_range: Option<Range<usize>> = None;
1357 let mut first_addition_old_row: Option<u32> = None;
1358
1359 for line_index in 0..line_item_count {
1360 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
1361 let kind = line.origin_value();
1362 let content_offset = line.content_offset() as isize;
1363 let content_len = line.content().len() as isize;
1364 match kind {
1365 GitDiffLineType::Addition => {
1366 if first_addition_old_row.is_none() {
1367 first_addition_old_row = Some(
1368 (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
1369 );
1370 }
1371 *buffer_row_divergence += 1;
1372 let row = line.new_lineno().unwrap().saturating_sub(1);
1373
1374 match &mut buffer_row_range {
1375 Some(Range { end, .. }) => *end = row + 1,
1376 None => buffer_row_range = Some(row..row + 1),
1377 }
1378 }
1379 GitDiffLineType::Deletion => {
1380 let end = content_offset + content_len;
1381
1382 match &mut diff_base_byte_range {
1383 Some(head_byte_range) => head_byte_range.end = end as usize,
1384 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
1385 }
1386
1387 if first_deletion_buffer_row.is_none() {
1388 let old_row = line.old_lineno().unwrap().saturating_sub(1);
1389 let row = old_row as i64 + *buffer_row_divergence;
1390 first_deletion_buffer_row = Some(row as u32);
1391 }
1392
1393 *buffer_row_divergence -= 1;
1394 }
1395 _ => {}
1396 }
1397 }
1398
1399 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
1400 // Pure deletion hunk without addition.
1401 let row = first_deletion_buffer_row.unwrap();
1402 row..row
1403 });
1404 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
1405 // Pure addition hunk without deletion.
1406 let row = first_addition_old_row.unwrap();
1407 let offset = diff_base.point_to_offset(Point::new(row, 0));
1408 offset..offset
1409 });
1410
1411 let start = Point::new(buffer_row_range.start, 0);
1412 let end = Point::new(buffer_row_range.end, 0);
1413 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
1414
1415 let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
1416
1417 let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
1418 && !buffer_row_range.is_empty()
1419 && base_line_count == buffer_row_range.len()
1420 && diff_options.max_word_diff_line_count >= base_line_count
1421 {
1422 let base_text: String = diff_base
1423 .chunks_in_range(diff_base_byte_range.clone())
1424 .collect();
1425
1426 let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
1427
1428 let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
1429 &base_text,
1430 &buffer_text,
1431 DiffOptions {
1432 language_scope: diff_options.language_scope.clone(),
1433 ..*diff_options
1434 },
1435 );
1436
1437 let buffer_start_offset = buffer_range.start.to_offset(buffer);
1438 let buffer_word_diffs = buffer_word_diffs_relative
1439 .into_iter()
1440 .map(|range| {
1441 let start = buffer.anchor_after(buffer_start_offset + range.start);
1442 let end = buffer.anchor_after(buffer_start_offset + range.end);
1443 start..end
1444 })
1445 .collect();
1446
1447 (base_word_diffs, buffer_word_diffs)
1448 } else {
1449 (Vec::default(), Vec::default())
1450 };
1451
1452 InternalDiffHunk {
1453 buffer_range,
1454 diff_base_byte_range,
1455 base_word_diffs,
1456 buffer_word_diffs,
1457 }
1458}
1459
1460impl std::fmt::Debug for BufferDiff {
1461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1462 f.debug_struct("BufferChangeSet")
1463 .field("buffer_id", &self.buffer_id)
1464 .finish()
1465 }
1466}
1467
1468#[derive(Clone, Debug, Default)]
1469pub struct DiffChanged {
1470 pub changed_range: Option<Range<text::Anchor>>,
1471 pub base_text_changed_range: Option<Range<usize>>,
1472 pub extended_range: Option<Range<text::Anchor>>,
1473}
1474
1475#[derive(Clone, Debug)]
1476pub enum BufferDiffEvent {
1477 DiffChanged(DiffChanged),
1478 LanguageChanged,
1479 HunksStagedOrUnstaged(Option<Rope>),
1480}
1481
1482impl EventEmitter<BufferDiffEvent> for BufferDiff {}
1483
1484impl BufferDiff {
1485 pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
1486 let base_text = cx.new(|cx| {
1487 let mut buffer = language::Buffer::local("", cx);
1488 buffer.set_capability(Capability::ReadOnly, cx);
1489 buffer
1490 });
1491
1492 BufferDiff {
1493 buffer_id: buffer.remote_id(),
1494 inner: BufferDiffInner {
1495 base_text,
1496 hunks: SumTree::new(buffer),
1497 pending_hunks: SumTree::new(buffer),
1498 base_text_exists: false,
1499 buffer_snapshot: buffer.clone(),
1500 },
1501 secondary_diff: None,
1502 }
1503 }
1504
1505 pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context<Self>) -> Self {
1506 let base_text = buffer.text();
1507 let base_text = cx.new(|cx| {
1508 let mut buffer = language::Buffer::local(base_text, cx);
1509 buffer.set_capability(Capability::ReadOnly, cx);
1510 buffer
1511 });
1512
1513 BufferDiff {
1514 buffer_id: buffer.remote_id(),
1515 inner: BufferDiffInner {
1516 base_text,
1517 hunks: SumTree::new(buffer),
1518 pending_hunks: SumTree::new(buffer),
1519 base_text_exists: true,
1520 buffer_snapshot: buffer.clone(),
1521 },
1522 secondary_diff: None,
1523 }
1524 }
1525
1526 #[cfg(any(test, feature = "test-support"))]
1527 pub fn new_with_base_text(
1528 base_text: &str,
1529 buffer: &text::BufferSnapshot,
1530 cx: &mut Context<Self>,
1531 ) -> Self {
1532 let mut this = BufferDiff::new(&buffer, cx);
1533 let mut base_text = base_text.to_owned();
1534 text::LineEnding::normalize(&mut base_text);
1535 let inner = cx.foreground_executor().block_on(this.update_diff(
1536 buffer.clone(),
1537 Some(Arc::from(base_text)),
1538 Some(false),
1539 None,
1540 cx,
1541 ));
1542 this.set_snapshot(inner, &buffer, cx).detach();
1543 this
1544 }
1545
1546 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
1547 self.secondary_diff = Some(diff);
1548 }
1549
1550 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
1551 self.secondary_diff.clone()
1552 }
1553
1554 pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
1555 if self.secondary_diff.is_some() {
1556 self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
1557 buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
1558 diff_base_byte_range: 0..0,
1559 });
1560 let changed_range = Some(Anchor::min_max_range_for_buffer(self.buffer_id));
1561 let base_text_range = Some(0..self.base_text(cx).len());
1562 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1563 changed_range: changed_range.clone(),
1564 base_text_changed_range: base_text_range,
1565 extended_range: changed_range,
1566 }));
1567 }
1568 }
1569
1570 pub fn stage_or_unstage_hunks(
1571 &mut self,
1572 stage: bool,
1573 hunks: &[DiffHunk],
1574 buffer: &text::BufferSnapshot,
1575 file_exists: bool,
1576 cx: &mut Context<Self>,
1577 ) -> Option<Rope> {
1578 let new_index_text = self
1579 .secondary_diff
1580 .as_ref()?
1581 .update(cx, |secondary_diff, cx| {
1582 self.inner.stage_or_unstage_hunks_impl(
1583 &secondary_diff.inner,
1584 stage,
1585 hunks,
1586 buffer,
1587 file_exists,
1588 cx,
1589 )
1590 });
1591
1592 cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
1593 new_index_text.clone(),
1594 ));
1595 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1596 let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1597 let base_text_changed_range =
1598 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1599 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1600 changed_range: changed_range.clone(),
1601 base_text_changed_range,
1602 extended_range: changed_range,
1603 }));
1604 }
1605 new_index_text
1606 }
1607
1608 pub fn stage_or_unstage_all_hunks(
1609 &mut self,
1610 stage: bool,
1611 buffer: &text::BufferSnapshot,
1612 file_exists: bool,
1613 cx: &mut Context<Self>,
1614 ) {
1615 let hunks = self
1616 .snapshot(cx)
1617 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
1618 .collect::<Vec<_>>();
1619 let Some(secondary) = self.secondary_diff.clone() else {
1620 return;
1621 };
1622 let secondary = secondary.read(cx).inner.clone();
1623 self.inner
1624 .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx);
1625 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1626 let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1627 let base_text_changed_range =
1628 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1629 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1630 changed_range: changed_range.clone(),
1631 base_text_changed_range,
1632 extended_range: changed_range,
1633 }));
1634 }
1635 }
1636
1637 pub fn update_diff(
1638 &self,
1639 buffer: text::BufferSnapshot,
1640 base_text: Option<Arc<str>>,
1641 base_text_change: Option<bool>,
1642 language: Option<Arc<Language>>,
1643 cx: &App,
1644 ) -> Task<BufferDiffUpdate> {
1645 let prev_base_text = self.base_text(cx).as_rope().clone();
1646 let base_text_changed = base_text_change.is_some();
1647 let compute_base_text_edits = base_text_change == Some(true);
1648 let diff_options = build_diff_options(
1649 None,
1650 language.as_ref().map(|l| l.name()),
1651 language.as_ref().map(|l| l.default_scope()),
1652 cx,
1653 );
1654 let buffer_snapshot = buffer.clone();
1655
1656 let base_text_diff_task = if base_text_changed && compute_base_text_edits {
1657 base_text
1658 .as_ref()
1659 .map(|new_text| self.inner.base_text.read(cx).diff(new_text.clone(), cx))
1660 } else {
1661 None
1662 };
1663
1664 let hunk_task = cx.background_executor().spawn({
1665 let buffer_snapshot = buffer_snapshot.clone();
1666 async move {
1667 let base_text_rope = if let Some(base_text) = &base_text {
1668 if base_text_changed {
1669 Rope::from(base_text.as_ref())
1670 } else {
1671 prev_base_text
1672 }
1673 } else {
1674 Rope::new()
1675 };
1676 let base_text_exists = base_text.is_some();
1677 let hunks = compute_hunks(
1678 base_text
1679 .clone()
1680 .map(|base_text| (base_text, base_text_rope.clone())),
1681 &buffer,
1682 diff_options,
1683 );
1684 let base_text = base_text.unwrap_or_default();
1685 BufferDiffInner {
1686 base_text,
1687 hunks,
1688 base_text_exists,
1689 pending_hunks: SumTree::new(&buffer),
1690 buffer_snapshot,
1691 }
1692 }
1693 });
1694
1695 cx.background_executor().spawn(async move {
1696 let (inner, base_text_edits) = match base_text_diff_task {
1697 Some(diff_task) => {
1698 let (inner, diff) = futures::join!(hunk_task, diff_task);
1699 (inner, Some(diff))
1700 }
1701 None => (hunk_task.await, None),
1702 };
1703
1704 BufferDiffUpdate {
1705 inner,
1706 buffer_snapshot,
1707 base_text_edits,
1708 base_text_changed,
1709 }
1710 })
1711 }
1712
1713 #[ztracing::instrument(skip_all)]
1714 pub fn language_changed(
1715 &mut self,
1716 language: Option<Arc<Language>>,
1717 language_registry: Option<Arc<LanguageRegistry>>,
1718 cx: &mut Context<Self>,
1719 ) {
1720 let fut = self.inner.base_text.update(cx, |base_text, cx| {
1721 if let Some(language_registry) = language_registry {
1722 base_text.set_language_registry(language_registry);
1723 }
1724 base_text.set_language(language, cx);
1725 base_text.parsing_idle()
1726 });
1727 cx.spawn(async move |this, cx| {
1728 fut.await;
1729 this.update(cx, |_, cx| {
1730 cx.emit(BufferDiffEvent::LanguageChanged);
1731 })
1732 .ok();
1733 })
1734 .detach();
1735 }
1736
1737 fn set_snapshot_with_secondary_inner(
1738 &mut self,
1739 update: BufferDiffUpdate,
1740 buffer: &text::BufferSnapshot,
1741 secondary_diff_change: Option<Range<Anchor>>,
1742 clear_pending_hunks: bool,
1743 cx: &mut Context<Self>,
1744 ) -> impl Future<Output = DiffChanged> + use<> {
1745 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1746
1747 let old_snapshot = self.snapshot(cx);
1748 let new_state = update.inner;
1749 let base_text_changed = update.base_text_changed;
1750
1751 let state = &mut self.inner;
1752 state.base_text_exists = new_state.base_text_exists;
1753 let should_compare_hunks = update.base_text_edits.is_some() || !base_text_changed;
1754 let parsing_idle = if let Some(diff) = update.base_text_edits {
1755 state.base_text.update(cx, |base_text, cx| {
1756 base_text.set_sync_parse_timeout(None);
1757 base_text.set_capability(Capability::ReadWrite, cx);
1758 base_text.apply_diff(diff, cx);
1759 base_text.set_capability(Capability::ReadOnly, cx);
1760 Some(base_text.parsing_idle())
1761 })
1762 } else if update.base_text_changed {
1763 state.base_text.update(cx, |base_text, cx| {
1764 base_text.set_sync_parse_timeout(None);
1765 base_text.set_capability(Capability::ReadWrite, cx);
1766 base_text.set_text(new_state.base_text.clone(), cx);
1767 base_text.set_capability(Capability::ReadOnly, cx);
1768 Some(base_text.parsing_idle())
1769 })
1770 } else {
1771 None
1772 };
1773
1774 let old_buffer_snapshot = &old_snapshot.inner.buffer_snapshot;
1775 let old_base_snapshot = &old_snapshot.inner.base_text;
1776 let new_base_snapshot = state.base_text.read(cx).snapshot();
1777 let DiffChanged {
1778 mut changed_range,
1779 mut base_text_changed_range,
1780 mut extended_range,
1781 } = match (state.base_text_exists, new_state.base_text_exists) {
1782 (false, false) => DiffChanged::default(),
1783 (true, true) if should_compare_hunks => compare_hunks(
1784 &new_state.hunks,
1785 &old_snapshot.inner.hunks,
1786 old_buffer_snapshot,
1787 buffer,
1788 old_base_snapshot,
1789 &new_base_snapshot,
1790 ),
1791 _ => {
1792 let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id);
1793 let full_base_range = 0..new_state.base_text.len();
1794 DiffChanged {
1795 changed_range: Some(full_range.clone()),
1796 base_text_changed_range: Some(full_base_range),
1797 extended_range: Some(full_range),
1798 }
1799 }
1800 };
1801 state.hunks = new_state.hunks;
1802 state.buffer_snapshot = update.buffer_snapshot;
1803
1804 if base_text_changed || clear_pending_hunks {
1805 if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1806 {
1807 let pending_range = first.buffer_range.start..last.buffer_range.end;
1808 if let Some(range) = &mut changed_range {
1809 range.start = *range.start.min(&pending_range.start, buffer);
1810 range.end = *range.end.max(&pending_range.end, buffer);
1811 } else {
1812 changed_range = Some(pending_range.clone());
1813 }
1814
1815 if let Some(base_text_range) = base_text_changed_range.as_mut() {
1816 base_text_range.start =
1817 base_text_range.start.min(first.diff_base_byte_range.start);
1818 base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end);
1819 } else {
1820 base_text_changed_range =
1821 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1822 }
1823
1824 if let Some(ext) = &mut extended_range {
1825 ext.start = *ext.start.min(&pending_range.start, buffer);
1826 ext.end = *ext.end.max(&pending_range.end, buffer);
1827 } else {
1828 extended_range = Some(pending_range);
1829 }
1830 }
1831 state.pending_hunks = SumTree::new(buffer);
1832 }
1833
1834 if let Some(secondary_changed_range) = secondary_diff_change
1835 && let (Some(secondary_hunk_range), Some(secondary_base_range)) =
1836 old_snapshot.range_to_hunk_range(secondary_changed_range, buffer)
1837 {
1838 if let Some(range) = &mut changed_range {
1839 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1840 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1841 } else {
1842 changed_range = Some(secondary_hunk_range.clone());
1843 }
1844
1845 if let Some(base_text_range) = base_text_changed_range.as_mut() {
1846 base_text_range.start = secondary_base_range.start.min(base_text_range.start);
1847 base_text_range.end = secondary_base_range.end.max(base_text_range.end);
1848 } else {
1849 base_text_changed_range = Some(secondary_base_range);
1850 }
1851
1852 if let Some(ext) = &mut extended_range {
1853 ext.start = *ext.start.min(&secondary_hunk_range.start, buffer);
1854 ext.end = *ext.end.max(&secondary_hunk_range.end, buffer);
1855 } else {
1856 extended_range = Some(secondary_hunk_range);
1857 }
1858 }
1859
1860 async move {
1861 if let Some(parsing_idle) = parsing_idle {
1862 parsing_idle.await;
1863 }
1864 DiffChanged {
1865 changed_range,
1866 base_text_changed_range,
1867 extended_range,
1868 }
1869 }
1870 }
1871
1872 pub fn set_snapshot(
1873 &mut self,
1874 new_state: BufferDiffUpdate,
1875 buffer: &text::BufferSnapshot,
1876 cx: &mut Context<Self>,
1877 ) -> Task<Option<Range<Anchor>>> {
1878 self.set_snapshot_with_secondary(new_state, buffer, None, false, cx)
1879 }
1880
1881 pub fn set_snapshot_with_secondary(
1882 &mut self,
1883 update: BufferDiffUpdate,
1884 buffer: &text::BufferSnapshot,
1885 secondary_diff_change: Option<Range<Anchor>>,
1886 clear_pending_hunks: bool,
1887 cx: &mut Context<Self>,
1888 ) -> Task<Option<Range<Anchor>>> {
1889 let fut = self.set_snapshot_with_secondary_inner(
1890 update,
1891 buffer,
1892 secondary_diff_change,
1893 clear_pending_hunks,
1894 cx,
1895 );
1896
1897 cx.spawn(async move |this, cx| {
1898 let change = fut.await;
1899 this.update(cx, |_, cx| {
1900 cx.emit(BufferDiffEvent::DiffChanged(change.clone()));
1901 })
1902 .ok();
1903 change.changed_range
1904 })
1905 }
1906
1907 pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
1908 self.inner.base_text.read(cx).snapshot()
1909 }
1910
1911 pub fn base_text_exists(&self) -> bool {
1912 self.inner.base_text_exists
1913 }
1914
1915 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1916 BufferDiffSnapshot {
1917 inner: BufferDiffInner {
1918 hunks: self.inner.hunks.clone(),
1919 pending_hunks: self.inner.pending_hunks.clone(),
1920 base_text: self.inner.base_text.read(cx).snapshot(),
1921 base_text_exists: self.inner.base_text_exists,
1922 buffer_snapshot: self.inner.buffer_snapshot.clone(),
1923 },
1924 secondary_diff: self.secondary_diff.as_ref().map(|diff| {
1925 debug_assert!(diff.read(cx).secondary_diff.is_none());
1926 Arc::new(diff.read(cx).snapshot(cx))
1927 }),
1928 }
1929 }
1930
1931 /// Used in cases where the change set isn't derived from git.
1932 pub fn set_base_text(
1933 &mut self,
1934 base_text: Option<Arc<str>>,
1935 language: Option<Arc<Language>>,
1936 buffer: text::BufferSnapshot,
1937 cx: &mut Context<Self>,
1938 ) -> oneshot::Receiver<()> {
1939 let (tx, rx) = oneshot::channel();
1940 let complete_on_drop = util::defer(|| {
1941 tx.send(()).ok();
1942 });
1943 cx.spawn(async move |this, cx| {
1944 let Some(state) = this
1945 .update(cx, |this, cx| {
1946 this.update_diff(buffer.clone(), base_text, Some(false), language, cx)
1947 })
1948 .log_err()
1949 else {
1950 return;
1951 };
1952 let state = state.await;
1953 if let Some(task) = this
1954 .update(cx, |this, cx| this.set_snapshot(state, &buffer, cx))
1955 .log_err()
1956 {
1957 task.await;
1958 }
1959 drop(complete_on_drop)
1960 })
1961 .detach();
1962 rx
1963 }
1964
1965 pub fn base_text_string(&self, cx: &App) -> Option<String> {
1966 self.inner
1967 .base_text_exists
1968 .then(|| self.inner.base_text.read(cx).text())
1969 }
1970
1971 #[cfg(any(test, feature = "test-support"))]
1972 pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) {
1973 let language = self.base_text(cx).language().cloned();
1974 let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
1975 let fut = self.update_diff(buffer.clone(), base_text, None, language, cx);
1976 let fg_executor = cx.foreground_executor().clone();
1977 let snapshot = fg_executor.block_on(fut);
1978 let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx);
1979 let change = fg_executor.block_on(fut);
1980 cx.emit(BufferDiffEvent::DiffChanged(change));
1981 }
1982
1983 pub fn base_text_buffer(&self) -> &Entity<language::Buffer> {
1984 &self.inner.base_text
1985 }
1986}
1987
1988impl DiffHunk {
1989 pub fn is_created_file(&self) -> bool {
1990 self.diff_base_byte_range == (0..0)
1991 && self.buffer_range.start.is_min()
1992 && self.buffer_range.end.is_max()
1993 }
1994
1995 pub fn status(&self) -> DiffHunkStatus {
1996 let kind = if self.buffer_range.start == self.buffer_range.end {
1997 DiffHunkStatusKind::Deleted
1998 } else if self.diff_base_byte_range.is_empty() {
1999 DiffHunkStatusKind::Added
2000 } else {
2001 DiffHunkStatusKind::Modified
2002 };
2003 DiffHunkStatus {
2004 kind,
2005 secondary: self.secondary_status,
2006 }
2007 }
2008}
2009
2010impl DiffHunkStatus {
2011 pub fn has_secondary_hunk(&self) -> bool {
2012 matches!(
2013 self.secondary,
2014 DiffHunkSecondaryStatus::HasSecondaryHunk
2015 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2016 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
2017 )
2018 }
2019
2020 pub fn is_pending(&self) -> bool {
2021 matches!(
2022 self.secondary,
2023 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2024 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2025 )
2026 }
2027
2028 pub fn is_deleted(&self) -> bool {
2029 self.kind == DiffHunkStatusKind::Deleted
2030 }
2031
2032 pub fn is_added(&self) -> bool {
2033 self.kind == DiffHunkStatusKind::Added
2034 }
2035
2036 pub fn is_modified(&self) -> bool {
2037 self.kind == DiffHunkStatusKind::Modified
2038 }
2039
2040 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
2041 Self {
2042 kind: DiffHunkStatusKind::Added,
2043 secondary,
2044 }
2045 }
2046
2047 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
2048 Self {
2049 kind: DiffHunkStatusKind::Modified,
2050 secondary,
2051 }
2052 }
2053
2054 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
2055 Self {
2056 kind: DiffHunkStatusKind::Deleted,
2057 secondary,
2058 }
2059 }
2060
2061 pub fn deleted_none() -> Self {
2062 Self {
2063 kind: DiffHunkStatusKind::Deleted,
2064 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2065 }
2066 }
2067
2068 pub fn added_none() -> Self {
2069 Self {
2070 kind: DiffHunkStatusKind::Added,
2071 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2072 }
2073 }
2074
2075 pub fn modified_none() -> Self {
2076 Self {
2077 kind: DiffHunkStatusKind::Modified,
2078 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2079 }
2080 }
2081}
2082
2083#[cfg(any(test, feature = "test-support"))]
2084#[track_caller]
2085pub fn assert_hunks<ExpectedText, HunkIter>(
2086 diff_hunks: HunkIter,
2087 buffer: &text::BufferSnapshot,
2088 diff_base: &str,
2089 // Line range, deleted, added, status
2090 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
2091) where
2092 HunkIter: Iterator<Item = DiffHunk>,
2093 ExpectedText: AsRef<str>,
2094{
2095 let actual_hunks = diff_hunks
2096 .map(|hunk| {
2097 (
2098 hunk.range.clone(),
2099 &diff_base[hunk.diff_base_byte_range.clone()],
2100 buffer
2101 .text_for_range(hunk.range.clone())
2102 .collect::<String>(),
2103 hunk.status(),
2104 )
2105 })
2106 .collect::<Vec<_>>();
2107
2108 let expected_hunks: Vec<_> = expected_hunks
2109 .iter()
2110 .map(|(line_range, deleted_text, added_text, status)| {
2111 (
2112 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
2113 deleted_text.as_ref(),
2114 added_text.as_ref().to_string(),
2115 *status,
2116 )
2117 })
2118 .collect();
2119
2120 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
2121}
2122
2123#[cfg(test)]
2124mod tests {
2125 use std::{fmt::Write as _, sync::mpsc};
2126
2127 use super::*;
2128 use gpui::TestAppContext;
2129 use pretty_assertions::{assert_eq, assert_ne};
2130 use rand::{Rng as _, rngs::StdRng};
2131 use text::{Buffer, BufferId, ReplicaId, Rope};
2132 use unindent::Unindent as _;
2133 use util::test::marked_text_ranges;
2134
2135 #[ctor::ctor]
2136 fn init_logger() {
2137 zlog::init_test();
2138 }
2139
2140 #[gpui::test]
2141 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
2142 let diff_base = "
2143 one
2144 two
2145 three
2146 "
2147 .unindent();
2148
2149 let buffer_text = "
2150 one
2151 HELLO
2152 three
2153 "
2154 .unindent();
2155
2156 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2157 let mut diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2158 assert_hunks(
2159 diff.hunks_intersecting_range(
2160 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2161 &buffer,
2162 ),
2163 &buffer,
2164 &diff_base,
2165 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
2166 );
2167
2168 buffer.edit([(0..0, "point five\n")]);
2169 diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2170 assert_hunks(
2171 diff.hunks_intersecting_range(
2172 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2173 &buffer,
2174 ),
2175 &buffer,
2176 &diff_base,
2177 &[
2178 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
2179 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
2180 ],
2181 );
2182
2183 diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2184 assert_hunks::<&str, _>(
2185 diff.hunks_intersecting_range(
2186 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2187 &buffer,
2188 ),
2189 &buffer,
2190 &diff_base,
2191 &[],
2192 );
2193 }
2194
2195 #[gpui::test]
2196 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
2197 let head_text = "
2198 zero
2199 one
2200 two
2201 three
2202 four
2203 five
2204 six
2205 seven
2206 eight
2207 nine
2208 "
2209 .unindent();
2210
2211 let index_text = "
2212 zero
2213 one
2214 TWO
2215 three
2216 FOUR
2217 five
2218 six
2219 seven
2220 eight
2221 NINE
2222 "
2223 .unindent();
2224
2225 let buffer_text = "
2226 zero
2227 one
2228 TWO
2229 three
2230 FOUR
2231 FIVE
2232 six
2233 SEVEN
2234 eight
2235 nine
2236 "
2237 .unindent();
2238
2239 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2240 let unstaged_diff = BufferDiffSnapshot::new_sync(&buffer, index_text, cx);
2241 let mut uncommitted_diff = BufferDiffSnapshot::new_sync(&buffer, head_text.clone(), cx);
2242 uncommitted_diff.secondary_diff = Some(Arc::new(unstaged_diff));
2243
2244 let expected_hunks = vec![
2245 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
2246 (
2247 4..6,
2248 "four\nfive\n",
2249 "FOUR\nFIVE\n",
2250 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
2251 ),
2252 (
2253 7..8,
2254 "seven\n",
2255 "SEVEN\n",
2256 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
2257 ),
2258 ];
2259
2260 assert_hunks(
2261 uncommitted_diff.hunks_intersecting_range(
2262 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2263 &buffer,
2264 ),
2265 &buffer,
2266 &head_text,
2267 &expected_hunks,
2268 );
2269 }
2270
2271 #[gpui::test]
2272 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
2273 let diff_base = "
2274 one
2275 two
2276 three
2277 four
2278 five
2279 six
2280 seven
2281 eight
2282 nine
2283 ten
2284 "
2285 .unindent();
2286
2287 let buffer_text = "
2288 A
2289 one
2290 B
2291 two
2292 C
2293 three
2294 HELLO
2295 four
2296 five
2297 SIXTEEN
2298 seven
2299 eight
2300 WORLD
2301 nine
2302
2303 ten
2304
2305 "
2306 .unindent();
2307
2308 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2309 let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx);
2310 assert_eq!(
2311 diff.hunks_intersecting_range(
2312 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2313 &buffer
2314 )
2315 .count(),
2316 8
2317 );
2318
2319 assert_hunks(
2320 diff.hunks_intersecting_range(
2321 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
2322 &buffer,
2323 ),
2324 &buffer,
2325 &diff_base,
2326 &[
2327 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
2328 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
2329 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
2330 ],
2331 );
2332 }
2333
2334 #[gpui::test]
2335 async fn test_stage_hunk(cx: &mut TestAppContext) {
2336 struct Example {
2337 name: &'static str,
2338 head_text: String,
2339 index_text: String,
2340 buffer_marked_text: String,
2341 final_index_text: String,
2342 }
2343
2344 let table = [
2345 Example {
2346 name: "uncommitted hunk straddles end of unstaged hunk",
2347 head_text: "
2348 one
2349 two
2350 three
2351 four
2352 five
2353 "
2354 .unindent(),
2355 index_text: "
2356 one
2357 TWO_HUNDRED
2358 three
2359 FOUR_HUNDRED
2360 five
2361 "
2362 .unindent(),
2363 buffer_marked_text: "
2364 ZERO
2365 one
2366 two
2367 «THREE_HUNDRED
2368 FOUR_HUNDRED»
2369 five
2370 SIX
2371 "
2372 .unindent(),
2373 final_index_text: "
2374 one
2375 two
2376 THREE_HUNDRED
2377 FOUR_HUNDRED
2378 five
2379 "
2380 .unindent(),
2381 },
2382 Example {
2383 name: "uncommitted hunk straddles start of unstaged hunk",
2384 head_text: "
2385 one
2386 two
2387 three
2388 four
2389 five
2390 "
2391 .unindent(),
2392 index_text: "
2393 one
2394 TWO_HUNDRED
2395 three
2396 FOUR_HUNDRED
2397 five
2398 "
2399 .unindent(),
2400 buffer_marked_text: "
2401 ZERO
2402 one
2403 «TWO_HUNDRED
2404 THREE_HUNDRED»
2405 four
2406 five
2407 SIX
2408 "
2409 .unindent(),
2410 final_index_text: "
2411 one
2412 TWO_HUNDRED
2413 THREE_HUNDRED
2414 four
2415 five
2416 "
2417 .unindent(),
2418 },
2419 Example {
2420 name: "uncommitted hunk strictly contains unstaged hunks",
2421 head_text: "
2422 one
2423 two
2424 three
2425 four
2426 five
2427 six
2428 seven
2429 "
2430 .unindent(),
2431 index_text: "
2432 one
2433 TWO
2434 THREE
2435 FOUR
2436 FIVE
2437 SIX
2438 seven
2439 "
2440 .unindent(),
2441 buffer_marked_text: "
2442 one
2443 TWO
2444 «THREE_HUNDRED
2445 FOUR
2446 FIVE_HUNDRED»
2447 SIX
2448 seven
2449 "
2450 .unindent(),
2451 final_index_text: "
2452 one
2453 TWO
2454 THREE_HUNDRED
2455 FOUR
2456 FIVE_HUNDRED
2457 SIX
2458 seven
2459 "
2460 .unindent(),
2461 },
2462 Example {
2463 name: "uncommitted deletion hunk",
2464 head_text: "
2465 one
2466 two
2467 three
2468 four
2469 five
2470 "
2471 .unindent(),
2472 index_text: "
2473 one
2474 two
2475 three
2476 four
2477 five
2478 "
2479 .unindent(),
2480 buffer_marked_text: "
2481 one
2482 ˇfive
2483 "
2484 .unindent(),
2485 final_index_text: "
2486 one
2487 five
2488 "
2489 .unindent(),
2490 },
2491 Example {
2492 name: "one unstaged hunk that contains two uncommitted hunks",
2493 head_text: "
2494 one
2495 two
2496
2497 three
2498 four
2499 "
2500 .unindent(),
2501 index_text: "
2502 one
2503 two
2504 three
2505 four
2506 "
2507 .unindent(),
2508 buffer_marked_text: "
2509 «one
2510
2511 three // modified
2512 four»
2513 "
2514 .unindent(),
2515 final_index_text: "
2516 one
2517
2518 three // modified
2519 four
2520 "
2521 .unindent(),
2522 },
2523 Example {
2524 name: "one uncommitted hunk that contains two unstaged hunks",
2525 head_text: "
2526 one
2527 two
2528 three
2529 four
2530 five
2531 "
2532 .unindent(),
2533 index_text: "
2534 ZERO
2535 one
2536 TWO
2537 THREE
2538 FOUR
2539 five
2540 "
2541 .unindent(),
2542 buffer_marked_text: "
2543 «one
2544 TWO_HUNDRED
2545 THREE
2546 FOUR_HUNDRED
2547 five»
2548 "
2549 .unindent(),
2550 final_index_text: "
2551 ZERO
2552 one
2553 TWO_HUNDRED
2554 THREE
2555 FOUR_HUNDRED
2556 five
2557 "
2558 .unindent(),
2559 },
2560 ];
2561
2562 for example in table {
2563 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2564 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2565 let hunk_range =
2566 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2567
2568 let unstaged_diff =
2569 cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
2570
2571 let uncommitted_diff = cx.new(|cx| {
2572 let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx);
2573 diff.set_secondary_diff(unstaged_diff);
2574 diff
2575 });
2576
2577 uncommitted_diff.update(cx, |diff, cx| {
2578 let hunks = diff
2579 .snapshot(cx)
2580 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2581 .collect::<Vec<_>>();
2582 for hunk in &hunks {
2583 assert_ne!(
2584 hunk.secondary_status,
2585 DiffHunkSecondaryStatus::NoSecondaryHunk
2586 )
2587 }
2588
2589 let new_index_text = diff
2590 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2591 .unwrap()
2592 .to_string();
2593
2594 let hunks = diff
2595 .snapshot(cx)
2596 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2597 .collect::<Vec<_>>();
2598 for hunk in &hunks {
2599 assert_eq!(
2600 hunk.secondary_status,
2601 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2602 )
2603 }
2604
2605 pretty_assertions::assert_eq!(
2606 new_index_text,
2607 example.final_index_text,
2608 "example: {}",
2609 example.name
2610 );
2611 });
2612 }
2613 }
2614
2615 #[gpui::test]
2616 async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) {
2617 // This test reproduces a crash where staging all hunks would cause an underflow
2618 // when there's one large unstaged hunk containing multiple uncommitted hunks.
2619 let head_text = "
2620 aaa
2621 bbb
2622 ccc
2623 ddd
2624 eee
2625 fff
2626 ggg
2627 hhh
2628 iii
2629 jjj
2630 kkk
2631 lll
2632 "
2633 .unindent();
2634
2635 let index_text = "
2636 aaa
2637 bbb
2638 CCC-index
2639 DDD-index
2640 EEE-index
2641 FFF-index
2642 GGG-index
2643 HHH-index
2644 III-index
2645 JJJ-index
2646 kkk
2647 lll
2648 "
2649 .unindent();
2650
2651 let buffer_text = "
2652 aaa
2653 bbb
2654 ccc-modified
2655 ddd
2656 eee-modified
2657 fff
2658 ggg
2659 hhh-modified
2660 iii
2661 jjj
2662 kkk
2663 lll
2664 "
2665 .unindent();
2666
2667 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2668
2669 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2670 let uncommitted_diff = cx.new(|cx| {
2671 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2672 diff.set_secondary_diff(unstaged_diff);
2673 diff
2674 });
2675
2676 uncommitted_diff.update(cx, |diff, cx| {
2677 diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
2678 });
2679 }
2680
2681 #[gpui::test]
2682 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2683 let head_text = "
2684 one
2685 two
2686 three
2687 "
2688 .unindent();
2689 let index_text = head_text.clone();
2690 let buffer_text = "
2691 one
2692 three
2693 "
2694 .unindent();
2695
2696 let buffer = Buffer::new(
2697 ReplicaId::LOCAL,
2698 BufferId::new(1).unwrap(),
2699 buffer_text.clone(),
2700 );
2701 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2702 let uncommitted_diff = cx.new(|cx| {
2703 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2704 diff.set_secondary_diff(unstaged_diff.clone());
2705 diff
2706 });
2707
2708 uncommitted_diff.update(cx, |diff, cx| {
2709 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2710
2711 let new_index_text = diff
2712 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
2713 .unwrap()
2714 .to_string();
2715 assert_eq!(new_index_text, buffer_text);
2716
2717 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2718 assert_eq!(
2719 hunk.secondary_status,
2720 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2721 );
2722
2723 let index_text = diff
2724 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
2725 .unwrap()
2726 .to_string();
2727 assert_eq!(index_text, head_text);
2728
2729 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2730 // optimistically unstaged (fine, could also be HasSecondaryHunk)
2731 assert_eq!(
2732 hunk.secondary_status,
2733 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2734 );
2735 });
2736 }
2737
2738 #[gpui::test]
2739 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
2740 let base_text = "
2741 zero
2742 one
2743 two
2744 three
2745 four
2746 five
2747 six
2748 seven
2749 eight
2750 nine
2751 "
2752 .unindent();
2753
2754 let buffer_text_1 = "
2755 one
2756 three
2757 four
2758 five
2759 SIX
2760 seven
2761 eight
2762 NINE
2763 "
2764 .unindent();
2765
2766 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
2767
2768 let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2769 let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2770 let DiffChanged {
2771 changed_range,
2772 base_text_changed_range,
2773 extended_range: _,
2774 } = compare_hunks(
2775 &diff_1.inner.hunks,
2776 &empty_diff.inner.hunks,
2777 &buffer,
2778 &buffer,
2779 &diff_1.base_text(),
2780 &diff_1.base_text(),
2781 );
2782 let range = changed_range.unwrap();
2783 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
2784 let base_text_range = base_text_changed_range.unwrap();
2785 assert_eq!(
2786 base_text_range.to_point(diff_1.base_text()),
2787 Point::new(0, 0)..Point::new(10, 0)
2788 );
2789
2790 // Edit does affects the diff because it recalculates word diffs.
2791 buffer.edit_via_marked_text(
2792 &"
2793 one
2794 three
2795 four
2796 five
2797 «SIX.5»
2798 seven
2799 eight
2800 NINE
2801 "
2802 .unindent(),
2803 );
2804 let diff_2 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2805 let DiffChanged {
2806 changed_range,
2807 base_text_changed_range,
2808 extended_range: _,
2809 } = compare_hunks(
2810 &diff_2.inner.hunks,
2811 &diff_1.inner.hunks,
2812 &buffer,
2813 &buffer,
2814 diff_2.base_text(),
2815 diff_2.base_text(),
2816 );
2817 assert_eq!(
2818 changed_range.unwrap().to_point(&buffer),
2819 Point::new(4, 0)..Point::new(5, 0),
2820 );
2821 assert_eq!(
2822 base_text_changed_range
2823 .unwrap()
2824 .to_point(diff_2.base_text()),
2825 Point::new(6, 0)..Point::new(7, 0),
2826 );
2827
2828 // Edit turns a deletion hunk into a modification.
2829 buffer.edit_via_marked_text(
2830 &"
2831 one
2832 «THREE»
2833 four
2834 five
2835 SIX.5
2836 seven
2837 eight
2838 NINE
2839 "
2840 .unindent(),
2841 );
2842 let diff_3 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2843 let DiffChanged {
2844 changed_range,
2845 base_text_changed_range,
2846 extended_range: _,
2847 } = compare_hunks(
2848 &diff_3.inner.hunks,
2849 &diff_2.inner.hunks,
2850 &buffer,
2851 &buffer,
2852 diff_3.base_text(),
2853 diff_3.base_text(),
2854 );
2855 let range = changed_range.unwrap();
2856 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2857 let base_text_range = base_text_changed_range.unwrap();
2858 assert_eq!(
2859 base_text_range.to_point(diff_3.base_text()),
2860 Point::new(2, 0)..Point::new(4, 0)
2861 );
2862
2863 // Edit turns a modification hunk into a deletion.
2864 buffer.edit_via_marked_text(
2865 &"
2866 one
2867 THREE
2868 four
2869 five«»
2870 seven
2871 eight
2872 NINE
2873 "
2874 .unindent(),
2875 );
2876 let diff_4 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2877 let DiffChanged {
2878 changed_range,
2879 base_text_changed_range,
2880 extended_range: _,
2881 } = compare_hunks(
2882 &diff_4.inner.hunks,
2883 &diff_3.inner.hunks,
2884 &buffer,
2885 &buffer,
2886 diff_4.base_text(),
2887 diff_4.base_text(),
2888 );
2889 let range = changed_range.unwrap();
2890 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2891 let base_text_range = base_text_changed_range.unwrap();
2892 assert_eq!(
2893 base_text_range.to_point(diff_4.base_text()),
2894 Point::new(6, 0)..Point::new(7, 0)
2895 );
2896
2897 // Edit introduces a new insertion hunk.
2898 buffer.edit_via_marked_text(
2899 &"
2900 one
2901 THREE
2902 four«
2903 FOUR.5
2904 »five
2905 seven
2906 eight
2907 NINE
2908 "
2909 .unindent(),
2910 );
2911 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2912 let DiffChanged {
2913 changed_range,
2914 base_text_changed_range,
2915 extended_range: _,
2916 } = compare_hunks(
2917 &diff_5.inner.hunks,
2918 &diff_4.inner.hunks,
2919 &buffer,
2920 &buffer,
2921 diff_5.base_text(),
2922 diff_5.base_text(),
2923 );
2924 let range = changed_range.unwrap();
2925 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2926 let base_text_range = base_text_changed_range.unwrap();
2927 assert_eq!(
2928 base_text_range.to_point(diff_5.base_text()),
2929 Point::new(5, 0)..Point::new(5, 0)
2930 );
2931
2932 // Edit removes a hunk.
2933 buffer.edit_via_marked_text(
2934 &"
2935 one
2936 THREE
2937 four
2938 FOUR.5
2939 five
2940 seven
2941 eight
2942 «nine»
2943 "
2944 .unindent(),
2945 );
2946 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2947 let DiffChanged {
2948 changed_range,
2949 base_text_changed_range,
2950 extended_range: _,
2951 } = compare_hunks(
2952 &diff_6.inner.hunks,
2953 &diff_5.inner.hunks,
2954 &buffer,
2955 &buffer,
2956 diff_6.base_text(),
2957 diff_6.base_text(),
2958 );
2959 let range = changed_range.unwrap();
2960 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2961 let base_text_range = base_text_changed_range.unwrap();
2962 assert_eq!(
2963 base_text_range.to_point(diff_6.base_text()),
2964 Point::new(9, 0)..Point::new(10, 0)
2965 );
2966
2967 buffer.edit_via_marked_text(
2968 &"
2969 one
2970 THREE
2971 four«»
2972 five
2973 seven
2974 eight
2975 «NINE»
2976 "
2977 .unindent(),
2978 );
2979
2980 let diff_7 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2981 let DiffChanged {
2982 changed_range,
2983 base_text_changed_range,
2984 extended_range: _,
2985 } = compare_hunks(
2986 &diff_7.inner.hunks,
2987 &diff_6.inner.hunks,
2988 &buffer,
2989 &buffer,
2990 diff_7.base_text(),
2991 diff_7.base_text(),
2992 );
2993 let range = changed_range.unwrap();
2994 assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0));
2995 let base_text_range = base_text_changed_range.unwrap();
2996 assert_eq!(
2997 base_text_range.to_point(diff_7.base_text()),
2998 Point::new(5, 0)..Point::new(10, 0)
2999 );
3000
3001 buffer.edit_via_marked_text(
3002 &"
3003 one
3004 THREE
3005 four
3006 five«»seven
3007 eight
3008 NINE
3009 "
3010 .unindent(),
3011 );
3012
3013 let diff_8 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
3014 let DiffChanged {
3015 changed_range,
3016 base_text_changed_range,
3017 extended_range: _,
3018 } = compare_hunks(
3019 &diff_8.inner.hunks,
3020 &diff_7.inner.hunks,
3021 &buffer,
3022 &buffer,
3023 diff_8.base_text(),
3024 diff_8.base_text(),
3025 );
3026 let range = changed_range.unwrap();
3027 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4));
3028 let base_text_range = base_text_changed_range.unwrap();
3029 assert_eq!(
3030 base_text_range.to_point(diff_8.base_text()),
3031 Point::new(5, 0)..Point::new(8, 0)
3032 );
3033 }
3034
3035 #[gpui::test(iterations = 100)]
3036 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
3037 fn gen_line(rng: &mut StdRng) -> String {
3038 if rng.random_bool(0.2) {
3039 "\n".to_owned()
3040 } else {
3041 let c = rng.random_range('A'..='Z');
3042 format!("{c}{c}{c}\n")
3043 }
3044 }
3045
3046 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
3047 let mut old_lines = {
3048 let mut old_lines = Vec::new();
3049 let old_lines_iter = head.lines();
3050 for line in old_lines_iter {
3051 assert!(!line.ends_with("\n"));
3052 old_lines.push(line.to_owned());
3053 }
3054 if old_lines.last().is_some_and(|line| line.is_empty()) {
3055 old_lines.pop();
3056 }
3057 old_lines.into_iter()
3058 };
3059 let mut result = String::new();
3060 let unchanged_count = rng.random_range(0..=old_lines.len());
3061 result +=
3062 &old_lines
3063 .by_ref()
3064 .take(unchanged_count)
3065 .fold(String::new(), |mut s, line| {
3066 writeln!(&mut s, "{line}").unwrap();
3067 s
3068 });
3069 while old_lines.len() > 0 {
3070 let deleted_count = rng.random_range(0..=old_lines.len());
3071 let _advance = old_lines
3072 .by_ref()
3073 .take(deleted_count)
3074 .map(|line| line.len() + 1)
3075 .sum::<usize>();
3076 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
3077 let added_count = rng.random_range(minimum_added..=5);
3078 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
3079 result += &addition;
3080
3081 if old_lines.len() > 0 {
3082 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
3083 if blank_lines == old_lines.len() {
3084 break;
3085 };
3086 let unchanged_count =
3087 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
3088 result += &old_lines.by_ref().take(unchanged_count).fold(
3089 String::new(),
3090 |mut s, line| {
3091 writeln!(&mut s, "{line}").unwrap();
3092 s
3093 },
3094 );
3095 }
3096 }
3097 result
3098 }
3099
3100 fn uncommitted_diff(
3101 working_copy: &language::BufferSnapshot,
3102 index_text: &Rope,
3103 head_text: String,
3104 cx: &mut TestAppContext,
3105 ) -> Entity<BufferDiff> {
3106 let secondary = cx.new(|cx| {
3107 BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
3108 });
3109 cx.new(|cx| {
3110 let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
3111 diff.secondary_diff = Some(secondary);
3112 diff
3113 })
3114 }
3115
3116 let operations = std::env::var("OPERATIONS")
3117 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
3118 .unwrap_or(10);
3119
3120 let rng = &mut rng;
3121 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
3122 writeln!(&mut s, "{c}{c}{c}").unwrap();
3123 s
3124 });
3125 let working_copy = gen_working_copy(rng, &head_text);
3126 let working_copy = cx.new(|cx| {
3127 language::Buffer::local_normalized(
3128 Rope::from(working_copy.as_str()),
3129 text::LineEnding::default(),
3130 cx,
3131 )
3132 });
3133 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
3134 let mut index_text = if rng.random() {
3135 Rope::from(head_text.as_str())
3136 } else {
3137 working_copy.as_rope().clone()
3138 };
3139
3140 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3141 let mut hunks = diff.update(cx, |diff, cx| {
3142 diff.snapshot(cx)
3143 .hunks_intersecting_range(
3144 Anchor::min_max_range_for_buffer(diff.buffer_id),
3145 &working_copy,
3146 )
3147 .collect::<Vec<_>>()
3148 });
3149 if hunks.is_empty() {
3150 return;
3151 }
3152
3153 for _ in 0..operations {
3154 let i = rng.random_range(0..hunks.len());
3155 let hunk = &mut hunks[i];
3156 let hunk_to_change = hunk.clone();
3157 let stage = match hunk.secondary_status {
3158 DiffHunkSecondaryStatus::HasSecondaryHunk => {
3159 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
3160 true
3161 }
3162 DiffHunkSecondaryStatus::NoSecondaryHunk => {
3163 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
3164 false
3165 }
3166 _ => unreachable!(),
3167 };
3168
3169 index_text = diff.update(cx, |diff, cx| {
3170 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
3171 .unwrap()
3172 });
3173
3174 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3175 let found_hunks = diff.update(cx, |diff, cx| {
3176 diff.snapshot(cx)
3177 .hunks_intersecting_range(
3178 Anchor::min_max_range_for_buffer(diff.buffer_id),
3179 &working_copy,
3180 )
3181 .collect::<Vec<_>>()
3182 });
3183 assert_eq!(hunks.len(), found_hunks.len());
3184
3185 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
3186 assert_eq!(
3187 expected_hunk.buffer_range.to_point(&working_copy),
3188 found_hunk.buffer_range.to_point(&working_copy)
3189 );
3190 assert_eq!(
3191 expected_hunk.diff_base_byte_range,
3192 found_hunk.diff_base_byte_range
3193 );
3194 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
3195 }
3196 hunks = found_hunks;
3197 }
3198 }
3199
3200 #[gpui::test]
3201 async fn test_changed_ranges(cx: &mut gpui::TestAppContext) {
3202 let base_text = "
3203 one
3204 two
3205 three
3206 four
3207 five
3208 six
3209 "
3210 .unindent();
3211 let buffer_text = "
3212 one
3213 TWO
3214 three
3215 four
3216 FIVE
3217 six
3218 "
3219 .unindent();
3220 let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
3221 let diff = cx.new(|cx| {
3222 BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
3223 });
3224 cx.run_until_parked();
3225 let (tx, rx) = mpsc::channel();
3226 let subscription =
3227 cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
3228
3229 let snapshot = buffer.update(cx, |buffer, cx| {
3230 buffer.set_text(
3231 "
3232 ONE
3233 TWO
3234 THREE
3235 FOUR
3236 FIVE
3237 SIX
3238 "
3239 .unindent(),
3240 cx,
3241 );
3242 buffer.text_snapshot()
3243 });
3244 let update = diff
3245 .update(cx, |diff, cx| {
3246 diff.update_diff(
3247 snapshot.clone(),
3248 Some(base_text.as_str().into()),
3249 None,
3250 None,
3251 cx,
3252 )
3253 })
3254 .await;
3255 diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx))
3256 .await;
3257 cx.run_until_parked();
3258 drop(subscription);
3259 let events = rx.into_iter().collect::<Vec<_>>();
3260 match events.as_slice() {
3261 [
3262 BufferDiffEvent::DiffChanged(DiffChanged {
3263 changed_range: _,
3264 base_text_changed_range,
3265 extended_range: _,
3266 }),
3267 ] => {
3268 // TODO(cole) this seems like it should pass but currently fails (see compare_hunks)
3269 // assert_eq!(
3270 // *changed_range,
3271 // Some(Anchor::min_max_range_for_buffer(
3272 // buffer.read_with(cx, |buffer, _| buffer.remote_id())
3273 // ))
3274 // );
3275 assert_eq!(*base_text_changed_range, Some(0..base_text.len()));
3276 }
3277 _ => panic!("unexpected events: {:?}", events),
3278 }
3279 }
3280
3281 #[gpui::test]
3282 async fn test_extended_range(cx: &mut TestAppContext) {
3283 let base_text = "
3284 aaa
3285 bbb
3286
3287
3288
3289
3290
3291 ccc
3292 ddd
3293 "
3294 .unindent();
3295
3296 let buffer_text = "
3297 aaa
3298 bbb
3299
3300
3301
3302
3303
3304 CCC
3305 ddd
3306 "
3307 .unindent();
3308
3309 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
3310 let old_buffer = buffer.snapshot().clone();
3311 let diff_a = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3312
3313 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")]);
3314 let diff_b = BufferDiffSnapshot::new_sync(&buffer, base_text, cx);
3315
3316 let DiffChanged {
3317 changed_range,
3318 base_text_changed_range: _,
3319 extended_range,
3320 } = compare_hunks(
3321 &diff_b.inner.hunks,
3322 &diff_a.inner.hunks,
3323 &old_buffer,
3324 &buffer,
3325 &diff_a.base_text(),
3326 &diff_a.base_text(),
3327 );
3328
3329 let changed_range = changed_range.unwrap();
3330 assert_eq!(
3331 changed_range.to_point(&buffer),
3332 Point::new(7, 0)..Point::new(9, 0),
3333 "changed_range should span from old hunk position to new hunk end"
3334 );
3335
3336 let extended_range = extended_range.unwrap();
3337 assert_eq!(
3338 extended_range.start.to_point(&buffer),
3339 Point::new(1, 3),
3340 "extended_range.start should extend to include the edit outside changed_range"
3341 );
3342 assert_eq!(
3343 extended_range.end.to_point(&buffer),
3344 Point::new(9, 0),
3345 "extended_range.end should collapse to changed_range.end when no edits in end margin"
3346 );
3347
3348 let base_text_2 = "
3349 one
3350 two
3351 three
3352 four
3353 five
3354 six
3355 seven
3356 eight
3357 "
3358 .unindent();
3359
3360 let buffer_text_2 = "
3361 ONE
3362 two
3363 THREE
3364 four
3365 FIVE
3366 six
3367 SEVEN
3368 eight
3369 "
3370 .unindent();
3371
3372 let mut buffer_2 = Buffer::new(ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2);
3373 let old_buffer_2 = buffer_2.snapshot().clone();
3374 let diff_2a = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2.clone(), cx);
3375
3376 buffer_2.edit([(Point::new(4, 0)..Point::new(4, 4), "FIVE_CHANGED")]);
3377 let diff_2b = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2, cx);
3378
3379 let DiffChanged {
3380 changed_range,
3381 base_text_changed_range: _,
3382 extended_range,
3383 } = compare_hunks(
3384 &diff_2b.inner.hunks,
3385 &diff_2a.inner.hunks,
3386 &old_buffer_2,
3387 &buffer_2,
3388 &diff_2a.base_text(),
3389 &diff_2a.base_text(),
3390 );
3391
3392 let changed_range = changed_range.unwrap();
3393 assert_eq!(
3394 changed_range.to_point(&buffer_2),
3395 Point::new(4, 0)..Point::new(5, 0),
3396 "changed_range should be just the hunk that changed (FIVE)"
3397 );
3398
3399 let extended_range = extended_range.unwrap();
3400 assert_eq!(
3401 extended_range.to_point(&buffer_2),
3402 Point::new(4, 0)..Point::new(5, 0),
3403 "extended_range should equal changed_range when edit is within the hunk"
3404 );
3405 }
3406
3407 #[gpui::test]
3408 async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) {
3409 // Use a shared base text buffer so that anchors from old and new snapshots
3410 // share the same remote_id and resolve correctly across versions.
3411 let initial_base = "aaa\nbbb\nccc\nddd\neee\n";
3412 let mut base_text_buffer = Buffer::new(
3413 ReplicaId::LOCAL,
3414 BufferId::new(99).unwrap(),
3415 initial_base.to_string(),
3416 );
3417
3418 // --- Scenario 1: Base text gains a line, producing a new deletion hunk ---
3419 //
3420 // Buffer has a modification (ccc → CCC). When the base text gains
3421 // a new line "XXX" after "aaa", the diff now also contains a
3422 // deletion for that line, and the modification hunk shifts in the
3423 // base text.
3424 let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n";
3425 let buffer = Buffer::new(
3426 ReplicaId::LOCAL,
3427 BufferId::new(1).unwrap(),
3428 buffer_text_1.to_string(),
3429 );
3430
3431 let old_base_snapshot_1 = base_text_buffer.snapshot().clone();
3432 let old_hunks_1 = compute_hunks(
3433 Some((Arc::from(initial_base), Rope::from(initial_base))),
3434 buffer.snapshot(),
3435 None,
3436 );
3437
3438 // Insert "XXX\n" after "aaa\n" in the base text.
3439 base_text_buffer.edit([(4..4, "XXX\n")]);
3440 let new_base_str_1: Arc<str> = Arc::from(base_text_buffer.text().as_str());
3441 let new_base_snapshot_1 = base_text_buffer.snapshot();
3442
3443 let new_hunks_1 = compute_hunks(
3444 Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))),
3445 buffer.snapshot(),
3446 None,
3447 );
3448
3449 let DiffChanged {
3450 changed_range,
3451 base_text_changed_range,
3452 extended_range: _,
3453 } = compare_hunks(
3454 &new_hunks_1,
3455 &old_hunks_1,
3456 &buffer.snapshot(),
3457 &buffer.snapshot(),
3458 &old_base_snapshot_1,
3459 &new_base_snapshot_1,
3460 );
3461
3462 // The new deletion hunk (XXX) starts at buffer row 1 and the
3463 // modification hunk (ccc → CCC) now has a different
3464 // diff_base_byte_range, so the changed range spans both.
3465 let range = changed_range.unwrap();
3466 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),);
3467 let base_range = base_text_changed_range.unwrap();
3468 assert_eq!(
3469 base_range.to_point(&new_base_snapshot_1),
3470 Point::new(1, 0)..Point::new(4, 0),
3471 );
3472
3473 // --- Scenario 2: Base text changes to match the buffer (hunk disappears) ---
3474 //
3475 // Start fresh with a simple base text.
3476 let simple_base = "one\ntwo\nthree\n";
3477 let mut base_buf_2 = Buffer::new(
3478 ReplicaId::LOCAL,
3479 BufferId::new(100).unwrap(),
3480 simple_base.to_string(),
3481 );
3482
3483 let buffer_text_2 = "one\nTWO\nthree\n";
3484 let buffer_2 = Buffer::new(
3485 ReplicaId::LOCAL,
3486 BufferId::new(2).unwrap(),
3487 buffer_text_2.to_string(),
3488 );
3489
3490 let old_base_snapshot_2 = base_buf_2.snapshot().clone();
3491 let old_hunks_2 = compute_hunks(
3492 Some((Arc::from(simple_base), Rope::from(simple_base))),
3493 buffer_2.snapshot(),
3494 None,
3495 );
3496
3497 // The base text is edited so "two" becomes "TWO", now matching the buffer.
3498 base_buf_2.edit([(4..7, "TWO")]);
3499 let new_base_str_2: Arc<str> = Arc::from(base_buf_2.text().as_str());
3500 let new_base_snapshot_2 = base_buf_2.snapshot();
3501
3502 let new_hunks_2 = compute_hunks(
3503 Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))),
3504 buffer_2.snapshot(),
3505 None,
3506 );
3507
3508 let DiffChanged {
3509 changed_range,
3510 base_text_changed_range,
3511 extended_range: _,
3512 } = compare_hunks(
3513 &new_hunks_2,
3514 &old_hunks_2,
3515 &buffer_2.snapshot(),
3516 &buffer_2.snapshot(),
3517 &old_base_snapshot_2,
3518 &new_base_snapshot_2,
3519 );
3520
3521 // The old modification hunk (two → TWO) is now gone because the
3522 // base text matches the buffer. The changed range covers where the
3523 // old hunk used to be.
3524 let range = changed_range.unwrap();
3525 assert_eq!(
3526 range.to_point(&buffer_2),
3527 Point::new(1, 0)..Point::new(2, 0),
3528 );
3529 let base_range = base_text_changed_range.unwrap();
3530 // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8).
3531 // anchor_after(4) is right-biased at the start of the deleted "two",
3532 // so after the edit replacing "two" with "TWO" it resolves past the
3533 // insertion to Point(1, 3).
3534 assert_eq!(
3535 base_range.to_point(&new_base_snapshot_2),
3536 Point::new(1, 3)..Point::new(2, 0),
3537 );
3538
3539 // --- Scenario 3: Base text edit changes one hunk but not another ---
3540 //
3541 // Two modification hunks exist. Only one of them is resolved by
3542 // the base text change; the other remains identical.
3543 let base_3 = "aaa\nbbb\nccc\nddd\neee\n";
3544 let mut base_buf_3 = Buffer::new(
3545 ReplicaId::LOCAL,
3546 BufferId::new(101).unwrap(),
3547 base_3.to_string(),
3548 );
3549
3550 let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n";
3551 let buffer_3 = Buffer::new(
3552 ReplicaId::LOCAL,
3553 BufferId::new(3).unwrap(),
3554 buffer_text_3.to_string(),
3555 );
3556
3557 let old_base_snapshot_3 = base_buf_3.snapshot().clone();
3558 let old_hunks_3 = compute_hunks(
3559 Some((Arc::from(base_3), Rope::from(base_3))),
3560 buffer_3.snapshot(),
3561 None,
3562 );
3563
3564 // Change "ddd" to "DDD" in the base text so that hunk disappears,
3565 // but "bbb" stays, so its hunk remains.
3566 base_buf_3.edit([(12..15, "DDD")]);
3567 let new_base_str_3: Arc<str> = Arc::from(base_buf_3.text().as_str());
3568 let new_base_snapshot_3 = base_buf_3.snapshot();
3569
3570 let new_hunks_3 = compute_hunks(
3571 Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))),
3572 buffer_3.snapshot(),
3573 None,
3574 );
3575
3576 let DiffChanged {
3577 changed_range,
3578 base_text_changed_range,
3579 extended_range: _,
3580 } = compare_hunks(
3581 &new_hunks_3,
3582 &old_hunks_3,
3583 &buffer_3.snapshot(),
3584 &buffer_3.snapshot(),
3585 &old_base_snapshot_3,
3586 &new_base_snapshot_3,
3587 );
3588
3589 // Only the second hunk (ddd → DDD) disappeared; the first hunk
3590 // (bbb → BBB) is unchanged, so the changed range covers only line 3.
3591 let range = changed_range.unwrap();
3592 assert_eq!(
3593 range.to_point(&buffer_3),
3594 Point::new(3, 0)..Point::new(4, 0),
3595 );
3596 let base_range = base_text_changed_range.unwrap();
3597 // anchor_after(12) is right-biased at the start of deleted "ddd",
3598 // so after the edit replacing "ddd" with "DDD" it resolves past
3599 // the insertion to Point(3, 3).
3600 assert_eq!(
3601 base_range.to_point(&new_base_snapshot_3),
3602 Point::new(3, 3)..Point::new(4, 0),
3603 );
3604
3605 // --- Scenario 4: Both buffer and base text change simultaneously ---
3606 //
3607 // The buffer gains an edit that introduces a new hunk while the
3608 // base text also changes.
3609 let base_4 = "alpha\nbeta\ngamma\ndelta\n";
3610 let mut base_buf_4 = Buffer::new(
3611 ReplicaId::LOCAL,
3612 BufferId::new(102).unwrap(),
3613 base_4.to_string(),
3614 );
3615
3616 let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n";
3617 let mut buffer_4 = Buffer::new(
3618 ReplicaId::LOCAL,
3619 BufferId::new(4).unwrap(),
3620 buffer_text_4.to_string(),
3621 );
3622
3623 let old_base_snapshot_4 = base_buf_4.snapshot().clone();
3624 let old_buffer_snapshot_4 = buffer_4.snapshot().clone();
3625 let old_hunks_4 = compute_hunks(
3626 Some((Arc::from(base_4), Rope::from(base_4))),
3627 buffer_4.snapshot(),
3628 None,
3629 );
3630
3631 // Edit the buffer: change "delta" to "DELTA" (new modification hunk).
3632 buffer_4.edit_via_marked_text(
3633 &"
3634 alpha
3635 BETA
3636 gamma
3637 «DELTA»
3638 "
3639 .unindent(),
3640 );
3641
3642 // Edit the base text: change "beta" to "BETA" (resolves that hunk).
3643 base_buf_4.edit([(6..10, "BETA")]);
3644 let new_base_str_4: Arc<str> = Arc::from(base_buf_4.text().as_str());
3645 let new_base_snapshot_4 = base_buf_4.snapshot();
3646
3647 let new_hunks_4 = compute_hunks(
3648 Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))),
3649 buffer_4.snapshot(),
3650 None,
3651 );
3652
3653 let DiffChanged {
3654 changed_range,
3655 base_text_changed_range,
3656 extended_range: _,
3657 } = compare_hunks(
3658 &new_hunks_4,
3659 &old_hunks_4,
3660 &old_buffer_snapshot_4,
3661 &buffer_4.snapshot(),
3662 &old_base_snapshot_4,
3663 &new_base_snapshot_4,
3664 );
3665
3666 // The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3)
3667 // appeared, so the changed range spans from line 1 through line 4.
3668 let range = changed_range.unwrap();
3669 assert_eq!(
3670 range.to_point(&buffer_4),
3671 Point::new(1, 0)..Point::new(4, 0),
3672 );
3673 let base_range = base_text_changed_range.unwrap();
3674 // The old BETA hunk's base range started at byte 6 ("beta"). After
3675 // the base text edit replacing "beta" with "BETA", anchor_after(6)
3676 // resolves past the insertion to Point(1, 4).
3677 assert_eq!(
3678 base_range.to_point(&new_base_snapshot_4),
3679 Point::new(1, 4)..Point::new(4, 0),
3680 );
3681 }
3682
3683 #[gpui::test(iterations = 100)]
3684 async fn test_patch_for_range_random(cx: &mut TestAppContext, mut rng: StdRng) {
3685 fn gen_line(rng: &mut StdRng) -> String {
3686 if rng.random_bool(0.2) {
3687 "\n".to_owned()
3688 } else {
3689 let c = rng.random_range('A'..='Z');
3690 format!("{c}{c}{c}\n")
3691 }
3692 }
3693
3694 fn gen_text(rng: &mut StdRng, line_count: usize) -> String {
3695 (0..line_count).map(|_| gen_line(rng)).collect()
3696 }
3697
3698 fn gen_edits_from(rng: &mut StdRng, base: &str) -> String {
3699 let mut old_lines: Vec<&str> = base.lines().collect();
3700 let mut result = String::new();
3701
3702 while !old_lines.is_empty() {
3703 let unchanged_count = rng.random_range(0..=old_lines.len());
3704 for _ in 0..unchanged_count {
3705 if old_lines.is_empty() {
3706 break;
3707 }
3708 result.push_str(old_lines.remove(0));
3709 result.push('\n');
3710 }
3711
3712 if old_lines.is_empty() {
3713 break;
3714 }
3715
3716 let deleted_count = rng.random_range(0..=old_lines.len().min(3));
3717 for _ in 0..deleted_count {
3718 if old_lines.is_empty() {
3719 break;
3720 }
3721 old_lines.remove(0);
3722 }
3723
3724 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
3725 let added_count = rng.random_range(minimum_added..=3);
3726 for _ in 0..added_count {
3727 result.push_str(&gen_line(rng));
3728 }
3729 }
3730
3731 result
3732 }
3733
3734 fn random_point_in_text(rng: &mut StdRng, lines: &[&str]) -> Point {
3735 if lines.is_empty() {
3736 return Point::zero();
3737 }
3738 let row = rng.random_range(0..lines.len() as u32);
3739 let line = lines[row as usize];
3740 let col = if line.is_empty() {
3741 0
3742 } else {
3743 rng.random_range(0..=line.len() as u32)
3744 };
3745 Point::new(row, col)
3746 }
3747
3748 fn random_range_in_text(rng: &mut StdRng, lines: &[&str]) -> RangeInclusive<Point> {
3749 let start = random_point_in_text(rng, lines);
3750 let end = random_point_in_text(rng, lines);
3751 if start <= end {
3752 start..=end
3753 } else {
3754 end..=start
3755 }
3756 }
3757
3758 fn points_in_range(range: &RangeInclusive<Point>, lines: &[&str]) -> Vec<Point> {
3759 let mut points = Vec::new();
3760 for row in range.start().row..=range.end().row {
3761 if row as usize >= lines.len() {
3762 points.push(Point::new(row, 0));
3763 continue;
3764 }
3765 let line = lines[row as usize];
3766 let start_col = if row == range.start().row {
3767 range.start().column
3768 } else {
3769 0
3770 };
3771 let end_col = if row == range.end().row {
3772 range.end().column
3773 } else {
3774 line.len() as u32
3775 };
3776 for col in start_col..=end_col {
3777 points.push(Point::new(row, col));
3778 }
3779 }
3780 points
3781 }
3782
3783 let rng = &mut rng;
3784
3785 let line_count = rng.random_range(5..20);
3786 let base_text = gen_text(rng, line_count);
3787 let initial_buffer_text = gen_edits_from(rng, &base_text);
3788
3789 let mut buffer = Buffer::new(
3790 ReplicaId::LOCAL,
3791 BufferId::new(1).unwrap(),
3792 initial_buffer_text.clone(),
3793 );
3794
3795 let diff = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3796
3797 let edit_count = rng.random_range(1..=5);
3798 for _ in 0..edit_count {
3799 let buffer_text = buffer.text();
3800 if buffer_text.is_empty() {
3801 buffer.edit([(0..0, gen_line(rng))]);
3802 } else {
3803 let lines: Vec<&str> = buffer_text.lines().collect();
3804 let start_row = rng.random_range(0..lines.len());
3805 let end_row = rng.random_range(start_row..=lines.len().min(start_row + 3));
3806
3807 let start_col = if start_row < lines.len() {
3808 rng.random_range(0..=lines[start_row].len())
3809 } else {
3810 0
3811 };
3812 let end_col = if end_row < lines.len() {
3813 rng.random_range(0..=lines[end_row].len())
3814 } else {
3815 0
3816 };
3817
3818 let start_offset = buffer
3819 .point_to_offset(Point::new(start_row as u32, start_col as u32))
3820 .min(buffer.len());
3821 let end_offset = buffer
3822 .point_to_offset(Point::new(end_row as u32, end_col as u32))
3823 .min(buffer.len());
3824
3825 let (start, end) = if start_offset <= end_offset {
3826 (start_offset, end_offset)
3827 } else {
3828 (end_offset, start_offset)
3829 };
3830
3831 let new_text = if rng.random_bool(0.3) {
3832 String::new()
3833 } else {
3834 let line_count = rng.random_range(0..=2);
3835 gen_text(rng, line_count)
3836 };
3837
3838 buffer.edit([(start..end, new_text)]);
3839 }
3840 }
3841
3842 let buffer_snapshot = buffer.snapshot();
3843
3844 let buffer_text = buffer_snapshot.text();
3845 let buffer_lines: Vec<&str> = buffer_text.lines().collect();
3846 let base_lines: Vec<&str> = base_text.lines().collect();
3847
3848 let test_count = 10;
3849 for _ in 0..test_count {
3850 let range = random_range_in_text(rng, &buffer_lines);
3851 let points = points_in_range(&range, &buffer_lines);
3852
3853 let optimized_patch = diff.patch_for_buffer_range(range.clone(), &buffer_snapshot);
3854 let naive_patch = diff.patch_for_buffer_range_naive(&buffer_snapshot);
3855
3856 for point in points {
3857 let optimized_edit = optimized_patch.edit_for_old_position(point);
3858 let naive_edit = naive_patch.edit_for_old_position(point);
3859
3860 assert_eq!(
3861 optimized_edit,
3862 naive_edit,
3863 "patch_for_buffer_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
3864 point,
3865 range,
3866 base_text,
3867 initial_buffer_text,
3868 buffer_snapshot.text()
3869 );
3870 }
3871 }
3872
3873 for _ in 0..test_count {
3874 let range = random_range_in_text(rng, &base_lines);
3875 let points = points_in_range(&range, &base_lines);
3876
3877 let optimized_patch = diff.patch_for_base_text_range(range.clone(), &buffer_snapshot);
3878 let naive_patch = diff.patch_for_base_text_range_naive(&buffer_snapshot);
3879
3880 for point in points {
3881 let optimized_edit = optimized_patch.edit_for_old_position(point);
3882 let naive_edit = naive_patch.edit_for_old_position(point);
3883
3884 assert_eq!(
3885 optimized_edit,
3886 naive_edit,
3887 "patch_for_base_text_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
3888 point,
3889 range,
3890 base_text,
3891 initial_buffer_text,
3892 buffer_snapshot.text()
3893 );
3894 }
3895 }
3896 }
3897}