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 pub 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 mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
918
919 let anchor_iter = iter::from_fn(move || {
920 cursor.next();
921 cursor.item()
922 })
923 .flat_map(move |hunk| {
924 [
925 (
926 &hunk.buffer_range.start,
927 (
928 hunk.buffer_range.start,
929 hunk.diff_base_byte_range.start,
930 hunk,
931 ),
932 ),
933 (
934 &hunk.buffer_range.end,
935 (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
936 ),
937 ]
938 });
939
940 let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
941 pending_hunks_cursor.next();
942
943 let mut secondary_cursor = None;
944 if let Some(secondary) = secondary.as_ref() {
945 let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
946 cursor.next();
947 secondary_cursor = Some(cursor);
948 }
949
950 let max_point = buffer.max_point();
951 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
952 iter::from_fn(move || {
953 loop {
954 let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
955 let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
956
957 let base_word_diffs = hunk.base_word_diffs.clone();
958 let buffer_word_diffs = hunk.buffer_word_diffs.clone();
959
960 if !start_anchor.is_valid(buffer) {
961 continue;
962 }
963
964 if end_point.column > 0 && end_point < max_point {
965 end_point.row += 1;
966 end_point.column = 0;
967 end_anchor = buffer.anchor_before(end_point);
968 }
969
970 let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
971
972 let mut has_pending = false;
973 if start_anchor
974 .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
975 .is_gt()
976 {
977 pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
978 }
979
980 if let Some(pending_hunk) = pending_hunks_cursor.item() {
981 let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
982 if pending_range.end.column > 0 {
983 pending_range.end.row += 1;
984 pending_range.end.column = 0;
985 }
986
987 if pending_range == (start_point..end_point)
988 && !buffer.has_edits_since_in_range(
989 &pending_hunk.buffer_version,
990 start_anchor..end_anchor,
991 )
992 {
993 has_pending = true;
994 secondary_status = pending_hunk.new_status;
995 }
996 }
997
998 if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
999 if start_anchor
1000 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
1001 .is_gt()
1002 {
1003 secondary_cursor.seek_forward(&start_anchor, Bias::Left);
1004 }
1005
1006 if let Some(secondary_hunk) = secondary_cursor.item() {
1007 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
1008 if secondary_range.end.column > 0 {
1009 secondary_range.end.row += 1;
1010 secondary_range.end.column = 0;
1011 }
1012 if secondary_range.is_empty()
1013 && secondary_hunk.diff_base_byte_range.is_empty()
1014 {
1015 // ignore
1016 } else if secondary_range == (start_point..end_point) {
1017 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1018 } else if secondary_range.start <= end_point {
1019 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
1020 }
1021 }
1022 }
1023
1024 return Some(DiffHunk {
1025 range: start_point..end_point,
1026 diff_base_byte_range: start_base..end_base,
1027 buffer_range: start_anchor..end_anchor,
1028 base_word_diffs,
1029 buffer_word_diffs,
1030 secondary_status,
1031 });
1032 }
1033 })
1034 }
1035
1036 fn hunks_intersecting_range_rev_impl<'a>(
1037 &'a self,
1038 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
1039 buffer: &'a text::BufferSnapshot,
1040 ) -> impl 'a + Iterator<Item = DiffHunk> {
1041 let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
1042
1043 iter::from_fn(move || {
1044 cursor.prev();
1045
1046 let hunk = cursor.item()?;
1047 let range = hunk.buffer_range.to_point(buffer);
1048
1049 Some(DiffHunk {
1050 range,
1051 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1052 buffer_range: hunk.buffer_range.clone(),
1053 // The secondary status is not used by callers of this method.
1054 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
1055 base_word_diffs: hunk.base_word_diffs.clone(),
1056 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
1057 })
1058 })
1059 }
1060}
1061
1062fn build_diff_options(
1063 file: Option<&Arc<dyn File>>,
1064 language: Option<LanguageName>,
1065 language_scope: Option<language::LanguageScope>,
1066 cx: &App,
1067) -> Option<DiffOptions> {
1068 #[cfg(any(test, feature = "test-support"))]
1069 {
1070 if !cx.has_global::<settings::SettingsStore>() {
1071 return Some(DiffOptions {
1072 language_scope,
1073 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1074 ..Default::default()
1075 });
1076 }
1077 }
1078
1079 language_settings(language, file, cx)
1080 .word_diff_enabled
1081 .then_some(DiffOptions {
1082 language_scope,
1083 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
1084 ..Default::default()
1085 })
1086}
1087
1088fn compute_hunks(
1089 diff_base: Option<(Arc<str>, Rope)>,
1090 buffer: &text::BufferSnapshot,
1091 diff_options: Option<DiffOptions>,
1092) -> SumTree<InternalDiffHunk> {
1093 let mut tree = SumTree::new(buffer);
1094
1095 if let Some((diff_base, diff_base_rope)) = diff_base {
1096 let buffer_text = buffer.as_rope().to_string();
1097
1098 let mut options = GitOptions::default();
1099 options.context_lines(0);
1100 let patch = GitPatch::from_buffers(
1101 diff_base.as_bytes(),
1102 None,
1103 buffer_text.as_bytes(),
1104 None,
1105 Some(&mut options),
1106 )
1107 .log_err();
1108
1109 // A common case in Zed is that the empty buffer is represented as just a newline,
1110 // but if we just compute a naive diff you get a "preserved" line in the middle,
1111 // which is a bit odd.
1112 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
1113 tree.push(
1114 InternalDiffHunk {
1115 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
1116 diff_base_byte_range: 0..diff_base.len() - 1,
1117 base_word_diffs: Vec::default(),
1118 buffer_word_diffs: Vec::default(),
1119 },
1120 buffer,
1121 );
1122 return tree;
1123 }
1124
1125 if let Some(patch) = patch {
1126 let mut divergence = 0;
1127 for hunk_index in 0..patch.num_hunks() {
1128 let hunk = process_patch_hunk(
1129 &patch,
1130 hunk_index,
1131 &diff_base_rope,
1132 buffer,
1133 &mut divergence,
1134 diff_options.as_ref(),
1135 );
1136 tree.push(hunk, buffer);
1137 }
1138 }
1139 } else {
1140 tree.push(
1141 InternalDiffHunk {
1142 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
1143 diff_base_byte_range: 0..0,
1144 base_word_diffs: Vec::default(),
1145 buffer_word_diffs: Vec::default(),
1146 },
1147 buffer,
1148 );
1149 }
1150
1151 tree
1152}
1153
1154fn compare_hunks(
1155 new_hunks: &SumTree<InternalDiffHunk>,
1156 old_hunks: &SumTree<InternalDiffHunk>,
1157 old_snapshot: &text::BufferSnapshot,
1158 new_snapshot: &text::BufferSnapshot,
1159 old_base_text: &text::BufferSnapshot,
1160 new_base_text: &text::BufferSnapshot,
1161) -> DiffChanged {
1162 let mut new_cursor = new_hunks.cursor::<()>(new_snapshot);
1163 let mut old_cursor = old_hunks.cursor::<()>(new_snapshot);
1164 old_cursor.next();
1165 new_cursor.next();
1166 let mut start = None;
1167 let mut end = None;
1168 let mut base_text_start: Option<Anchor> = None;
1169 let mut base_text_end: Option<Anchor> = None;
1170
1171 let mut last_unchanged_new_hunk_end: Option<text::Anchor> = None;
1172 let mut has_changes = false;
1173 let mut extended_end_candidate: Option<text::Anchor> = None;
1174
1175 loop {
1176 match (new_cursor.item(), old_cursor.item()) {
1177 (Some(new_hunk), Some(old_hunk)) => {
1178 match new_hunk
1179 .buffer_range
1180 .start
1181 .cmp(&old_hunk.buffer_range.start, new_snapshot)
1182 {
1183 Ordering::Less => {
1184 has_changes = true;
1185 extended_end_candidate = None;
1186 start.get_or_insert(new_hunk.buffer_range.start);
1187 base_text_start.get_or_insert(
1188 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1189 );
1190 end.replace(new_hunk.buffer_range.end);
1191 let new_diff_range_end =
1192 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1193 if base_text_end.is_none_or(|base_text_end| {
1194 new_diff_range_end
1195 .cmp(&base_text_end, &new_base_text)
1196 .is_gt()
1197 }) {
1198 base_text_end = Some(new_diff_range_end)
1199 }
1200 new_cursor.next();
1201 }
1202 Ordering::Equal => {
1203 if new_hunk != old_hunk {
1204 has_changes = true;
1205 extended_end_candidate = None;
1206 start.get_or_insert(new_hunk.buffer_range.start);
1207 base_text_start.get_or_insert(
1208 new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1209 );
1210 if old_hunk
1211 .buffer_range
1212 .end
1213 .cmp(&new_hunk.buffer_range.end, new_snapshot)
1214 .is_ge()
1215 {
1216 end.replace(old_hunk.buffer_range.end);
1217 } else {
1218 end.replace(new_hunk.buffer_range.end);
1219 }
1220
1221 let old_hunk_diff_base_range_end =
1222 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1223 let new_hunk_diff_base_range_end =
1224 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1225
1226 base_text_end.replace(
1227 *old_hunk_diff_base_range_end
1228 .max(&new_hunk_diff_base_range_end, new_base_text),
1229 );
1230 } else {
1231 if !has_changes {
1232 last_unchanged_new_hunk_end = Some(new_hunk.buffer_range.end);
1233 } else if extended_end_candidate.is_none() {
1234 extended_end_candidate = Some(new_hunk.buffer_range.start);
1235 }
1236 }
1237
1238 new_cursor.next();
1239 old_cursor.next();
1240 }
1241 Ordering::Greater => {
1242 has_changes = true;
1243 extended_end_candidate = None;
1244 start.get_or_insert(old_hunk.buffer_range.start);
1245 base_text_start.get_or_insert(
1246 old_base_text.anchor_after(old_hunk.diff_base_byte_range.start),
1247 );
1248 end.replace(old_hunk.buffer_range.end);
1249 let old_diff_range_end =
1250 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1251 if base_text_end.is_none_or(|base_text_end| {
1252 old_diff_range_end
1253 .cmp(&base_text_end, new_base_text)
1254 .is_gt()
1255 }) {
1256 base_text_end = Some(old_diff_range_end)
1257 }
1258 old_cursor.next();
1259 }
1260 }
1261 }
1262 (Some(new_hunk), None) => {
1263 has_changes = true;
1264 extended_end_candidate = None;
1265 start.get_or_insert(new_hunk.buffer_range.start);
1266 base_text_start
1267 .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start));
1268 if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le())
1269 {
1270 end.replace(new_hunk.buffer_range.end);
1271 }
1272 let new_base_text_end =
1273 new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1274 if base_text_end.is_none_or(|base_text_end| {
1275 new_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1276 }) {
1277 base_text_end = Some(new_base_text_end)
1278 }
1279 new_cursor.next();
1280 }
1281 (None, Some(old_hunk)) => {
1282 has_changes = true;
1283 extended_end_candidate = None;
1284 start.get_or_insert(old_hunk.buffer_range.start);
1285 base_text_start
1286 .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start));
1287 if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le())
1288 {
1289 end.replace(old_hunk.buffer_range.end);
1290 }
1291 let old_base_text_end =
1292 old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1293 if base_text_end.is_none_or(|base_text_end| {
1294 old_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1295 }) {
1296 base_text_end = Some(old_base_text_end);
1297 }
1298 old_cursor.next();
1299 }
1300 (None, None) => break,
1301 }
1302 }
1303
1304 let changed_range = start.zip(end).map(|(start, end)| start..end);
1305 let base_text_changed_range = base_text_start
1306 .zip(base_text_end)
1307 .map(|(start, end)| (start..end).to_offset(new_base_text));
1308
1309 let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() {
1310 let extended_start = *last_unchanged_new_hunk_end
1311 .unwrap_or(text::Anchor::min_for_buffer(new_snapshot.remote_id()))
1312 .min(&changed_range.start, new_snapshot);
1313 let extended_start = new_snapshot
1314 .anchored_edits_since_in_range::<usize>(
1315 &old_snapshot.version(),
1316 extended_start..changed_range.start,
1317 )
1318 .map(|(_, anchors)| anchors.start)
1319 .min_by(|a, b| a.cmp(b, new_snapshot))
1320 .unwrap_or(changed_range.start);
1321
1322 let extended_end = *extended_end_candidate
1323 .unwrap_or(text::Anchor::max_for_buffer(new_snapshot.remote_id()))
1324 .max(&changed_range.end, new_snapshot);
1325 let extended_end = new_snapshot
1326 .anchored_edits_since_in_range::<usize>(
1327 &old_snapshot.version(),
1328 changed_range.end..extended_end,
1329 )
1330 .map(|(_, anchors)| anchors.end)
1331 .max_by(|a, b| a.cmp(b, new_snapshot))
1332 .unwrap_or(changed_range.end);
1333
1334 Some(extended_start..extended_end)
1335 } else {
1336 None
1337 };
1338
1339 DiffChanged {
1340 changed_range,
1341 base_text_changed_range,
1342 extended_range,
1343 }
1344}
1345
1346fn process_patch_hunk(
1347 patch: &GitPatch<'_>,
1348 hunk_index: usize,
1349 diff_base: &Rope,
1350 buffer: &text::BufferSnapshot,
1351 buffer_row_divergence: &mut i64,
1352 diff_options: Option<&DiffOptions>,
1353) -> InternalDiffHunk {
1354 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
1355 assert!(line_item_count > 0);
1356
1357 let mut first_deletion_buffer_row: Option<u32> = None;
1358 let mut buffer_row_range: Option<Range<u32>> = None;
1359 let mut diff_base_byte_range: Option<Range<usize>> = None;
1360 let mut first_addition_old_row: Option<u32> = None;
1361
1362 for line_index in 0..line_item_count {
1363 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
1364 let kind = line.origin_value();
1365 let content_offset = line.content_offset() as isize;
1366 let content_len = line.content().len() as isize;
1367 match kind {
1368 GitDiffLineType::Addition => {
1369 if first_addition_old_row.is_none() {
1370 first_addition_old_row = Some(
1371 (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
1372 );
1373 }
1374 *buffer_row_divergence += 1;
1375 let row = line.new_lineno().unwrap().saturating_sub(1);
1376
1377 match &mut buffer_row_range {
1378 Some(Range { end, .. }) => *end = row + 1,
1379 None => buffer_row_range = Some(row..row + 1),
1380 }
1381 }
1382 GitDiffLineType::Deletion => {
1383 let end = content_offset + content_len;
1384
1385 match &mut diff_base_byte_range {
1386 Some(head_byte_range) => head_byte_range.end = end as usize,
1387 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
1388 }
1389
1390 if first_deletion_buffer_row.is_none() {
1391 let old_row = line.old_lineno().unwrap().saturating_sub(1);
1392 let row = old_row as i64 + *buffer_row_divergence;
1393 first_deletion_buffer_row = Some(row as u32);
1394 }
1395
1396 *buffer_row_divergence -= 1;
1397 }
1398 _ => {}
1399 }
1400 }
1401
1402 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
1403 // Pure deletion hunk without addition.
1404 let row = first_deletion_buffer_row.unwrap();
1405 row..row
1406 });
1407 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
1408 // Pure addition hunk without deletion.
1409 let row = first_addition_old_row.unwrap();
1410 let offset = diff_base.point_to_offset(Point::new(row, 0));
1411 offset..offset
1412 });
1413
1414 let start = Point::new(buffer_row_range.start, 0);
1415 let end = Point::new(buffer_row_range.end, 0);
1416 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
1417
1418 let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
1419
1420 let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
1421 && !buffer_row_range.is_empty()
1422 && base_line_count == buffer_row_range.len()
1423 && diff_options.max_word_diff_line_count >= base_line_count
1424 {
1425 let base_text: String = diff_base
1426 .chunks_in_range(diff_base_byte_range.clone())
1427 .collect();
1428
1429 let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
1430
1431 let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
1432 &base_text,
1433 &buffer_text,
1434 DiffOptions {
1435 language_scope: diff_options.language_scope.clone(),
1436 ..*diff_options
1437 },
1438 );
1439
1440 let buffer_start_offset = buffer_range.start.to_offset(buffer);
1441 let buffer_word_diffs = buffer_word_diffs_relative
1442 .into_iter()
1443 .map(|range| {
1444 let start = buffer.anchor_after(buffer_start_offset + range.start);
1445 let end = buffer.anchor_after(buffer_start_offset + range.end);
1446 start..end
1447 })
1448 .collect();
1449
1450 (base_word_diffs, buffer_word_diffs)
1451 } else {
1452 (Vec::default(), Vec::default())
1453 };
1454
1455 InternalDiffHunk {
1456 buffer_range,
1457 diff_base_byte_range,
1458 base_word_diffs,
1459 buffer_word_diffs,
1460 }
1461}
1462
1463impl std::fmt::Debug for BufferDiff {
1464 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1465 f.debug_struct("BufferChangeSet")
1466 .field("buffer_id", &self.buffer_id)
1467 .finish()
1468 }
1469}
1470
1471#[derive(Clone, Debug, Default)]
1472pub struct DiffChanged {
1473 pub changed_range: Option<Range<text::Anchor>>,
1474 pub base_text_changed_range: Option<Range<usize>>,
1475 pub extended_range: Option<Range<text::Anchor>>,
1476}
1477
1478#[derive(Clone, Debug)]
1479pub enum BufferDiffEvent {
1480 DiffChanged(DiffChanged),
1481 LanguageChanged,
1482 HunksStagedOrUnstaged(Option<Rope>),
1483}
1484
1485impl EventEmitter<BufferDiffEvent> for BufferDiff {}
1486
1487impl BufferDiff {
1488 pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
1489 let base_text = cx.new(|cx| {
1490 let mut buffer = language::Buffer::local("", cx);
1491 buffer.set_capability(Capability::ReadOnly, cx);
1492 buffer
1493 });
1494
1495 BufferDiff {
1496 buffer_id: buffer.remote_id(),
1497 inner: BufferDiffInner {
1498 base_text,
1499 hunks: SumTree::new(buffer),
1500 pending_hunks: SumTree::new(buffer),
1501 base_text_exists: false,
1502 buffer_snapshot: buffer.clone(),
1503 },
1504 secondary_diff: None,
1505 }
1506 }
1507
1508 pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context<Self>) -> Self {
1509 let base_text = buffer.text();
1510 let base_text = cx.new(|cx| {
1511 let mut buffer = language::Buffer::local(base_text, cx);
1512 buffer.set_capability(Capability::ReadOnly, cx);
1513 buffer
1514 });
1515
1516 BufferDiff {
1517 buffer_id: buffer.remote_id(),
1518 inner: BufferDiffInner {
1519 base_text,
1520 hunks: SumTree::new(buffer),
1521 pending_hunks: SumTree::new(buffer),
1522 base_text_exists: true,
1523 buffer_snapshot: buffer.clone(),
1524 },
1525 secondary_diff: None,
1526 }
1527 }
1528
1529 #[cfg(any(test, feature = "test-support"))]
1530 pub fn new_with_base_text(
1531 base_text: &str,
1532 buffer: &text::BufferSnapshot,
1533 cx: &mut Context<Self>,
1534 ) -> Self {
1535 let mut this = BufferDiff::new(&buffer, cx);
1536 let mut base_text = base_text.to_owned();
1537 text::LineEnding::normalize(&mut base_text);
1538 let inner = cx.foreground_executor().block_on(this.update_diff(
1539 buffer.clone(),
1540 Some(Arc::from(base_text)),
1541 Some(false),
1542 None,
1543 cx,
1544 ));
1545 this.set_snapshot(inner, &buffer, cx).detach();
1546 this
1547 }
1548
1549 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
1550 self.secondary_diff = Some(diff);
1551 }
1552
1553 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
1554 self.secondary_diff.clone()
1555 }
1556
1557 pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
1558 if self.secondary_diff.is_some() {
1559 self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
1560 buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
1561 diff_base_byte_range: 0..0,
1562 });
1563 let changed_range = Some(Anchor::min_max_range_for_buffer(self.buffer_id));
1564 let base_text_range = Some(0..self.base_text(cx).len());
1565 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1566 changed_range: changed_range.clone(),
1567 base_text_changed_range: base_text_range,
1568 extended_range: changed_range,
1569 }));
1570 }
1571 }
1572
1573 pub fn stage_or_unstage_hunks(
1574 &mut self,
1575 stage: bool,
1576 hunks: &[DiffHunk],
1577 buffer: &text::BufferSnapshot,
1578 file_exists: bool,
1579 cx: &mut Context<Self>,
1580 ) -> Option<Rope> {
1581 let new_index_text = self
1582 .secondary_diff
1583 .as_ref()?
1584 .update(cx, |secondary_diff, cx| {
1585 self.inner.stage_or_unstage_hunks_impl(
1586 &secondary_diff.inner,
1587 stage,
1588 hunks,
1589 buffer,
1590 file_exists,
1591 cx,
1592 )
1593 });
1594
1595 cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
1596 new_index_text.clone(),
1597 ));
1598 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1599 let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1600 let base_text_changed_range =
1601 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1602 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1603 changed_range: changed_range.clone(),
1604 base_text_changed_range,
1605 extended_range: changed_range,
1606 }));
1607 }
1608 new_index_text
1609 }
1610
1611 pub fn stage_or_unstage_all_hunks(
1612 &mut self,
1613 stage: bool,
1614 buffer: &text::BufferSnapshot,
1615 file_exists: bool,
1616 cx: &mut Context<Self>,
1617 ) {
1618 let hunks = self
1619 .snapshot(cx)
1620 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
1621 .collect::<Vec<_>>();
1622 let Some(secondary) = self.secondary_diff.clone() else {
1623 return;
1624 };
1625 let secondary = secondary.read(cx).inner.clone();
1626 self.inner
1627 .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx);
1628 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1629 let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1630 let base_text_changed_range =
1631 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1632 cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1633 changed_range: changed_range.clone(),
1634 base_text_changed_range,
1635 extended_range: changed_range,
1636 }));
1637 }
1638 }
1639
1640 pub fn update_diff(
1641 &self,
1642 buffer: text::BufferSnapshot,
1643 base_text: Option<Arc<str>>,
1644 base_text_change: Option<bool>,
1645 language: Option<Arc<Language>>,
1646 cx: &App,
1647 ) -> Task<BufferDiffUpdate> {
1648 let prev_base_text = self.base_text(cx).as_rope().clone();
1649 let base_text_changed = base_text_change.is_some();
1650 let compute_base_text_edits = base_text_change == Some(true);
1651 let diff_options = build_diff_options(
1652 None,
1653 language.as_ref().map(|l| l.name()),
1654 language.as_ref().map(|l| l.default_scope()),
1655 cx,
1656 );
1657 let buffer_snapshot = buffer.clone();
1658
1659 let base_text_diff_task = if base_text_changed && compute_base_text_edits {
1660 base_text
1661 .as_ref()
1662 .map(|new_text| self.inner.base_text.read(cx).diff(new_text.clone(), cx))
1663 } else {
1664 None
1665 };
1666
1667 let hunk_task = cx.background_executor().spawn({
1668 let buffer_snapshot = buffer_snapshot.clone();
1669 async move {
1670 let base_text_rope = if let Some(base_text) = &base_text {
1671 if base_text_changed {
1672 Rope::from(base_text.as_ref())
1673 } else {
1674 prev_base_text
1675 }
1676 } else {
1677 Rope::new()
1678 };
1679 let base_text_exists = base_text.is_some();
1680 let hunks = compute_hunks(
1681 base_text
1682 .clone()
1683 .map(|base_text| (base_text, base_text_rope.clone())),
1684 &buffer,
1685 diff_options,
1686 );
1687 let base_text = base_text.unwrap_or_default();
1688 BufferDiffInner {
1689 base_text,
1690 hunks,
1691 base_text_exists,
1692 pending_hunks: SumTree::new(&buffer),
1693 buffer_snapshot,
1694 }
1695 }
1696 });
1697
1698 cx.background_executor().spawn(async move {
1699 let (inner, base_text_edits) = match base_text_diff_task {
1700 Some(diff_task) => {
1701 let (inner, diff) = futures::join!(hunk_task, diff_task);
1702 (inner, Some(diff))
1703 }
1704 None => (hunk_task.await, None),
1705 };
1706
1707 BufferDiffUpdate {
1708 inner,
1709 buffer_snapshot,
1710 base_text_edits,
1711 base_text_changed,
1712 }
1713 })
1714 }
1715
1716 #[ztracing::instrument(skip_all)]
1717 pub fn language_changed(
1718 &mut self,
1719 language: Option<Arc<Language>>,
1720 language_registry: Option<Arc<LanguageRegistry>>,
1721 cx: &mut Context<Self>,
1722 ) {
1723 let fut = self.inner.base_text.update(cx, |base_text, cx| {
1724 if let Some(language_registry) = language_registry {
1725 base_text.set_language_registry(language_registry);
1726 }
1727 base_text.set_language(language, cx);
1728 base_text.parsing_idle()
1729 });
1730 cx.spawn(async move |this, cx| {
1731 fut.await;
1732 this.update(cx, |_, cx| {
1733 cx.emit(BufferDiffEvent::LanguageChanged);
1734 })
1735 .ok();
1736 })
1737 .detach();
1738 }
1739
1740 fn set_snapshot_with_secondary_inner(
1741 &mut self,
1742 update: BufferDiffUpdate,
1743 buffer: &text::BufferSnapshot,
1744 secondary_diff_change: Option<Range<Anchor>>,
1745 clear_pending_hunks: bool,
1746 cx: &mut Context<Self>,
1747 ) -> impl Future<Output = DiffChanged> + use<> {
1748 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1749
1750 let old_snapshot = self.snapshot(cx);
1751 let new_state = update.inner;
1752 let base_text_changed = update.base_text_changed;
1753
1754 let state = &mut self.inner;
1755 state.base_text_exists = new_state.base_text_exists;
1756 let should_compare_hunks = update.base_text_edits.is_some() || !base_text_changed;
1757 let parsing_idle = if let Some(diff) = update.base_text_edits {
1758 state.base_text.update(cx, |base_text, cx| {
1759 base_text.set_capability(Capability::ReadWrite, cx);
1760 base_text.apply_diff(diff, cx);
1761 base_text.set_capability(Capability::ReadOnly, cx);
1762 Some(base_text.parsing_idle())
1763 })
1764 } else if update.base_text_changed {
1765 state.base_text.update(cx, |base_text, cx| {
1766 base_text.set_capability(Capability::ReadWrite, cx);
1767 base_text.set_text(new_state.base_text.clone(), cx);
1768 base_text.set_capability(Capability::ReadOnly, cx);
1769 Some(base_text.parsing_idle())
1770 })
1771 } else {
1772 None
1773 };
1774
1775 let old_buffer_snapshot = &old_snapshot.inner.buffer_snapshot;
1776 let old_base_snapshot = &old_snapshot.inner.base_text;
1777 let new_base_snapshot = state.base_text.read(cx).snapshot();
1778 let DiffChanged {
1779 mut changed_range,
1780 mut base_text_changed_range,
1781 mut extended_range,
1782 } = match (state.base_text_exists, new_state.base_text_exists) {
1783 (false, false) => DiffChanged::default(),
1784 (true, true) if should_compare_hunks => compare_hunks(
1785 &new_state.hunks,
1786 &old_snapshot.inner.hunks,
1787 old_buffer_snapshot,
1788 buffer,
1789 old_base_snapshot,
1790 &new_base_snapshot,
1791 ),
1792 _ => {
1793 let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id);
1794 let full_base_range = 0..new_state.base_text.len();
1795 DiffChanged {
1796 changed_range: Some(full_range.clone()),
1797 base_text_changed_range: Some(full_base_range),
1798 extended_range: Some(full_range),
1799 }
1800 }
1801 };
1802 state.hunks = new_state.hunks;
1803 state.buffer_snapshot = update.buffer_snapshot;
1804
1805 if base_text_changed || clear_pending_hunks {
1806 if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1807 {
1808 let pending_range = first.buffer_range.start..last.buffer_range.end;
1809 if let Some(range) = &mut changed_range {
1810 range.start = *range.start.min(&pending_range.start, buffer);
1811 range.end = *range.end.max(&pending_range.end, buffer);
1812 } else {
1813 changed_range = Some(pending_range.clone());
1814 }
1815
1816 if let Some(base_text_range) = base_text_changed_range.as_mut() {
1817 base_text_range.start =
1818 base_text_range.start.min(first.diff_base_byte_range.start);
1819 base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end);
1820 } else {
1821 base_text_changed_range =
1822 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1823 }
1824
1825 if let Some(ext) = &mut extended_range {
1826 ext.start = *ext.start.min(&pending_range.start, buffer);
1827 ext.end = *ext.end.max(&pending_range.end, buffer);
1828 } else {
1829 extended_range = Some(pending_range);
1830 }
1831 }
1832 state.pending_hunks = SumTree::new(buffer);
1833 }
1834
1835 if let Some(secondary_changed_range) = secondary_diff_change
1836 && let (Some(secondary_hunk_range), Some(secondary_base_range)) =
1837 old_snapshot.range_to_hunk_range(secondary_changed_range, buffer)
1838 {
1839 if let Some(range) = &mut changed_range {
1840 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1841 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1842 } else {
1843 changed_range = Some(secondary_hunk_range.clone());
1844 }
1845
1846 if let Some(base_text_range) = base_text_changed_range.as_mut() {
1847 base_text_range.start = secondary_base_range.start.min(base_text_range.start);
1848 base_text_range.end = secondary_base_range.end.max(base_text_range.end);
1849 } else {
1850 base_text_changed_range = Some(secondary_base_range);
1851 }
1852
1853 if let Some(ext) = &mut extended_range {
1854 ext.start = *ext.start.min(&secondary_hunk_range.start, buffer);
1855 ext.end = *ext.end.max(&secondary_hunk_range.end, buffer);
1856 } else {
1857 extended_range = Some(secondary_hunk_range);
1858 }
1859 }
1860
1861 async move {
1862 if let Some(parsing_idle) = parsing_idle {
1863 parsing_idle.await;
1864 }
1865 DiffChanged {
1866 changed_range,
1867 base_text_changed_range,
1868 extended_range,
1869 }
1870 }
1871 }
1872
1873 pub fn set_snapshot(
1874 &mut self,
1875 new_state: BufferDiffUpdate,
1876 buffer: &text::BufferSnapshot,
1877 cx: &mut Context<Self>,
1878 ) -> Task<Option<Range<Anchor>>> {
1879 self.set_snapshot_with_secondary(new_state, buffer, None, false, cx)
1880 }
1881
1882 pub fn set_snapshot_with_secondary(
1883 &mut self,
1884 update: BufferDiffUpdate,
1885 buffer: &text::BufferSnapshot,
1886 secondary_diff_change: Option<Range<Anchor>>,
1887 clear_pending_hunks: bool,
1888 cx: &mut Context<Self>,
1889 ) -> Task<Option<Range<Anchor>>> {
1890 let fut = self.set_snapshot_with_secondary_inner(
1891 update,
1892 buffer,
1893 secondary_diff_change,
1894 clear_pending_hunks,
1895 cx,
1896 );
1897
1898 cx.spawn(async move |this, cx| {
1899 let change = fut.await;
1900 this.update(cx, |_, cx| {
1901 cx.emit(BufferDiffEvent::DiffChanged(change.clone()));
1902 })
1903 .ok();
1904 change.changed_range
1905 })
1906 }
1907
1908 pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
1909 self.inner.base_text.read(cx).snapshot()
1910 }
1911
1912 pub fn base_text_exists(&self) -> bool {
1913 self.inner.base_text_exists
1914 }
1915
1916 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1917 BufferDiffSnapshot {
1918 inner: BufferDiffInner {
1919 hunks: self.inner.hunks.clone(),
1920 pending_hunks: self.inner.pending_hunks.clone(),
1921 base_text: self.inner.base_text.read(cx).snapshot(),
1922 base_text_exists: self.inner.base_text_exists,
1923 buffer_snapshot: self.inner.buffer_snapshot.clone(),
1924 },
1925 secondary_diff: self.secondary_diff.as_ref().map(|diff| {
1926 debug_assert!(diff.read(cx).secondary_diff.is_none());
1927 Arc::new(diff.read(cx).snapshot(cx))
1928 }),
1929 }
1930 }
1931
1932 /// Used in cases where the change set isn't derived from git.
1933 pub fn set_base_text(
1934 &mut self,
1935 base_text: Option<Arc<str>>,
1936 language: Option<Arc<Language>>,
1937 buffer: text::BufferSnapshot,
1938 cx: &mut Context<Self>,
1939 ) -> oneshot::Receiver<()> {
1940 let (tx, rx) = oneshot::channel();
1941 let complete_on_drop = util::defer(|| {
1942 tx.send(()).ok();
1943 });
1944 cx.spawn(async move |this, cx| {
1945 let Some(state) = this
1946 .update(cx, |this, cx| {
1947 this.update_diff(buffer.clone(), base_text, Some(false), language, cx)
1948 })
1949 .log_err()
1950 else {
1951 return;
1952 };
1953 let state = state.await;
1954 if let Some(task) = this
1955 .update(cx, |this, cx| this.set_snapshot(state, &buffer, cx))
1956 .log_err()
1957 {
1958 task.await;
1959 }
1960 drop(complete_on_drop)
1961 })
1962 .detach();
1963 rx
1964 }
1965
1966 pub fn base_text_string(&self, cx: &App) -> Option<String> {
1967 self.inner
1968 .base_text_exists
1969 .then(|| self.inner.base_text.read(cx).text())
1970 }
1971
1972 #[cfg(any(test, feature = "test-support"))]
1973 pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) {
1974 let language = self.base_text(cx).language().cloned();
1975 let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
1976 let fut = self.update_diff(buffer.clone(), base_text, None, language, cx);
1977 let fg_executor = cx.foreground_executor().clone();
1978 let snapshot = fg_executor.block_on(fut);
1979 let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx);
1980 let change = fg_executor.block_on(fut);
1981 cx.emit(BufferDiffEvent::DiffChanged(change));
1982 }
1983
1984 pub fn base_text_buffer(&self) -> Entity<language::Buffer> {
1985 self.inner.base_text.clone()
1986 }
1987}
1988
1989impl DiffHunk {
1990 pub fn is_created_file(&self) -> bool {
1991 self.diff_base_byte_range == (0..0)
1992 && self.buffer_range.start.is_min()
1993 && self.buffer_range.end.is_max()
1994 }
1995
1996 pub fn status(&self) -> DiffHunkStatus {
1997 let kind = if self.buffer_range.start == self.buffer_range.end {
1998 DiffHunkStatusKind::Deleted
1999 } else if self.diff_base_byte_range.is_empty() {
2000 DiffHunkStatusKind::Added
2001 } else {
2002 DiffHunkStatusKind::Modified
2003 };
2004 DiffHunkStatus {
2005 kind,
2006 secondary: self.secondary_status,
2007 }
2008 }
2009}
2010
2011impl DiffHunkStatus {
2012 pub fn has_secondary_hunk(&self) -> bool {
2013 matches!(
2014 self.secondary,
2015 DiffHunkSecondaryStatus::HasSecondaryHunk
2016 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2017 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
2018 )
2019 }
2020
2021 pub fn is_pending(&self) -> bool {
2022 matches!(
2023 self.secondary,
2024 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2025 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2026 )
2027 }
2028
2029 pub fn is_deleted(&self) -> bool {
2030 self.kind == DiffHunkStatusKind::Deleted
2031 }
2032
2033 pub fn is_added(&self) -> bool {
2034 self.kind == DiffHunkStatusKind::Added
2035 }
2036
2037 pub fn is_modified(&self) -> bool {
2038 self.kind == DiffHunkStatusKind::Modified
2039 }
2040
2041 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
2042 Self {
2043 kind: DiffHunkStatusKind::Added,
2044 secondary,
2045 }
2046 }
2047
2048 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
2049 Self {
2050 kind: DiffHunkStatusKind::Modified,
2051 secondary,
2052 }
2053 }
2054
2055 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
2056 Self {
2057 kind: DiffHunkStatusKind::Deleted,
2058 secondary,
2059 }
2060 }
2061
2062 pub fn deleted_none() -> Self {
2063 Self {
2064 kind: DiffHunkStatusKind::Deleted,
2065 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2066 }
2067 }
2068
2069 pub fn added_none() -> Self {
2070 Self {
2071 kind: DiffHunkStatusKind::Added,
2072 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2073 }
2074 }
2075
2076 pub fn modified_none() -> Self {
2077 Self {
2078 kind: DiffHunkStatusKind::Modified,
2079 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
2080 }
2081 }
2082}
2083
2084#[cfg(any(test, feature = "test-support"))]
2085#[track_caller]
2086pub fn assert_hunks<ExpectedText, HunkIter>(
2087 diff_hunks: HunkIter,
2088 buffer: &text::BufferSnapshot,
2089 diff_base: &str,
2090 // Line range, deleted, added, status
2091 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
2092) where
2093 HunkIter: Iterator<Item = DiffHunk>,
2094 ExpectedText: AsRef<str>,
2095{
2096 let actual_hunks = diff_hunks
2097 .map(|hunk| {
2098 (
2099 hunk.range.clone(),
2100 &diff_base[hunk.diff_base_byte_range.clone()],
2101 buffer
2102 .text_for_range(hunk.range.clone())
2103 .collect::<String>(),
2104 hunk.status(),
2105 )
2106 })
2107 .collect::<Vec<_>>();
2108
2109 let expected_hunks: Vec<_> = expected_hunks
2110 .iter()
2111 .map(|(line_range, deleted_text, added_text, status)| {
2112 (
2113 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
2114 deleted_text.as_ref(),
2115 added_text.as_ref().to_string(),
2116 *status,
2117 )
2118 })
2119 .collect();
2120
2121 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
2122}
2123
2124#[cfg(test)]
2125mod tests {
2126 use std::{fmt::Write as _, sync::mpsc};
2127
2128 use super::*;
2129 use gpui::TestAppContext;
2130 use pretty_assertions::{assert_eq, assert_ne};
2131 use rand::{Rng as _, rngs::StdRng};
2132 use text::{Buffer, BufferId, ReplicaId, Rope};
2133 use unindent::Unindent as _;
2134 use util::test::marked_text_ranges;
2135
2136 #[ctor::ctor]
2137 fn init_logger() {
2138 zlog::init_test();
2139 }
2140
2141 #[gpui::test]
2142 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
2143 let diff_base = "
2144 one
2145 two
2146 three
2147 "
2148 .unindent();
2149
2150 let buffer_text = "
2151 one
2152 HELLO
2153 three
2154 "
2155 .unindent();
2156
2157 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2158 let mut diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2159 assert_hunks(
2160 diff.hunks_intersecting_range(
2161 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2162 &buffer,
2163 ),
2164 &buffer,
2165 &diff_base,
2166 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
2167 );
2168
2169 buffer.edit([(0..0, "point five\n")]);
2170 diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2171 assert_hunks(
2172 diff.hunks_intersecting_range(
2173 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2174 &buffer,
2175 ),
2176 &buffer,
2177 &diff_base,
2178 &[
2179 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
2180 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
2181 ],
2182 );
2183
2184 diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2185 assert_hunks::<&str, _>(
2186 diff.hunks_intersecting_range(
2187 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2188 &buffer,
2189 ),
2190 &buffer,
2191 &diff_base,
2192 &[],
2193 );
2194 }
2195
2196 #[gpui::test]
2197 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
2198 let head_text = "
2199 zero
2200 one
2201 two
2202 three
2203 four
2204 five
2205 six
2206 seven
2207 eight
2208 nine
2209 "
2210 .unindent();
2211
2212 let index_text = "
2213 zero
2214 one
2215 TWO
2216 three
2217 FOUR
2218 five
2219 six
2220 seven
2221 eight
2222 NINE
2223 "
2224 .unindent();
2225
2226 let buffer_text = "
2227 zero
2228 one
2229 TWO
2230 three
2231 FOUR
2232 FIVE
2233 six
2234 SEVEN
2235 eight
2236 nine
2237 "
2238 .unindent();
2239
2240 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2241 let unstaged_diff = BufferDiffSnapshot::new_sync(&buffer, index_text, cx);
2242 let mut uncommitted_diff = BufferDiffSnapshot::new_sync(&buffer, head_text.clone(), cx);
2243 uncommitted_diff.secondary_diff = Some(Arc::new(unstaged_diff));
2244
2245 let expected_hunks = vec![
2246 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
2247 (
2248 4..6,
2249 "four\nfive\n",
2250 "FOUR\nFIVE\n",
2251 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
2252 ),
2253 (
2254 7..8,
2255 "seven\n",
2256 "SEVEN\n",
2257 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
2258 ),
2259 ];
2260
2261 assert_hunks(
2262 uncommitted_diff.hunks_intersecting_range(
2263 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2264 &buffer,
2265 ),
2266 &buffer,
2267 &head_text,
2268 &expected_hunks,
2269 );
2270 }
2271
2272 #[gpui::test]
2273 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
2274 let diff_base = "
2275 one
2276 two
2277 three
2278 four
2279 five
2280 six
2281 seven
2282 eight
2283 nine
2284 ten
2285 "
2286 .unindent();
2287
2288 let buffer_text = "
2289 A
2290 one
2291 B
2292 two
2293 C
2294 three
2295 HELLO
2296 four
2297 five
2298 SIXTEEN
2299 seven
2300 eight
2301 WORLD
2302 nine
2303
2304 ten
2305
2306 "
2307 .unindent();
2308
2309 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2310 let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx);
2311 assert_eq!(
2312 diff.hunks_intersecting_range(
2313 Anchor::min_max_range_for_buffer(buffer.remote_id()),
2314 &buffer
2315 )
2316 .count(),
2317 8
2318 );
2319
2320 assert_hunks(
2321 diff.hunks_intersecting_range(
2322 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
2323 &buffer,
2324 ),
2325 &buffer,
2326 &diff_base,
2327 &[
2328 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
2329 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
2330 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
2331 ],
2332 );
2333 }
2334
2335 #[gpui::test]
2336 async fn test_stage_hunk(cx: &mut TestAppContext) {
2337 struct Example {
2338 name: &'static str,
2339 head_text: String,
2340 index_text: String,
2341 buffer_marked_text: String,
2342 final_index_text: String,
2343 }
2344
2345 let table = [
2346 Example {
2347 name: "uncommitted hunk straddles end of unstaged hunk",
2348 head_text: "
2349 one
2350 two
2351 three
2352 four
2353 five
2354 "
2355 .unindent(),
2356 index_text: "
2357 one
2358 TWO_HUNDRED
2359 three
2360 FOUR_HUNDRED
2361 five
2362 "
2363 .unindent(),
2364 buffer_marked_text: "
2365 ZERO
2366 one
2367 two
2368 «THREE_HUNDRED
2369 FOUR_HUNDRED»
2370 five
2371 SIX
2372 "
2373 .unindent(),
2374 final_index_text: "
2375 one
2376 two
2377 THREE_HUNDRED
2378 FOUR_HUNDRED
2379 five
2380 "
2381 .unindent(),
2382 },
2383 Example {
2384 name: "uncommitted hunk straddles start of unstaged hunk",
2385 head_text: "
2386 one
2387 two
2388 three
2389 four
2390 five
2391 "
2392 .unindent(),
2393 index_text: "
2394 one
2395 TWO_HUNDRED
2396 three
2397 FOUR_HUNDRED
2398 five
2399 "
2400 .unindent(),
2401 buffer_marked_text: "
2402 ZERO
2403 one
2404 «TWO_HUNDRED
2405 THREE_HUNDRED»
2406 four
2407 five
2408 SIX
2409 "
2410 .unindent(),
2411 final_index_text: "
2412 one
2413 TWO_HUNDRED
2414 THREE_HUNDRED
2415 four
2416 five
2417 "
2418 .unindent(),
2419 },
2420 Example {
2421 name: "uncommitted hunk strictly contains unstaged hunks",
2422 head_text: "
2423 one
2424 two
2425 three
2426 four
2427 five
2428 six
2429 seven
2430 "
2431 .unindent(),
2432 index_text: "
2433 one
2434 TWO
2435 THREE
2436 FOUR
2437 FIVE
2438 SIX
2439 seven
2440 "
2441 .unindent(),
2442 buffer_marked_text: "
2443 one
2444 TWO
2445 «THREE_HUNDRED
2446 FOUR
2447 FIVE_HUNDRED»
2448 SIX
2449 seven
2450 "
2451 .unindent(),
2452 final_index_text: "
2453 one
2454 TWO
2455 THREE_HUNDRED
2456 FOUR
2457 FIVE_HUNDRED
2458 SIX
2459 seven
2460 "
2461 .unindent(),
2462 },
2463 Example {
2464 name: "uncommitted deletion hunk",
2465 head_text: "
2466 one
2467 two
2468 three
2469 four
2470 five
2471 "
2472 .unindent(),
2473 index_text: "
2474 one
2475 two
2476 three
2477 four
2478 five
2479 "
2480 .unindent(),
2481 buffer_marked_text: "
2482 one
2483 ˇfive
2484 "
2485 .unindent(),
2486 final_index_text: "
2487 one
2488 five
2489 "
2490 .unindent(),
2491 },
2492 Example {
2493 name: "one unstaged hunk that contains two uncommitted hunks",
2494 head_text: "
2495 one
2496 two
2497
2498 three
2499 four
2500 "
2501 .unindent(),
2502 index_text: "
2503 one
2504 two
2505 three
2506 four
2507 "
2508 .unindent(),
2509 buffer_marked_text: "
2510 «one
2511
2512 three // modified
2513 four»
2514 "
2515 .unindent(),
2516 final_index_text: "
2517 one
2518
2519 three // modified
2520 four
2521 "
2522 .unindent(),
2523 },
2524 Example {
2525 name: "one uncommitted hunk that contains two unstaged hunks",
2526 head_text: "
2527 one
2528 two
2529 three
2530 four
2531 five
2532 "
2533 .unindent(),
2534 index_text: "
2535 ZERO
2536 one
2537 TWO
2538 THREE
2539 FOUR
2540 five
2541 "
2542 .unindent(),
2543 buffer_marked_text: "
2544 «one
2545 TWO_HUNDRED
2546 THREE
2547 FOUR_HUNDRED
2548 five»
2549 "
2550 .unindent(),
2551 final_index_text: "
2552 ZERO
2553 one
2554 TWO_HUNDRED
2555 THREE
2556 FOUR_HUNDRED
2557 five
2558 "
2559 .unindent(),
2560 },
2561 ];
2562
2563 for example in table {
2564 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2565 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2566 let hunk_range =
2567 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2568
2569 let unstaged_diff =
2570 cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
2571
2572 let uncommitted_diff = cx.new(|cx| {
2573 let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx);
2574 diff.set_secondary_diff(unstaged_diff);
2575 diff
2576 });
2577
2578 uncommitted_diff.update(cx, |diff, cx| {
2579 let hunks = diff
2580 .snapshot(cx)
2581 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2582 .collect::<Vec<_>>();
2583 for hunk in &hunks {
2584 assert_ne!(
2585 hunk.secondary_status,
2586 DiffHunkSecondaryStatus::NoSecondaryHunk
2587 )
2588 }
2589
2590 let new_index_text = diff
2591 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2592 .unwrap()
2593 .to_string();
2594
2595 let hunks = diff
2596 .snapshot(cx)
2597 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2598 .collect::<Vec<_>>();
2599 for hunk in &hunks {
2600 assert_eq!(
2601 hunk.secondary_status,
2602 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2603 )
2604 }
2605
2606 pretty_assertions::assert_eq!(
2607 new_index_text,
2608 example.final_index_text,
2609 "example: {}",
2610 example.name
2611 );
2612 });
2613 }
2614 }
2615
2616 #[gpui::test]
2617 async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) {
2618 // This test reproduces a crash where staging all hunks would cause an underflow
2619 // when there's one large unstaged hunk containing multiple uncommitted hunks.
2620 let head_text = "
2621 aaa
2622 bbb
2623 ccc
2624 ddd
2625 eee
2626 fff
2627 ggg
2628 hhh
2629 iii
2630 jjj
2631 kkk
2632 lll
2633 "
2634 .unindent();
2635
2636 let index_text = "
2637 aaa
2638 bbb
2639 CCC-index
2640 DDD-index
2641 EEE-index
2642 FFF-index
2643 GGG-index
2644 HHH-index
2645 III-index
2646 JJJ-index
2647 kkk
2648 lll
2649 "
2650 .unindent();
2651
2652 let buffer_text = "
2653 aaa
2654 bbb
2655 ccc-modified
2656 ddd
2657 eee-modified
2658 fff
2659 ggg
2660 hhh-modified
2661 iii
2662 jjj
2663 kkk
2664 lll
2665 "
2666 .unindent();
2667
2668 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2669
2670 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2671 let uncommitted_diff = cx.new(|cx| {
2672 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2673 diff.set_secondary_diff(unstaged_diff);
2674 diff
2675 });
2676
2677 uncommitted_diff.update(cx, |diff, cx| {
2678 diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
2679 });
2680 }
2681
2682 #[gpui::test]
2683 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2684 let head_text = "
2685 one
2686 two
2687 three
2688 "
2689 .unindent();
2690 let index_text = head_text.clone();
2691 let buffer_text = "
2692 one
2693 three
2694 "
2695 .unindent();
2696
2697 let buffer = Buffer::new(
2698 ReplicaId::LOCAL,
2699 BufferId::new(1).unwrap(),
2700 buffer_text.clone(),
2701 );
2702 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2703 let uncommitted_diff = cx.new(|cx| {
2704 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2705 diff.set_secondary_diff(unstaged_diff.clone());
2706 diff
2707 });
2708
2709 uncommitted_diff.update(cx, |diff, cx| {
2710 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2711
2712 let new_index_text = diff
2713 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
2714 .unwrap()
2715 .to_string();
2716 assert_eq!(new_index_text, buffer_text);
2717
2718 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2719 assert_eq!(
2720 hunk.secondary_status,
2721 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2722 );
2723
2724 let index_text = diff
2725 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
2726 .unwrap()
2727 .to_string();
2728 assert_eq!(index_text, head_text);
2729
2730 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2731 // optimistically unstaged (fine, could also be HasSecondaryHunk)
2732 assert_eq!(
2733 hunk.secondary_status,
2734 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2735 );
2736 });
2737 }
2738
2739 #[gpui::test]
2740 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
2741 let base_text = "
2742 zero
2743 one
2744 two
2745 three
2746 four
2747 five
2748 six
2749 seven
2750 eight
2751 nine
2752 "
2753 .unindent();
2754
2755 let buffer_text_1 = "
2756 one
2757 three
2758 four
2759 five
2760 SIX
2761 seven
2762 eight
2763 NINE
2764 "
2765 .unindent();
2766
2767 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
2768
2769 let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2770 let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2771 let DiffChanged {
2772 changed_range,
2773 base_text_changed_range,
2774 extended_range: _,
2775 } = compare_hunks(
2776 &diff_1.inner.hunks,
2777 &empty_diff.inner.hunks,
2778 &buffer,
2779 &buffer,
2780 &diff_1.base_text(),
2781 &diff_1.base_text(),
2782 );
2783 let range = changed_range.unwrap();
2784 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
2785 let base_text_range = base_text_changed_range.unwrap();
2786 assert_eq!(
2787 base_text_range.to_point(diff_1.base_text()),
2788 Point::new(0, 0)..Point::new(10, 0)
2789 );
2790
2791 // Edit does affects the diff because it recalculates word diffs.
2792 buffer.edit_via_marked_text(
2793 &"
2794 one
2795 three
2796 four
2797 five
2798 «SIX.5»
2799 seven
2800 eight
2801 NINE
2802 "
2803 .unindent(),
2804 );
2805 let diff_2 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2806 let DiffChanged {
2807 changed_range,
2808 base_text_changed_range,
2809 extended_range: _,
2810 } = compare_hunks(
2811 &diff_2.inner.hunks,
2812 &diff_1.inner.hunks,
2813 &buffer,
2814 &buffer,
2815 diff_2.base_text(),
2816 diff_2.base_text(),
2817 );
2818 assert_eq!(
2819 changed_range.unwrap().to_point(&buffer),
2820 Point::new(4, 0)..Point::new(5, 0),
2821 );
2822 assert_eq!(
2823 base_text_changed_range
2824 .unwrap()
2825 .to_point(diff_2.base_text()),
2826 Point::new(6, 0)..Point::new(7, 0),
2827 );
2828
2829 // Edit turns a deletion hunk into a modification.
2830 buffer.edit_via_marked_text(
2831 &"
2832 one
2833 «THREE»
2834 four
2835 five
2836 SIX.5
2837 seven
2838 eight
2839 NINE
2840 "
2841 .unindent(),
2842 );
2843 let diff_3 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2844 let DiffChanged {
2845 changed_range,
2846 base_text_changed_range,
2847 extended_range: _,
2848 } = compare_hunks(
2849 &diff_3.inner.hunks,
2850 &diff_2.inner.hunks,
2851 &buffer,
2852 &buffer,
2853 diff_3.base_text(),
2854 diff_3.base_text(),
2855 );
2856 let range = changed_range.unwrap();
2857 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2858 let base_text_range = base_text_changed_range.unwrap();
2859 assert_eq!(
2860 base_text_range.to_point(diff_3.base_text()),
2861 Point::new(2, 0)..Point::new(4, 0)
2862 );
2863
2864 // Edit turns a modification hunk into a deletion.
2865 buffer.edit_via_marked_text(
2866 &"
2867 one
2868 THREE
2869 four
2870 five«»
2871 seven
2872 eight
2873 NINE
2874 "
2875 .unindent(),
2876 );
2877 let diff_4 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2878 let DiffChanged {
2879 changed_range,
2880 base_text_changed_range,
2881 extended_range: _,
2882 } = compare_hunks(
2883 &diff_4.inner.hunks,
2884 &diff_3.inner.hunks,
2885 &buffer,
2886 &buffer,
2887 diff_4.base_text(),
2888 diff_4.base_text(),
2889 );
2890 let range = changed_range.unwrap();
2891 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2892 let base_text_range = base_text_changed_range.unwrap();
2893 assert_eq!(
2894 base_text_range.to_point(diff_4.base_text()),
2895 Point::new(6, 0)..Point::new(7, 0)
2896 );
2897
2898 // Edit introduces a new insertion hunk.
2899 buffer.edit_via_marked_text(
2900 &"
2901 one
2902 THREE
2903 four«
2904 FOUR.5
2905 »five
2906 seven
2907 eight
2908 NINE
2909 "
2910 .unindent(),
2911 );
2912 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2913 let DiffChanged {
2914 changed_range,
2915 base_text_changed_range,
2916 extended_range: _,
2917 } = compare_hunks(
2918 &diff_5.inner.hunks,
2919 &diff_4.inner.hunks,
2920 &buffer,
2921 &buffer,
2922 diff_5.base_text(),
2923 diff_5.base_text(),
2924 );
2925 let range = changed_range.unwrap();
2926 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2927 let base_text_range = base_text_changed_range.unwrap();
2928 assert_eq!(
2929 base_text_range.to_point(diff_5.base_text()),
2930 Point::new(5, 0)..Point::new(5, 0)
2931 );
2932
2933 // Edit removes a hunk.
2934 buffer.edit_via_marked_text(
2935 &"
2936 one
2937 THREE
2938 four
2939 FOUR.5
2940 five
2941 seven
2942 eight
2943 «nine»
2944 "
2945 .unindent(),
2946 );
2947 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2948 let DiffChanged {
2949 changed_range,
2950 base_text_changed_range,
2951 extended_range: _,
2952 } = compare_hunks(
2953 &diff_6.inner.hunks,
2954 &diff_5.inner.hunks,
2955 &buffer,
2956 &buffer,
2957 diff_6.base_text(),
2958 diff_6.base_text(),
2959 );
2960 let range = changed_range.unwrap();
2961 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2962 let base_text_range = base_text_changed_range.unwrap();
2963 assert_eq!(
2964 base_text_range.to_point(diff_6.base_text()),
2965 Point::new(9, 0)..Point::new(10, 0)
2966 );
2967
2968 buffer.edit_via_marked_text(
2969 &"
2970 one
2971 THREE
2972 four«»
2973 five
2974 seven
2975 eight
2976 «NINE»
2977 "
2978 .unindent(),
2979 );
2980
2981 let diff_7 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2982 let DiffChanged {
2983 changed_range,
2984 base_text_changed_range,
2985 extended_range: _,
2986 } = compare_hunks(
2987 &diff_7.inner.hunks,
2988 &diff_6.inner.hunks,
2989 &buffer,
2990 &buffer,
2991 diff_7.base_text(),
2992 diff_7.base_text(),
2993 );
2994 let range = changed_range.unwrap();
2995 assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0));
2996 let base_text_range = base_text_changed_range.unwrap();
2997 assert_eq!(
2998 base_text_range.to_point(diff_7.base_text()),
2999 Point::new(5, 0)..Point::new(10, 0)
3000 );
3001
3002 buffer.edit_via_marked_text(
3003 &"
3004 one
3005 THREE
3006 four
3007 five«»seven
3008 eight
3009 NINE
3010 "
3011 .unindent(),
3012 );
3013
3014 let diff_8 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
3015 let DiffChanged {
3016 changed_range,
3017 base_text_changed_range,
3018 extended_range: _,
3019 } = compare_hunks(
3020 &diff_8.inner.hunks,
3021 &diff_7.inner.hunks,
3022 &buffer,
3023 &buffer,
3024 diff_8.base_text(),
3025 diff_8.base_text(),
3026 );
3027 let range = changed_range.unwrap();
3028 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4));
3029 let base_text_range = base_text_changed_range.unwrap();
3030 assert_eq!(
3031 base_text_range.to_point(diff_8.base_text()),
3032 Point::new(5, 0)..Point::new(8, 0)
3033 );
3034 }
3035
3036 #[gpui::test(iterations = 100)]
3037 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
3038 fn gen_line(rng: &mut StdRng) -> String {
3039 if rng.random_bool(0.2) {
3040 "\n".to_owned()
3041 } else {
3042 let c = rng.random_range('A'..='Z');
3043 format!("{c}{c}{c}\n")
3044 }
3045 }
3046
3047 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
3048 let mut old_lines = {
3049 let mut old_lines = Vec::new();
3050 let old_lines_iter = head.lines();
3051 for line in old_lines_iter {
3052 assert!(!line.ends_with("\n"));
3053 old_lines.push(line.to_owned());
3054 }
3055 if old_lines.last().is_some_and(|line| line.is_empty()) {
3056 old_lines.pop();
3057 }
3058 old_lines.into_iter()
3059 };
3060 let mut result = String::new();
3061 let unchanged_count = rng.random_range(0..=old_lines.len());
3062 result +=
3063 &old_lines
3064 .by_ref()
3065 .take(unchanged_count)
3066 .fold(String::new(), |mut s, line| {
3067 writeln!(&mut s, "{line}").unwrap();
3068 s
3069 });
3070 while old_lines.len() > 0 {
3071 let deleted_count = rng.random_range(0..=old_lines.len());
3072 let _advance = old_lines
3073 .by_ref()
3074 .take(deleted_count)
3075 .map(|line| line.len() + 1)
3076 .sum::<usize>();
3077 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
3078 let added_count = rng.random_range(minimum_added..=5);
3079 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
3080 result += &addition;
3081
3082 if old_lines.len() > 0 {
3083 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
3084 if blank_lines == old_lines.len() {
3085 break;
3086 };
3087 let unchanged_count =
3088 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
3089 result += &old_lines.by_ref().take(unchanged_count).fold(
3090 String::new(),
3091 |mut s, line| {
3092 writeln!(&mut s, "{line}").unwrap();
3093 s
3094 },
3095 );
3096 }
3097 }
3098 result
3099 }
3100
3101 fn uncommitted_diff(
3102 working_copy: &language::BufferSnapshot,
3103 index_text: &Rope,
3104 head_text: String,
3105 cx: &mut TestAppContext,
3106 ) -> Entity<BufferDiff> {
3107 let secondary = cx.new(|cx| {
3108 BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
3109 });
3110 cx.new(|cx| {
3111 let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
3112 diff.secondary_diff = Some(secondary);
3113 diff
3114 })
3115 }
3116
3117 let operations = std::env::var("OPERATIONS")
3118 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
3119 .unwrap_or(10);
3120
3121 let rng = &mut rng;
3122 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
3123 writeln!(&mut s, "{c}{c}{c}").unwrap();
3124 s
3125 });
3126 let working_copy = gen_working_copy(rng, &head_text);
3127 let working_copy = cx.new(|cx| {
3128 language::Buffer::local_normalized(
3129 Rope::from(working_copy.as_str()),
3130 text::LineEnding::default(),
3131 cx,
3132 )
3133 });
3134 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
3135 let mut index_text = if rng.random() {
3136 Rope::from(head_text.as_str())
3137 } else {
3138 working_copy.as_rope().clone()
3139 };
3140
3141 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3142 let mut hunks = diff.update(cx, |diff, cx| {
3143 diff.snapshot(cx)
3144 .hunks_intersecting_range(
3145 Anchor::min_max_range_for_buffer(diff.buffer_id),
3146 &working_copy,
3147 )
3148 .collect::<Vec<_>>()
3149 });
3150 if hunks.is_empty() {
3151 return;
3152 }
3153
3154 for _ in 0..operations {
3155 let i = rng.random_range(0..hunks.len());
3156 let hunk = &mut hunks[i];
3157 let hunk_to_change = hunk.clone();
3158 let stage = match hunk.secondary_status {
3159 DiffHunkSecondaryStatus::HasSecondaryHunk => {
3160 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
3161 true
3162 }
3163 DiffHunkSecondaryStatus::NoSecondaryHunk => {
3164 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
3165 false
3166 }
3167 _ => unreachable!(),
3168 };
3169
3170 index_text = diff.update(cx, |diff, cx| {
3171 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
3172 .unwrap()
3173 });
3174
3175 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3176 let found_hunks = diff.update(cx, |diff, cx| {
3177 diff.snapshot(cx)
3178 .hunks_intersecting_range(
3179 Anchor::min_max_range_for_buffer(diff.buffer_id),
3180 &working_copy,
3181 )
3182 .collect::<Vec<_>>()
3183 });
3184 assert_eq!(hunks.len(), found_hunks.len());
3185
3186 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
3187 assert_eq!(
3188 expected_hunk.buffer_range.to_point(&working_copy),
3189 found_hunk.buffer_range.to_point(&working_copy)
3190 );
3191 assert_eq!(
3192 expected_hunk.diff_base_byte_range,
3193 found_hunk.diff_base_byte_range
3194 );
3195 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
3196 }
3197 hunks = found_hunks;
3198 }
3199 }
3200
3201 #[gpui::test]
3202 async fn test_changed_ranges(cx: &mut gpui::TestAppContext) {
3203 let base_text = "
3204 one
3205 two
3206 three
3207 four
3208 five
3209 six
3210 "
3211 .unindent();
3212 let buffer_text = "
3213 one
3214 TWO
3215 three
3216 four
3217 FIVE
3218 six
3219 "
3220 .unindent();
3221 let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
3222 let diff = cx.new(|cx| {
3223 BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
3224 });
3225 cx.run_until_parked();
3226 let (tx, rx) = mpsc::channel();
3227 let subscription =
3228 cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
3229
3230 let snapshot = buffer.update(cx, |buffer, cx| {
3231 buffer.set_text(
3232 "
3233 ONE
3234 TWO
3235 THREE
3236 FOUR
3237 FIVE
3238 SIX
3239 "
3240 .unindent(),
3241 cx,
3242 );
3243 buffer.text_snapshot()
3244 });
3245 let update = diff
3246 .update(cx, |diff, cx| {
3247 diff.update_diff(
3248 snapshot.clone(),
3249 Some(base_text.as_str().into()),
3250 None,
3251 None,
3252 cx,
3253 )
3254 })
3255 .await;
3256 diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx))
3257 .await;
3258 cx.run_until_parked();
3259 drop(subscription);
3260 let events = rx.into_iter().collect::<Vec<_>>();
3261 match events.as_slice() {
3262 [
3263 BufferDiffEvent::DiffChanged(DiffChanged {
3264 changed_range: _,
3265 base_text_changed_range,
3266 extended_range: _,
3267 }),
3268 ] => {
3269 // TODO(cole) this seems like it should pass but currently fails (see compare_hunks)
3270 // assert_eq!(
3271 // *changed_range,
3272 // Some(Anchor::min_max_range_for_buffer(
3273 // buffer.read_with(cx, |buffer, _| buffer.remote_id())
3274 // ))
3275 // );
3276 assert_eq!(*base_text_changed_range, Some(0..base_text.len()));
3277 }
3278 _ => panic!("unexpected events: {:?}", events),
3279 }
3280 }
3281
3282 #[gpui::test]
3283 async fn test_extended_range(cx: &mut TestAppContext) {
3284 let base_text = "
3285 aaa
3286 bbb
3287
3288
3289
3290
3291
3292 ccc
3293 ddd
3294 "
3295 .unindent();
3296
3297 let buffer_text = "
3298 aaa
3299 bbb
3300
3301
3302
3303
3304
3305 CCC
3306 ddd
3307 "
3308 .unindent();
3309
3310 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
3311 let old_buffer = buffer.snapshot().clone();
3312 let diff_a = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3313
3314 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")]);
3315 let diff_b = BufferDiffSnapshot::new_sync(&buffer, base_text, cx);
3316
3317 let DiffChanged {
3318 changed_range,
3319 base_text_changed_range: _,
3320 extended_range,
3321 } = compare_hunks(
3322 &diff_b.inner.hunks,
3323 &diff_a.inner.hunks,
3324 &old_buffer,
3325 &buffer,
3326 &diff_a.base_text(),
3327 &diff_a.base_text(),
3328 );
3329
3330 let changed_range = changed_range.unwrap();
3331 assert_eq!(
3332 changed_range.to_point(&buffer),
3333 Point::new(7, 0)..Point::new(9, 0),
3334 "changed_range should span from old hunk position to new hunk end"
3335 );
3336
3337 let extended_range = extended_range.unwrap();
3338 assert_eq!(
3339 extended_range.start.to_point(&buffer),
3340 Point::new(1, 3),
3341 "extended_range.start should extend to include the edit outside changed_range"
3342 );
3343 assert_eq!(
3344 extended_range.end.to_point(&buffer),
3345 Point::new(9, 0),
3346 "extended_range.end should collapse to changed_range.end when no edits in end margin"
3347 );
3348
3349 let base_text_2 = "
3350 one
3351 two
3352 three
3353 four
3354 five
3355 six
3356 seven
3357 eight
3358 "
3359 .unindent();
3360
3361 let buffer_text_2 = "
3362 ONE
3363 two
3364 THREE
3365 four
3366 FIVE
3367 six
3368 SEVEN
3369 eight
3370 "
3371 .unindent();
3372
3373 let mut buffer_2 = Buffer::new(ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2);
3374 let old_buffer_2 = buffer_2.snapshot().clone();
3375 let diff_2a = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2.clone(), cx);
3376
3377 buffer_2.edit([(Point::new(4, 0)..Point::new(4, 4), "FIVE_CHANGED")]);
3378 let diff_2b = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2, cx);
3379
3380 let DiffChanged {
3381 changed_range,
3382 base_text_changed_range: _,
3383 extended_range,
3384 } = compare_hunks(
3385 &diff_2b.inner.hunks,
3386 &diff_2a.inner.hunks,
3387 &old_buffer_2,
3388 &buffer_2,
3389 &diff_2a.base_text(),
3390 &diff_2a.base_text(),
3391 );
3392
3393 let changed_range = changed_range.unwrap();
3394 assert_eq!(
3395 changed_range.to_point(&buffer_2),
3396 Point::new(4, 0)..Point::new(5, 0),
3397 "changed_range should be just the hunk that changed (FIVE)"
3398 );
3399
3400 let extended_range = extended_range.unwrap();
3401 assert_eq!(
3402 extended_range.to_point(&buffer_2),
3403 Point::new(4, 0)..Point::new(5, 0),
3404 "extended_range should equal changed_range when edit is within the hunk"
3405 );
3406 }
3407
3408 #[gpui::test]
3409 async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) {
3410 // Use a shared base text buffer so that anchors from old and new snapshots
3411 // share the same remote_id and resolve correctly across versions.
3412 let initial_base = "aaa\nbbb\nccc\nddd\neee\n";
3413 let mut base_text_buffer = Buffer::new(
3414 ReplicaId::LOCAL,
3415 BufferId::new(99).unwrap(),
3416 initial_base.to_string(),
3417 );
3418
3419 // --- Scenario 1: Base text gains a line, producing a new deletion hunk ---
3420 //
3421 // Buffer has a modification (ccc → CCC). When the base text gains
3422 // a new line "XXX" after "aaa", the diff now also contains a
3423 // deletion for that line, and the modification hunk shifts in the
3424 // base text.
3425 let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n";
3426 let buffer = Buffer::new(
3427 ReplicaId::LOCAL,
3428 BufferId::new(1).unwrap(),
3429 buffer_text_1.to_string(),
3430 );
3431
3432 let old_base_snapshot_1 = base_text_buffer.snapshot().clone();
3433 let old_hunks_1 = compute_hunks(
3434 Some((Arc::from(initial_base), Rope::from(initial_base))),
3435 buffer.snapshot(),
3436 None,
3437 );
3438
3439 // Insert "XXX\n" after "aaa\n" in the base text.
3440 base_text_buffer.edit([(4..4, "XXX\n")]);
3441 let new_base_str_1: Arc<str> = Arc::from(base_text_buffer.text().as_str());
3442 let new_base_snapshot_1 = base_text_buffer.snapshot();
3443
3444 let new_hunks_1 = compute_hunks(
3445 Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))),
3446 buffer.snapshot(),
3447 None,
3448 );
3449
3450 let DiffChanged {
3451 changed_range,
3452 base_text_changed_range,
3453 extended_range: _,
3454 } = compare_hunks(
3455 &new_hunks_1,
3456 &old_hunks_1,
3457 &buffer.snapshot(),
3458 &buffer.snapshot(),
3459 &old_base_snapshot_1,
3460 &new_base_snapshot_1,
3461 );
3462
3463 // The new deletion hunk (XXX) starts at buffer row 1 and the
3464 // modification hunk (ccc → CCC) now has a different
3465 // diff_base_byte_range, so the changed range spans both.
3466 let range = changed_range.unwrap();
3467 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),);
3468 let base_range = base_text_changed_range.unwrap();
3469 assert_eq!(
3470 base_range.to_point(&new_base_snapshot_1),
3471 Point::new(1, 0)..Point::new(4, 0),
3472 );
3473
3474 // --- Scenario 2: Base text changes to match the buffer (hunk disappears) ---
3475 //
3476 // Start fresh with a simple base text.
3477 let simple_base = "one\ntwo\nthree\n";
3478 let mut base_buf_2 = Buffer::new(
3479 ReplicaId::LOCAL,
3480 BufferId::new(100).unwrap(),
3481 simple_base.to_string(),
3482 );
3483
3484 let buffer_text_2 = "one\nTWO\nthree\n";
3485 let buffer_2 = Buffer::new(
3486 ReplicaId::LOCAL,
3487 BufferId::new(2).unwrap(),
3488 buffer_text_2.to_string(),
3489 );
3490
3491 let old_base_snapshot_2 = base_buf_2.snapshot().clone();
3492 let old_hunks_2 = compute_hunks(
3493 Some((Arc::from(simple_base), Rope::from(simple_base))),
3494 buffer_2.snapshot(),
3495 None,
3496 );
3497
3498 // The base text is edited so "two" becomes "TWO", now matching the buffer.
3499 base_buf_2.edit([(4..7, "TWO")]);
3500 let new_base_str_2: Arc<str> = Arc::from(base_buf_2.text().as_str());
3501 let new_base_snapshot_2 = base_buf_2.snapshot();
3502
3503 let new_hunks_2 = compute_hunks(
3504 Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))),
3505 buffer_2.snapshot(),
3506 None,
3507 );
3508
3509 let DiffChanged {
3510 changed_range,
3511 base_text_changed_range,
3512 extended_range: _,
3513 } = compare_hunks(
3514 &new_hunks_2,
3515 &old_hunks_2,
3516 &buffer_2.snapshot(),
3517 &buffer_2.snapshot(),
3518 &old_base_snapshot_2,
3519 &new_base_snapshot_2,
3520 );
3521
3522 // The old modification hunk (two → TWO) is now gone because the
3523 // base text matches the buffer. The changed range covers where the
3524 // old hunk used to be.
3525 let range = changed_range.unwrap();
3526 assert_eq!(
3527 range.to_point(&buffer_2),
3528 Point::new(1, 0)..Point::new(2, 0),
3529 );
3530 let base_range = base_text_changed_range.unwrap();
3531 // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8).
3532 // anchor_after(4) is right-biased at the start of the deleted "two",
3533 // so after the edit replacing "two" with "TWO" it resolves past the
3534 // insertion to Point(1, 3).
3535 assert_eq!(
3536 base_range.to_point(&new_base_snapshot_2),
3537 Point::new(1, 3)..Point::new(2, 0),
3538 );
3539
3540 // --- Scenario 3: Base text edit changes one hunk but not another ---
3541 //
3542 // Two modification hunks exist. Only one of them is resolved by
3543 // the base text change; the other remains identical.
3544 let base_3 = "aaa\nbbb\nccc\nddd\neee\n";
3545 let mut base_buf_3 = Buffer::new(
3546 ReplicaId::LOCAL,
3547 BufferId::new(101).unwrap(),
3548 base_3.to_string(),
3549 );
3550
3551 let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n";
3552 let buffer_3 = Buffer::new(
3553 ReplicaId::LOCAL,
3554 BufferId::new(3).unwrap(),
3555 buffer_text_3.to_string(),
3556 );
3557
3558 let old_base_snapshot_3 = base_buf_3.snapshot().clone();
3559 let old_hunks_3 = compute_hunks(
3560 Some((Arc::from(base_3), Rope::from(base_3))),
3561 buffer_3.snapshot(),
3562 None,
3563 );
3564
3565 // Change "ddd" to "DDD" in the base text so that hunk disappears,
3566 // but "bbb" stays, so its hunk remains.
3567 base_buf_3.edit([(12..15, "DDD")]);
3568 let new_base_str_3: Arc<str> = Arc::from(base_buf_3.text().as_str());
3569 let new_base_snapshot_3 = base_buf_3.snapshot();
3570
3571 let new_hunks_3 = compute_hunks(
3572 Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))),
3573 buffer_3.snapshot(),
3574 None,
3575 );
3576
3577 let DiffChanged {
3578 changed_range,
3579 base_text_changed_range,
3580 extended_range: _,
3581 } = compare_hunks(
3582 &new_hunks_3,
3583 &old_hunks_3,
3584 &buffer_3.snapshot(),
3585 &buffer_3.snapshot(),
3586 &old_base_snapshot_3,
3587 &new_base_snapshot_3,
3588 );
3589
3590 // Only the second hunk (ddd → DDD) disappeared; the first hunk
3591 // (bbb → BBB) is unchanged, so the changed range covers only line 3.
3592 let range = changed_range.unwrap();
3593 assert_eq!(
3594 range.to_point(&buffer_3),
3595 Point::new(3, 0)..Point::new(4, 0),
3596 );
3597 let base_range = base_text_changed_range.unwrap();
3598 // anchor_after(12) is right-biased at the start of deleted "ddd",
3599 // so after the edit replacing "ddd" with "DDD" it resolves past
3600 // the insertion to Point(3, 3).
3601 assert_eq!(
3602 base_range.to_point(&new_base_snapshot_3),
3603 Point::new(3, 3)..Point::new(4, 0),
3604 );
3605
3606 // --- Scenario 4: Both buffer and base text change simultaneously ---
3607 //
3608 // The buffer gains an edit that introduces a new hunk while the
3609 // base text also changes.
3610 let base_4 = "alpha\nbeta\ngamma\ndelta\n";
3611 let mut base_buf_4 = Buffer::new(
3612 ReplicaId::LOCAL,
3613 BufferId::new(102).unwrap(),
3614 base_4.to_string(),
3615 );
3616
3617 let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n";
3618 let mut buffer_4 = Buffer::new(
3619 ReplicaId::LOCAL,
3620 BufferId::new(4).unwrap(),
3621 buffer_text_4.to_string(),
3622 );
3623
3624 let old_base_snapshot_4 = base_buf_4.snapshot().clone();
3625 let old_buffer_snapshot_4 = buffer_4.snapshot().clone();
3626 let old_hunks_4 = compute_hunks(
3627 Some((Arc::from(base_4), Rope::from(base_4))),
3628 buffer_4.snapshot(),
3629 None,
3630 );
3631
3632 // Edit the buffer: change "delta" to "DELTA" (new modification hunk).
3633 buffer_4.edit_via_marked_text(
3634 &"
3635 alpha
3636 BETA
3637 gamma
3638 «DELTA»
3639 "
3640 .unindent(),
3641 );
3642
3643 // Edit the base text: change "beta" to "BETA" (resolves that hunk).
3644 base_buf_4.edit([(6..10, "BETA")]);
3645 let new_base_str_4: Arc<str> = Arc::from(base_buf_4.text().as_str());
3646 let new_base_snapshot_4 = base_buf_4.snapshot();
3647
3648 let new_hunks_4 = compute_hunks(
3649 Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))),
3650 buffer_4.snapshot(),
3651 None,
3652 );
3653
3654 let DiffChanged {
3655 changed_range,
3656 base_text_changed_range,
3657 extended_range: _,
3658 } = compare_hunks(
3659 &new_hunks_4,
3660 &old_hunks_4,
3661 &old_buffer_snapshot_4,
3662 &buffer_4.snapshot(),
3663 &old_base_snapshot_4,
3664 &new_base_snapshot_4,
3665 );
3666
3667 // The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3)
3668 // appeared, so the changed range spans from line 1 through line 4.
3669 let range = changed_range.unwrap();
3670 assert_eq!(
3671 range.to_point(&buffer_4),
3672 Point::new(1, 0)..Point::new(4, 0),
3673 );
3674 let base_range = base_text_changed_range.unwrap();
3675 // The old BETA hunk's base range started at byte 6 ("beta"). After
3676 // the base text edit replacing "beta" with "BETA", anchor_after(6)
3677 // resolves past the insertion to Point(1, 4).
3678 assert_eq!(
3679 base_range.to_point(&new_base_snapshot_4),
3680 Point::new(1, 4)..Point::new(4, 0),
3681 );
3682 }
3683
3684 #[gpui::test(iterations = 100)]
3685 async fn test_patch_for_range_random(cx: &mut TestAppContext, mut rng: StdRng) {
3686 fn gen_line(rng: &mut StdRng) -> String {
3687 if rng.random_bool(0.2) {
3688 "\n".to_owned()
3689 } else {
3690 let c = rng.random_range('A'..='Z');
3691 format!("{c}{c}{c}\n")
3692 }
3693 }
3694
3695 fn gen_text(rng: &mut StdRng, line_count: usize) -> String {
3696 (0..line_count).map(|_| gen_line(rng)).collect()
3697 }
3698
3699 fn gen_edits_from(rng: &mut StdRng, base: &str) -> String {
3700 let mut old_lines: Vec<&str> = base.lines().collect();
3701 let mut result = String::new();
3702
3703 while !old_lines.is_empty() {
3704 let unchanged_count = rng.random_range(0..=old_lines.len());
3705 for _ in 0..unchanged_count {
3706 if old_lines.is_empty() {
3707 break;
3708 }
3709 result.push_str(old_lines.remove(0));
3710 result.push('\n');
3711 }
3712
3713 if old_lines.is_empty() {
3714 break;
3715 }
3716
3717 let deleted_count = rng.random_range(0..=old_lines.len().min(3));
3718 for _ in 0..deleted_count {
3719 if old_lines.is_empty() {
3720 break;
3721 }
3722 old_lines.remove(0);
3723 }
3724
3725 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
3726 let added_count = rng.random_range(minimum_added..=3);
3727 for _ in 0..added_count {
3728 result.push_str(&gen_line(rng));
3729 }
3730 }
3731
3732 result
3733 }
3734
3735 fn random_point_in_text(rng: &mut StdRng, lines: &[&str]) -> Point {
3736 if lines.is_empty() {
3737 return Point::zero();
3738 }
3739 let row = rng.random_range(0..lines.len() as u32);
3740 let line = lines[row as usize];
3741 let col = if line.is_empty() {
3742 0
3743 } else {
3744 rng.random_range(0..=line.len() as u32)
3745 };
3746 Point::new(row, col)
3747 }
3748
3749 fn random_range_in_text(rng: &mut StdRng, lines: &[&str]) -> RangeInclusive<Point> {
3750 let start = random_point_in_text(rng, lines);
3751 let end = random_point_in_text(rng, lines);
3752 if start <= end {
3753 start..=end
3754 } else {
3755 end..=start
3756 }
3757 }
3758
3759 fn points_in_range(range: &RangeInclusive<Point>, lines: &[&str]) -> Vec<Point> {
3760 let mut points = Vec::new();
3761 for row in range.start().row..=range.end().row {
3762 if row as usize >= lines.len() {
3763 points.push(Point::new(row, 0));
3764 continue;
3765 }
3766 let line = lines[row as usize];
3767 let start_col = if row == range.start().row {
3768 range.start().column
3769 } else {
3770 0
3771 };
3772 let end_col = if row == range.end().row {
3773 range.end().column
3774 } else {
3775 line.len() as u32
3776 };
3777 for col in start_col..=end_col {
3778 points.push(Point::new(row, col));
3779 }
3780 }
3781 points
3782 }
3783
3784 let rng = &mut rng;
3785
3786 let line_count = rng.random_range(5..20);
3787 let base_text = gen_text(rng, line_count);
3788 let initial_buffer_text = gen_edits_from(rng, &base_text);
3789
3790 let mut buffer = Buffer::new(
3791 ReplicaId::LOCAL,
3792 BufferId::new(1).unwrap(),
3793 initial_buffer_text.clone(),
3794 );
3795
3796 let diff = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3797
3798 let edit_count = rng.random_range(1..=5);
3799 for _ in 0..edit_count {
3800 let buffer_text = buffer.text();
3801 if buffer_text.is_empty() {
3802 buffer.edit([(0..0, gen_line(rng))]);
3803 } else {
3804 let lines: Vec<&str> = buffer_text.lines().collect();
3805 let start_row = rng.random_range(0..lines.len());
3806 let end_row = rng.random_range(start_row..=lines.len().min(start_row + 3));
3807
3808 let start_col = if start_row < lines.len() {
3809 rng.random_range(0..=lines[start_row].len())
3810 } else {
3811 0
3812 };
3813 let end_col = if end_row < lines.len() {
3814 rng.random_range(0..=lines[end_row].len())
3815 } else {
3816 0
3817 };
3818
3819 let start_offset = buffer
3820 .point_to_offset(Point::new(start_row as u32, start_col as u32))
3821 .min(buffer.len());
3822 let end_offset = buffer
3823 .point_to_offset(Point::new(end_row as u32, end_col as u32))
3824 .min(buffer.len());
3825
3826 let (start, end) = if start_offset <= end_offset {
3827 (start_offset, end_offset)
3828 } else {
3829 (end_offset, start_offset)
3830 };
3831
3832 let new_text = if rng.random_bool(0.3) {
3833 String::new()
3834 } else {
3835 let line_count = rng.random_range(0..=2);
3836 gen_text(rng, line_count)
3837 };
3838
3839 buffer.edit([(start..end, new_text)]);
3840 }
3841 }
3842
3843 let buffer_snapshot = buffer.snapshot();
3844
3845 let buffer_text = buffer_snapshot.text();
3846 let buffer_lines: Vec<&str> = buffer_text.lines().collect();
3847 let base_lines: Vec<&str> = base_text.lines().collect();
3848
3849 let test_count = 10;
3850 for _ in 0..test_count {
3851 let range = random_range_in_text(rng, &buffer_lines);
3852 let points = points_in_range(&range, &buffer_lines);
3853
3854 let optimized_patch = diff.patch_for_buffer_range(range.clone(), &buffer_snapshot);
3855 let naive_patch = diff.patch_for_buffer_range_naive(&buffer_snapshot);
3856
3857 for point in points {
3858 let optimized_edit = optimized_patch.edit_for_old_position(point);
3859 let naive_edit = naive_patch.edit_for_old_position(point);
3860
3861 assert_eq!(
3862 optimized_edit,
3863 naive_edit,
3864 "patch_for_buffer_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
3865 point,
3866 range,
3867 base_text,
3868 initial_buffer_text,
3869 buffer_snapshot.text()
3870 );
3871 }
3872 }
3873
3874 for _ in 0..test_count {
3875 let range = random_range_in_text(rng, &base_lines);
3876 let points = points_in_range(&range, &base_lines);
3877
3878 let optimized_patch = diff.patch_for_base_text_range(range.clone(), &buffer_snapshot);
3879 let naive_patch = diff.patch_for_base_text_range_naive(&buffer_snapshot);
3880
3881 for point in points {
3882 let optimized_edit = optimized_patch.edit_for_old_position(point);
3883 let naive_edit = naive_patch.edit_for_old_position(point);
3884
3885 assert_eq!(
3886 optimized_edit,
3887 naive_edit,
3888 "patch_for_base_text_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
3889 point,
3890 range,
3891 base_text,
3892 initial_buffer_text,
3893 buffer_snapshot.text()
3894 );
3895 }
3896 }
3897 }
3898}