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