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