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