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