1use futures::channel::oneshot;
2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
3use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task, TaskLabel};
4use language::{
5 BufferRow, Capability, DiffOptions, File, Language, LanguageName, LanguageRegistry,
6 language_settings::language_settings, word_diff_ranges,
7};
8use rope::Rope;
9use std::{
10 cmp::Ordering,
11 iter,
12 ops::Range,
13 sync::{Arc, LazyLock},
14};
15use sum_tree::SumTree;
16use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _};
17use util::ResultExt;
18
19pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
20pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
21
22pub struct BufferDiff {
23 pub buffer_id: BufferId,
24 inner: BufferDiffInner<Entity<language::Buffer>>,
25 // diff of the index vs head
26 secondary_diff: Option<Entity<BufferDiff>>,
27}
28
29#[derive(Clone)]
30pub struct BufferDiffSnapshot {
31 inner: BufferDiffInner<language::BufferSnapshot>,
32 secondary_diff: Option<Box<BufferDiffSnapshot>>,
33}
34
35impl std::fmt::Debug for BufferDiffSnapshot {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("BufferDiffSnapshot")
38 .field("inner", &self.inner)
39 .field("secondary_diff", &self.secondary_diff)
40 .finish()
41 }
42}
43
44#[derive(Clone)]
45pub struct BufferDiffUpdate {
46 base_text_changed: bool,
47 inner: BufferDiffInner<Arc<str>>,
48}
49
50#[derive(Clone)]
51struct BufferDiffInner<BaseText> {
52 hunks: SumTree<InternalDiffHunk>,
53 pending_hunks: SumTree<PendingHunk>,
54 base_text: BaseText,
55 base_text_exists: bool,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct DiffHunkStatus {
60 pub kind: DiffHunkStatusKind,
61 pub secondary: DiffHunkSecondaryStatus,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum DiffHunkStatusKind {
66 Added,
67 Modified,
68 Deleted,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72/// Diff of Working Copy vs Index
73/// aka 'is this hunk staged or not'
74pub enum DiffHunkSecondaryStatus {
75 /// Unstaged
76 HasSecondaryHunk,
77 /// Partially staged
78 OverlapsWithSecondaryHunk,
79 /// Staged
80 NoSecondaryHunk,
81 /// We are unstaging
82 SecondaryHunkAdditionPending,
83 /// We are stagind
84 SecondaryHunkRemovalPending,
85}
86
87/// A diff hunk resolved to rows in the buffer.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct DiffHunk {
90 /// The buffer range as points.
91 pub range: Range<Point>,
92 /// The range in the buffer to which this hunk corresponds.
93 pub buffer_range: Range<Anchor>,
94 /// The range in the buffer's diff base text to which this hunk corresponds.
95 pub diff_base_byte_range: Range<usize>,
96 pub secondary_status: DiffHunkSecondaryStatus,
97 // Anchors representing the word diff locations in the active buffer
98 pub buffer_word_diffs: Vec<Range<Anchor>>,
99 // Offsets relative to the start of the deleted diff that represent word diff locations
100 pub base_word_diffs: Vec<Range<usize>>,
101}
102
103/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
104#[derive(Debug, Clone, PartialEq, Eq)]
105struct InternalDiffHunk {
106 buffer_range: Range<Anchor>,
107 diff_base_byte_range: Range<usize>,
108 base_word_diffs: Vec<Range<usize>>,
109 buffer_word_diffs: Vec<Range<Anchor>>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113struct PendingHunk {
114 buffer_range: Range<Anchor>,
115 diff_base_byte_range: Range<usize>,
116 buffer_version: clock::Global,
117 new_status: DiffHunkSecondaryStatus,
118}
119
120#[derive(Debug, Clone)]
121pub struct DiffHunkSummary {
122 buffer_range: Range<Anchor>,
123 diff_base_byte_range: Range<usize>,
124}
125
126impl sum_tree::Item for InternalDiffHunk {
127 type Summary = DiffHunkSummary;
128
129 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
130 DiffHunkSummary {
131 buffer_range: self.buffer_range.clone(),
132 diff_base_byte_range: self.diff_base_byte_range.clone(),
133 }
134 }
135}
136
137impl sum_tree::Item for PendingHunk {
138 type Summary = DiffHunkSummary;
139
140 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
141 DiffHunkSummary {
142 buffer_range: self.buffer_range.clone(),
143 diff_base_byte_range: self.diff_base_byte_range.clone(),
144 }
145 }
146}
147
148impl sum_tree::Summary for DiffHunkSummary {
149 type Context<'a> = &'a text::BufferSnapshot;
150
151 fn zero(_cx: Self::Context<'_>) -> Self {
152 DiffHunkSummary {
153 buffer_range: Anchor::MIN..Anchor::MIN,
154 diff_base_byte_range: 0..0,
155 }
156 }
157
158 fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
159 self.buffer_range.start = *self
160 .buffer_range
161 .start
162 .min(&other.buffer_range.start, buffer);
163 self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
164
165 self.diff_base_byte_range.start = self
166 .diff_base_byte_range
167 .start
168 .min(other.diff_base_byte_range.start);
169 self.diff_base_byte_range.end = self
170 .diff_base_byte_range
171 .end
172 .max(other.diff_base_byte_range.end);
173 }
174}
175
176impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
177 fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
178 if self
179 .cmp(&cursor_location.buffer_range.start, buffer)
180 .is_lt()
181 {
182 Ordering::Less
183 } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
184 Ordering::Greater
185 } else {
186 Ordering::Equal
187 }
188 }
189}
190
191impl std::fmt::Debug for BufferDiffInner<language::BufferSnapshot> {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("BufferDiffSnapshot")
194 .field("hunks", &self.hunks)
195 .field("remote_id", &self.base_text.remote_id())
196 .finish()
197 }
198}
199
200impl BufferDiffSnapshot {
201 #[cfg(test)]
202 fn new_sync(
203 buffer: text::BufferSnapshot,
204 diff_base: String,
205 cx: &mut gpui::TestAppContext,
206 ) -> BufferDiffSnapshot {
207 let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
208 buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx))
209 }
210
211 pub fn is_empty(&self) -> bool {
212 self.inner.hunks.is_empty()
213 }
214
215 pub fn base_text_string(&self) -> Option<String> {
216 self.inner
217 .base_text_exists
218 .then(|| self.inner.base_text.text())
219 }
220
221 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
222 self.secondary_diff.as_deref()
223 }
224
225 pub fn hunks_intersecting_range<'a>(
226 &'a self,
227 range: Range<Anchor>,
228 buffer: &'a text::BufferSnapshot,
229 ) -> impl 'a + Iterator<Item = DiffHunk> {
230 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
231 self.inner
232 .hunks_intersecting_range(range, buffer, unstaged_counterpart)
233 }
234
235 pub fn hunks_intersecting_range_rev<'a>(
236 &'a self,
237 range: Range<Anchor>,
238 buffer: &'a text::BufferSnapshot,
239 ) -> impl 'a + Iterator<Item = DiffHunk> {
240 let filter = move |summary: &DiffHunkSummary| {
241 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
242 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
243 !before_start && !after_end
244 };
245 self.inner.hunks_intersecting_range_rev_impl(filter, buffer)
246 }
247
248 pub fn hunks_intersecting_base_text_range<'a>(
249 &'a self,
250 range: Range<usize>,
251 main_buffer: &'a text::BufferSnapshot,
252 ) -> impl 'a + Iterator<Item = DiffHunk> {
253 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
254 let filter = move |summary: &DiffHunkSummary| {
255 let before_start = summary.diff_base_byte_range.end < range.start;
256 let after_end = summary.diff_base_byte_range.start > range.end;
257 !before_start && !after_end
258 };
259 self.inner
260 .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart)
261 }
262
263 pub fn hunks_intersecting_base_text_range_rev<'a>(
264 &'a self,
265 range: Range<usize>,
266 main_buffer: &'a text::BufferSnapshot,
267 ) -> impl 'a + Iterator<Item = DiffHunk> {
268 let filter = move |summary: &DiffHunkSummary| {
269 let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt();
270 let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt();
271 !before_start && !after_end
272 };
273 self.inner
274 .hunks_intersecting_range_rev_impl(filter, main_buffer)
275 }
276
277 pub fn hunks<'a>(
278 &'a self,
279 buffer_snapshot: &'a text::BufferSnapshot,
280 ) -> impl 'a + Iterator<Item = DiffHunk> {
281 self.hunks_intersecting_range(
282 Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
283 buffer_snapshot,
284 )
285 }
286
287 pub fn hunks_in_row_range<'a>(
288 &'a self,
289 range: Range<u32>,
290 buffer: &'a text::BufferSnapshot,
291 ) -> impl 'a + Iterator<Item = DiffHunk> {
292 let start = buffer.anchor_before(Point::new(range.start, 0));
293 let end = buffer.anchor_after(Point::new(range.end, 0));
294 self.hunks_intersecting_range(start..end, buffer)
295 }
296
297 pub fn range_to_hunk_range(
298 &self,
299 range: Range<Anchor>,
300 buffer: &text::BufferSnapshot,
301 ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
302 let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next();
303 let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next();
304 let range = first_hunk
305 .as_ref()
306 .zip(last_hunk.as_ref())
307 .map(|(first, last)| first.buffer_range.start..last.buffer_range.end);
308 let base_text_range = first_hunk
309 .zip(last_hunk)
310 .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end);
311 (range, base_text_range)
312 }
313
314 pub fn base_text(&self) -> &language::BufferSnapshot {
315 &self.inner.base_text
316 }
317
318 /// If this function returns `true`, the base texts are equal. If this
319 /// function returns `false`, they might be equal, but might not. This
320 /// result is used to avoid recalculating diffs in situations where we know
321 /// nothing has changed.
322 pub fn base_texts_definitely_eq(&self, other: &Self) -> bool {
323 if self.inner.base_text_exists != other.inner.base_text_exists {
324 return false;
325 }
326 let left = &self.inner.base_text;
327 let right = &other.inner.base_text;
328 let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty());
329 let (new_id, new_version, new_empty) =
330 (right.remote_id(), right.version(), right.is_empty());
331 (new_id == old_id && new_version == old_version) || (new_empty && old_empty)
332 }
333
334 pub fn row_to_base_text_row(
335 &self,
336 row: BufferRow,
337 bias: Bias,
338 buffer: &text::BufferSnapshot,
339 ) -> u32 {
340 // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start
341 let target = buffer.anchor_before(Point::new(row, 0));
342 // Find the last hunk that starts before the target.
343 let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer);
344 cursor.seek(&target, Bias::Left);
345 if cursor
346 .item()
347 .is_none_or(|hunk| hunk.buffer_range.start.cmp(&target, buffer).is_gt())
348 {
349 cursor.prev();
350 }
351
352 let unclipped_point = if let Some(hunk) = cursor.item()
353 && hunk.buffer_range.start.cmp(&target, buffer).is_le()
354 {
355 // Found a hunk that starts before the target.
356 let hunk_base_text_end = cursor.end().diff_base_byte_range.end;
357 let unclipped_point = if target.cmp(&cursor.end().buffer_range.end, buffer).is_ge() {
358 // Target falls strictly between two hunks.
359 let mut unclipped_point = hunk_base_text_end.to_point(self.base_text());
360 unclipped_point +=
361 Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer);
362 unclipped_point
363 } else if bias == Bias::Right {
364 hunk_base_text_end.to_point(self.base_text())
365 } else {
366 hunk.diff_base_byte_range.start.to_point(self.base_text())
367 };
368 // Move the cursor so that at the next step we can clip with the start of the next hunk.
369 cursor.next();
370 unclipped_point
371 } else {
372 // Target is before the added region for the first hunk.
373 debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| {
374 target.cmp(&first_hunk.buffer_range.start, buffer).is_le()
375 }));
376 Point::new(row, 0)
377 };
378
379 // If the target falls in the region between two hunks, we added an overshoot above.
380 // There may be changes in the main buffer that are not reflected in the hunks,
381 // so we need to ensure this overshoot keeps us in the corresponding base text region.
382 let max_point = if let Some(next_hunk) = cursor.item() {
383 next_hunk
384 .diff_base_byte_range
385 .start
386 .to_point(self.base_text())
387 } else {
388 self.base_text().max_point()
389 };
390 unclipped_point.min(max_point).row
391 }
392}
393
394impl BufferDiffInner<Entity<language::Buffer>> {
395 /// Returns the new index text and new pending hunks.
396 fn stage_or_unstage_hunks_impl(
397 &mut self,
398 unstaged_diff: &Self,
399 stage: bool,
400 hunks: &[DiffHunk],
401 buffer: &text::BufferSnapshot,
402 file_exists: bool,
403 cx: &mut Context<BufferDiff>,
404 ) -> Option<Rope> {
405 let head_text = self
406 .base_text_exists
407 .then(|| self.base_text.read(cx).as_rope().clone());
408 let index_text = unstaged_diff
409 .base_text_exists
410 .then(|| unstaged_diff.base_text.read(cx).as_rope().clone());
411
412 // If the file doesn't exist in either HEAD or the index, then the
413 // entire file must be either created or deleted in the index.
414 let (index_text, head_text) = match (index_text, head_text) {
415 (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
416 (index_text, head_text) => {
417 let (new_index_text, new_status) = if stage {
418 log::debug!("stage all");
419 (
420 file_exists.then(|| buffer.as_rope().clone()),
421 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
422 )
423 } else {
424 log::debug!("unstage all");
425 (
426 head_text,
427 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
428 )
429 };
430
431 let hunk = PendingHunk {
432 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
433 diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
434 buffer_version: buffer.version().clone(),
435 new_status,
436 };
437 self.pending_hunks = SumTree::from_item(hunk, buffer);
438 return new_index_text;
439 }
440 };
441
442 let mut pending_hunks = SumTree::new(buffer);
443 let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
444
445 // first, merge new hunks into pending_hunks
446 for DiffHunk {
447 buffer_range,
448 diff_base_byte_range,
449 secondary_status,
450 ..
451 } in hunks.iter().cloned()
452 {
453 let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
454 pending_hunks.append(preceding_pending_hunks, buffer);
455
456 // Skip all overlapping or adjacent old pending hunks
457 while old_pending_hunks.item().is_some_and(|old_hunk| {
458 old_hunk
459 .buffer_range
460 .start
461 .cmp(&buffer_range.end, buffer)
462 .is_le()
463 }) {
464 old_pending_hunks.next();
465 }
466
467 if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
468 || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
469 {
470 continue;
471 }
472
473 pending_hunks.push(
474 PendingHunk {
475 buffer_range,
476 diff_base_byte_range,
477 buffer_version: buffer.version().clone(),
478 new_status: if stage {
479 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
480 } else {
481 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
482 },
483 },
484 buffer,
485 );
486 }
487 // append the remainder
488 pending_hunks.append(old_pending_hunks.suffix(), buffer);
489
490 let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
491 unstaged_hunk_cursor.next();
492
493 // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
494 let mut prev_unstaged_hunk_buffer_end = 0;
495 let mut prev_unstaged_hunk_base_text_end = 0;
496 let mut edits = Vec::<(Range<usize>, String)>::new();
497 let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
498 while let Some(PendingHunk {
499 buffer_range,
500 diff_base_byte_range,
501 new_status,
502 ..
503 }) = pending_hunks_iter.next()
504 {
505 // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
506 let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
507
508 if let Some(unstaged_hunk) = skipped_unstaged.last() {
509 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
510 prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
511 }
512
513 // Find where this hunk is in the index if it doesn't overlap
514 let mut buffer_offset_range = buffer_range.to_offset(buffer);
515 let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
516 let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
517
518 loop {
519 // Merge this hunk with any overlapping unstaged hunks.
520 if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
521 let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
522 if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
523 prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
524 prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
525
526 index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
527 buffer_offset_range.start = buffer_offset_range
528 .start
529 .min(unstaged_hunk_offset_range.start);
530 buffer_offset_range.end =
531 buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
532
533 unstaged_hunk_cursor.next();
534 continue;
535 }
536 }
537
538 // If any unstaged hunks were merged, then subsequent pending hunks may
539 // now overlap this hunk. Merge them.
540 if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
541 let next_pending_hunk_offset_range =
542 next_pending_hunk.buffer_range.to_offset(buffer);
543 if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
544 buffer_offset_range.end = next_pending_hunk_offset_range.end;
545 pending_hunks_iter.next();
546 continue;
547 }
548 }
549
550 break;
551 }
552
553 let end_overshoot = buffer_offset_range
554 .end
555 .saturating_sub(prev_unstaged_hunk_buffer_end);
556 let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
557 let index_byte_range = index_start..index_end;
558
559 let replacement_text = match new_status {
560 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
561 log::debug!("staging hunk {:?}", buffer_offset_range);
562 buffer
563 .text_for_range(buffer_offset_range)
564 .collect::<String>()
565 }
566 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
567 log::debug!("unstaging hunk {:?}", buffer_offset_range);
568 head_text
569 .chunks_in_range(diff_base_byte_range.clone())
570 .collect::<String>()
571 }
572 _ => {
573 debug_assert!(false);
574 continue;
575 }
576 };
577
578 edits.push((index_byte_range, replacement_text));
579 }
580 drop(pending_hunks_iter);
581 drop(old_pending_hunks);
582 self.pending_hunks = pending_hunks;
583
584 #[cfg(debug_assertions)] // invariants: non-overlapping and sorted
585 {
586 for window in edits.windows(2) {
587 let (range_a, range_b) = (&window[0].0, &window[1].0);
588 debug_assert!(range_a.end < range_b.start);
589 }
590 }
591
592 let mut new_index_text = Rope::new();
593 let mut index_cursor = index_text.cursor(0);
594
595 for (old_range, replacement_text) in edits {
596 new_index_text.append(index_cursor.slice(old_range.start));
597 index_cursor.seek_forward(old_range.end);
598 new_index_text.push(&replacement_text);
599 }
600 new_index_text.append(index_cursor.suffix());
601 Some(new_index_text)
602 }
603}
604
605impl BufferDiffInner<language::BufferSnapshot> {
606 fn hunks_intersecting_range<'a>(
607 &'a self,
608 range: Range<Anchor>,
609 buffer: &'a text::BufferSnapshot,
610 secondary: Option<&'a Self>,
611 ) -> impl 'a + Iterator<Item = DiffHunk> {
612 let range = range.to_offset(buffer);
613 let filter = move |summary: &DiffHunkSummary| {
614 let summary_range = summary.buffer_range.to_offset(buffer);
615 let before_start = summary_range.end < range.start;
616 let after_end = summary_range.start > range.end;
617 !before_start && !after_end
618 };
619 self.hunks_intersecting_range_impl(filter, buffer, secondary)
620 }
621
622 fn hunks_intersecting_range_impl<'a>(
623 &'a self,
624 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
625 buffer: &'a text::BufferSnapshot,
626 secondary: Option<&'a Self>,
627 ) -> impl 'a + Iterator<Item = DiffHunk> {
628 let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
629
630 let anchor_iter = iter::from_fn(move || {
631 cursor.next();
632 cursor.item()
633 })
634 .flat_map(move |hunk| {
635 [
636 (
637 &hunk.buffer_range.start,
638 (
639 hunk.buffer_range.start,
640 hunk.diff_base_byte_range.start,
641 hunk,
642 ),
643 ),
644 (
645 &hunk.buffer_range.end,
646 (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
647 ),
648 ]
649 });
650
651 let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
652 pending_hunks_cursor.next();
653
654 let mut secondary_cursor = None;
655 if let Some(secondary) = secondary.as_ref() {
656 let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
657 cursor.next();
658 secondary_cursor = Some(cursor);
659 }
660
661 let max_point = buffer.max_point();
662 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
663 iter::from_fn(move || {
664 loop {
665 let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
666 let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
667
668 let base_word_diffs = hunk.base_word_diffs.clone();
669 let buffer_word_diffs = hunk.buffer_word_diffs.clone();
670
671 if !start_anchor.is_valid(buffer) {
672 continue;
673 }
674
675 if end_point.column > 0 && end_point < max_point {
676 end_point.row += 1;
677 end_point.column = 0;
678 end_anchor = buffer.anchor_before(end_point);
679 }
680
681 let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
682
683 let mut has_pending = false;
684 if start_anchor
685 .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
686 .is_gt()
687 {
688 pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
689 }
690
691 if let Some(pending_hunk) = pending_hunks_cursor.item() {
692 let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
693 if pending_range.end.column > 0 {
694 pending_range.end.row += 1;
695 pending_range.end.column = 0;
696 }
697
698 if pending_range == (start_point..end_point)
699 && !buffer.has_edits_since_in_range(
700 &pending_hunk.buffer_version,
701 start_anchor..end_anchor,
702 )
703 {
704 has_pending = true;
705 secondary_status = pending_hunk.new_status;
706 }
707 }
708
709 if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
710 if start_anchor
711 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
712 .is_gt()
713 {
714 secondary_cursor.seek_forward(&start_anchor, Bias::Left);
715 }
716
717 if let Some(secondary_hunk) = secondary_cursor.item() {
718 let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
719 if secondary_range.end.column > 0 {
720 secondary_range.end.row += 1;
721 secondary_range.end.column = 0;
722 }
723 if secondary_range.is_empty()
724 && secondary_hunk.diff_base_byte_range.is_empty()
725 {
726 // ignore
727 } else if secondary_range == (start_point..end_point) {
728 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
729 } else if secondary_range.start <= end_point {
730 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
731 }
732 }
733 }
734
735 return Some(DiffHunk {
736 range: start_point..end_point,
737 diff_base_byte_range: start_base..end_base,
738 buffer_range: start_anchor..end_anchor,
739 base_word_diffs,
740 buffer_word_diffs,
741 secondary_status,
742 });
743 }
744 })
745 }
746
747 fn hunks_intersecting_range_rev_impl<'a>(
748 &'a self,
749 filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
750 buffer: &'a text::BufferSnapshot,
751 ) -> impl 'a + Iterator<Item = DiffHunk> {
752 let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
753
754 iter::from_fn(move || {
755 cursor.prev();
756
757 let hunk = cursor.item()?;
758 let range = hunk.buffer_range.to_point(buffer);
759
760 Some(DiffHunk {
761 range,
762 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
763 buffer_range: hunk.buffer_range.clone(),
764 // The secondary status is not used by callers of this method.
765 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
766 base_word_diffs: hunk.base_word_diffs.clone(),
767 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
768 })
769 })
770 }
771}
772
773fn build_diff_options(
774 file: Option<&Arc<dyn File>>,
775 language: Option<LanguageName>,
776 language_scope: Option<language::LanguageScope>,
777 cx: &App,
778) -> Option<DiffOptions> {
779 #[cfg(any(test, feature = "test-support"))]
780 {
781 if !cx.has_global::<settings::SettingsStore>() {
782 return Some(DiffOptions {
783 language_scope,
784 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
785 ..Default::default()
786 });
787 }
788 }
789
790 language_settings(language, file, cx)
791 .word_diff_enabled
792 .then_some(DiffOptions {
793 language_scope,
794 max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
795 ..Default::default()
796 })
797}
798
799fn compute_hunks(
800 diff_base: Option<(Arc<str>, Rope)>,
801 buffer: text::BufferSnapshot,
802 diff_options: Option<DiffOptions>,
803) -> SumTree<InternalDiffHunk> {
804 let mut tree = SumTree::new(&buffer);
805
806 if let Some((diff_base, diff_base_rope)) = diff_base {
807 let buffer_text = buffer.as_rope().to_string();
808
809 let mut options = GitOptions::default();
810 options.context_lines(0);
811 let patch = GitPatch::from_buffers(
812 diff_base.as_bytes(),
813 None,
814 buffer_text.as_bytes(),
815 None,
816 Some(&mut options),
817 )
818 .log_err();
819
820 // A common case in Zed is that the empty buffer is represented as just a newline,
821 // but if we just compute a naive diff you get a "preserved" line in the middle,
822 // which is a bit odd.
823 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
824 tree.push(
825 InternalDiffHunk {
826 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
827 diff_base_byte_range: 0..diff_base.len() - 1,
828 base_word_diffs: Vec::default(),
829 buffer_word_diffs: Vec::default(),
830 },
831 &buffer,
832 );
833 return tree;
834 }
835
836 if let Some(patch) = patch {
837 let mut divergence = 0;
838 for hunk_index in 0..patch.num_hunks() {
839 let hunk = process_patch_hunk(
840 &patch,
841 hunk_index,
842 &diff_base_rope,
843 &buffer,
844 &mut divergence,
845 diff_options.as_ref(),
846 );
847 tree.push(hunk, &buffer);
848 }
849 }
850 } else {
851 tree.push(
852 InternalDiffHunk {
853 buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
854 diff_base_byte_range: 0..0,
855 base_word_diffs: Vec::default(),
856 buffer_word_diffs: Vec::default(),
857 },
858 &buffer,
859 );
860 }
861
862 tree
863}
864
865fn compare_hunks(
866 new_hunks: &SumTree<InternalDiffHunk>,
867 old_hunks: &SumTree<InternalDiffHunk>,
868 new_snapshot: &text::BufferSnapshot,
869) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
870 let mut new_cursor = new_hunks.cursor::<()>(new_snapshot);
871 let mut old_cursor = old_hunks.cursor::<()>(new_snapshot);
872 old_cursor.next();
873 new_cursor.next();
874 let mut start = None;
875 let mut end = None;
876 let mut base_text_start = None;
877 let mut base_text_end = None;
878
879 loop {
880 match (new_cursor.item(), old_cursor.item()) {
881 (Some(new_hunk), Some(old_hunk)) => {
882 match new_hunk
883 .buffer_range
884 .start
885 .cmp(&old_hunk.buffer_range.start, new_snapshot)
886 {
887 Ordering::Less => {
888 start.get_or_insert(new_hunk.buffer_range.start);
889 base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start);
890 end.replace(new_hunk.buffer_range.end);
891 base_text_end.replace(new_hunk.diff_base_byte_range.end);
892 new_cursor.next();
893 }
894 Ordering::Equal => {
895 if new_hunk != old_hunk {
896 start.get_or_insert(new_hunk.buffer_range.start);
897 base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start);
898 if old_hunk
899 .buffer_range
900 .end
901 .cmp(&new_hunk.buffer_range.end, new_snapshot)
902 .is_ge()
903 {
904 end.replace(old_hunk.buffer_range.end);
905 } else {
906 end.replace(new_hunk.buffer_range.end);
907 }
908
909 base_text_end.replace(
910 old_hunk
911 .diff_base_byte_range
912 .end
913 .max(new_hunk.diff_base_byte_range.end),
914 );
915 }
916
917 new_cursor.next();
918 old_cursor.next();
919 }
920 Ordering::Greater => {
921 start.get_or_insert(old_hunk.buffer_range.start);
922 base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start);
923 end.replace(old_hunk.buffer_range.end);
924 base_text_end.replace(old_hunk.diff_base_byte_range.end);
925 old_cursor.next();
926 }
927 }
928 }
929 (Some(new_hunk), None) => {
930 start.get_or_insert(new_hunk.buffer_range.start);
931 base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start);
932 // TODO(cole) it seems like this could move end backward?
933 end.replace(new_hunk.buffer_range.end);
934 base_text_end = base_text_end.max(Some(new_hunk.diff_base_byte_range.end));
935 new_cursor.next();
936 }
937 (None, Some(old_hunk)) => {
938 start.get_or_insert(old_hunk.buffer_range.start);
939 base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start);
940 // TODO(cole) it seems like this could move end backward?
941 end.replace(old_hunk.buffer_range.end);
942 base_text_end = base_text_end.max(Some(old_hunk.diff_base_byte_range.end));
943 old_cursor.next();
944 }
945 (None, None) => break,
946 }
947 }
948
949 (
950 start.zip(end).map(|(start, end)| start..end),
951 base_text_start
952 .zip(base_text_end)
953 .map(|(start, end)| start..end),
954 )
955}
956
957fn process_patch_hunk(
958 patch: &GitPatch<'_>,
959 hunk_index: usize,
960 diff_base: &Rope,
961 buffer: &text::BufferSnapshot,
962 buffer_row_divergence: &mut i64,
963 diff_options: Option<&DiffOptions>,
964) -> InternalDiffHunk {
965 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
966 assert!(line_item_count > 0);
967
968 let mut first_deletion_buffer_row: Option<u32> = None;
969 let mut buffer_row_range: Option<Range<u32>> = None;
970 let mut diff_base_byte_range: Option<Range<usize>> = None;
971 let mut first_addition_old_row: Option<u32> = None;
972
973 for line_index in 0..line_item_count {
974 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
975 let kind = line.origin_value();
976 let content_offset = line.content_offset() as isize;
977 let content_len = line.content().len() as isize;
978 match kind {
979 GitDiffLineType::Addition => {
980 if first_addition_old_row.is_none() {
981 first_addition_old_row = Some(
982 (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
983 );
984 }
985 *buffer_row_divergence += 1;
986 let row = line.new_lineno().unwrap().saturating_sub(1);
987
988 match &mut buffer_row_range {
989 Some(Range { end, .. }) => *end = row + 1,
990 None => buffer_row_range = Some(row..row + 1),
991 }
992 }
993 GitDiffLineType::Deletion => {
994 let end = content_offset + content_len;
995
996 match &mut diff_base_byte_range {
997 Some(head_byte_range) => head_byte_range.end = end as usize,
998 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
999 }
1000
1001 if first_deletion_buffer_row.is_none() {
1002 let old_row = line.old_lineno().unwrap().saturating_sub(1);
1003 let row = old_row as i64 + *buffer_row_divergence;
1004 first_deletion_buffer_row = Some(row as u32);
1005 }
1006
1007 *buffer_row_divergence -= 1;
1008 }
1009 _ => {}
1010 }
1011 }
1012
1013 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
1014 // Pure deletion hunk without addition.
1015 let row = first_deletion_buffer_row.unwrap();
1016 row..row
1017 });
1018 let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
1019 // Pure addition hunk without deletion.
1020 let row = first_addition_old_row.unwrap();
1021 let offset = diff_base.point_to_offset(Point::new(row, 0));
1022 offset..offset
1023 });
1024
1025 let start = Point::new(buffer_row_range.start, 0);
1026 let end = Point::new(buffer_row_range.end, 0);
1027 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
1028
1029 let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
1030
1031 let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
1032 && !buffer_row_range.is_empty()
1033 && base_line_count == buffer_row_range.len()
1034 && diff_options.max_word_diff_line_count >= base_line_count
1035 {
1036 let base_text: String = diff_base
1037 .chunks_in_range(diff_base_byte_range.clone())
1038 .collect();
1039
1040 let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
1041
1042 let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
1043 &base_text,
1044 &buffer_text,
1045 DiffOptions {
1046 language_scope: diff_options.language_scope.clone(),
1047 ..*diff_options
1048 },
1049 );
1050
1051 let buffer_start_offset = buffer_range.start.to_offset(buffer);
1052 let buffer_word_diffs = buffer_word_diffs_relative
1053 .into_iter()
1054 .map(|range| {
1055 let start = buffer.anchor_after(buffer_start_offset + range.start);
1056 let end = buffer.anchor_after(buffer_start_offset + range.end);
1057 start..end
1058 })
1059 .collect();
1060
1061 (base_word_diffs, buffer_word_diffs)
1062 } else {
1063 (Vec::default(), Vec::default())
1064 };
1065
1066 InternalDiffHunk {
1067 buffer_range,
1068 diff_base_byte_range,
1069 base_word_diffs,
1070 buffer_word_diffs,
1071 }
1072}
1073
1074impl std::fmt::Debug for BufferDiff {
1075 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076 f.debug_struct("BufferChangeSet")
1077 .field("buffer_id", &self.buffer_id)
1078 .finish()
1079 }
1080}
1081
1082#[derive(Clone, Debug)]
1083pub enum BufferDiffEvent {
1084 DiffChanged {
1085 changed_range: Option<Range<text::Anchor>>,
1086 base_text_changed_range: Option<Range<usize>>,
1087 },
1088 LanguageChanged,
1089 HunksStagedOrUnstaged(Option<Rope>),
1090}
1091
1092impl EventEmitter<BufferDiffEvent> for BufferDiff {}
1093
1094impl BufferDiff {
1095 pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
1096 let base_text = cx.new(|cx| {
1097 let mut buffer = language::Buffer::local("", cx);
1098 buffer.set_capability(Capability::ReadOnly, cx);
1099 buffer
1100 });
1101
1102 BufferDiff {
1103 buffer_id: buffer.remote_id(),
1104 inner: BufferDiffInner {
1105 base_text,
1106 hunks: SumTree::new(buffer),
1107 pending_hunks: SumTree::new(buffer),
1108 base_text_exists: false,
1109 },
1110 secondary_diff: None,
1111 }
1112 }
1113
1114 pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context<Self>) -> Self {
1115 let base_text = buffer.text();
1116 let base_text = cx.new(|cx| {
1117 let mut buffer = language::Buffer::local(base_text, cx);
1118 buffer.set_capability(Capability::ReadOnly, cx);
1119 buffer
1120 });
1121
1122 BufferDiff {
1123 buffer_id: buffer.remote_id(),
1124 inner: BufferDiffInner {
1125 base_text,
1126 hunks: SumTree::new(buffer),
1127 pending_hunks: SumTree::new(buffer),
1128 base_text_exists: true,
1129 },
1130 secondary_diff: None,
1131 }
1132 }
1133
1134 #[cfg(any(test, feature = "test-support"))]
1135 pub fn new_with_base_text(
1136 base_text: &str,
1137 buffer: &text::BufferSnapshot,
1138 cx: &mut Context<Self>,
1139 ) -> Self {
1140 let mut this = BufferDiff::new(&buffer, cx);
1141 let executor = cx.background_executor().clone();
1142 let mut base_text = base_text.to_owned();
1143 text::LineEnding::normalize(&mut base_text);
1144 let inner = executor.block(this.update_diff(
1145 buffer.clone(),
1146 Some(Arc::from(base_text)),
1147 true,
1148 None,
1149 cx,
1150 ));
1151 this.set_snapshot(inner, &buffer, cx);
1152 this
1153 }
1154
1155 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
1156 self.secondary_diff = Some(diff);
1157 }
1158
1159 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
1160 self.secondary_diff.clone()
1161 }
1162
1163 pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
1164 if self.secondary_diff.is_some() {
1165 self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
1166 buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
1167 diff_base_byte_range: 0..0,
1168 });
1169 cx.emit(BufferDiffEvent::DiffChanged {
1170 changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)),
1171 base_text_changed_range: Some(0..self.base_text(cx).len()),
1172 });
1173 }
1174 }
1175
1176 pub fn stage_or_unstage_hunks(
1177 &mut self,
1178 stage: bool,
1179 hunks: &[DiffHunk],
1180 buffer: &text::BufferSnapshot,
1181 file_exists: bool,
1182 cx: &mut Context<Self>,
1183 ) -> Option<Rope> {
1184 let new_index_text = self
1185 .secondary_diff
1186 .as_ref()?
1187 .update(cx, |secondary_diff, cx| {
1188 self.inner.stage_or_unstage_hunks_impl(
1189 &secondary_diff.inner,
1190 stage,
1191 hunks,
1192 buffer,
1193 file_exists,
1194 cx,
1195 )
1196 });
1197
1198 cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
1199 new_index_text.clone(),
1200 ));
1201 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1202 let changed_range = first.buffer_range.start..last.buffer_range.end;
1203 let base_text_changed_range =
1204 first.diff_base_byte_range.start..last.diff_base_byte_range.end;
1205 cx.emit(BufferDiffEvent::DiffChanged {
1206 changed_range: Some(changed_range),
1207 base_text_changed_range: Some(base_text_changed_range),
1208 });
1209 }
1210 new_index_text
1211 }
1212
1213 pub fn stage_or_unstage_all_hunks(
1214 &mut self,
1215 stage: bool,
1216 buffer: &text::BufferSnapshot,
1217 file_exists: bool,
1218 cx: &mut Context<Self>,
1219 ) {
1220 let hunks = self
1221 .snapshot(cx)
1222 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
1223 .collect::<Vec<_>>();
1224 let Some(secondary) = self.secondary_diff.clone() else {
1225 return;
1226 };
1227 let secondary = secondary.read(cx).inner.clone();
1228 self.inner
1229 .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx);
1230 if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1231 let changed_range = first.buffer_range.start..last.buffer_range.end;
1232 let base_text_changed_range =
1233 first.diff_base_byte_range.start..last.diff_base_byte_range.end;
1234 cx.emit(BufferDiffEvent::DiffChanged {
1235 changed_range: Some(changed_range),
1236 base_text_changed_range: Some(base_text_changed_range),
1237 });
1238 }
1239 }
1240
1241 pub fn update_diff(
1242 &self,
1243 buffer: text::BufferSnapshot,
1244 base_text: Option<Arc<str>>,
1245 base_text_changed: bool,
1246 language: Option<Arc<Language>>,
1247 cx: &App,
1248 ) -> Task<BufferDiffUpdate> {
1249 let prev_base_text = self.base_text(cx).as_rope().clone();
1250 let diff_options = build_diff_options(
1251 None,
1252 language.as_ref().map(|l| l.name()),
1253 language.as_ref().map(|l| l.default_scope()),
1254 cx,
1255 );
1256
1257 cx.background_executor()
1258 .spawn_labeled(*CALCULATE_DIFF_TASK, async move {
1259 let base_text_rope = if let Some(base_text) = &base_text {
1260 if base_text_changed {
1261 Rope::from(base_text.as_ref())
1262 } else {
1263 prev_base_text
1264 }
1265 } else {
1266 Rope::new()
1267 };
1268 let base_text_exists = base_text.is_some();
1269 let hunks = compute_hunks(
1270 base_text
1271 .clone()
1272 .map(|base_text| (base_text, base_text_rope.clone())),
1273 buffer.clone(),
1274 diff_options,
1275 );
1276 let base_text = base_text.unwrap_or_default();
1277 let inner = BufferDiffInner {
1278 base_text,
1279 hunks,
1280 base_text_exists,
1281 pending_hunks: SumTree::new(&buffer),
1282 };
1283 BufferDiffUpdate {
1284 inner,
1285 base_text_changed,
1286 }
1287 })
1288 }
1289
1290 pub fn language_changed(
1291 &mut self,
1292 language: Option<Arc<Language>>,
1293 language_registry: Option<Arc<LanguageRegistry>>,
1294 cx: &mut Context<Self>,
1295 ) {
1296 self.inner.base_text.update(cx, |base_text, cx| {
1297 base_text.set_language(language, cx);
1298 if let Some(language_registry) = language_registry {
1299 base_text.set_language_registry(language_registry);
1300 }
1301 });
1302 cx.emit(BufferDiffEvent::LanguageChanged);
1303 }
1304
1305 pub fn set_snapshot(
1306 &mut self,
1307 new_state: BufferDiffUpdate,
1308 buffer: &text::BufferSnapshot,
1309 cx: &mut Context<Self>,
1310 ) -> Option<Range<Anchor>> {
1311 self.set_snapshot_with_secondary(new_state, buffer, None, false, cx)
1312 }
1313
1314 pub fn set_snapshot_with_secondary(
1315 &mut self,
1316 update: BufferDiffUpdate,
1317 buffer: &text::BufferSnapshot,
1318 secondary_diff_change: Option<Range<Anchor>>,
1319 clear_pending_hunks: bool,
1320 cx: &mut Context<Self>,
1321 ) -> Option<Range<Anchor>> {
1322 log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1323
1324 let old_snapshot = self.snapshot(cx);
1325 let state = &mut self.inner;
1326 let new_state = update.inner;
1327 let (mut changed_range, mut base_text_changed_range) =
1328 match (state.base_text_exists, new_state.base_text_exists) {
1329 (false, false) => (None, None),
1330 (true, true) if !update.base_text_changed => {
1331 compare_hunks(&new_state.hunks, &old_snapshot.inner.hunks, buffer)
1332 }
1333 _ => (
1334 Some(text::Anchor::min_max_range_for_buffer(self.buffer_id)),
1335 Some(0..new_state.base_text.len()),
1336 ),
1337 };
1338
1339 if let Some(secondary_changed_range) = secondary_diff_change
1340 && let (Some(secondary_hunk_range), Some(secondary_base_range)) =
1341 old_snapshot.range_to_hunk_range(secondary_changed_range, buffer)
1342 {
1343 if let Some(range) = &mut changed_range {
1344 range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1345 range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1346 } else {
1347 changed_range = Some(secondary_hunk_range);
1348 }
1349
1350 if let Some(base_text_range) = &mut base_text_changed_range {
1351 base_text_range.start = secondary_base_range.start.min(base_text_range.start);
1352 base_text_range.end = secondary_base_range.end.max(base_text_range.end);
1353 } else {
1354 base_text_changed_range = Some(secondary_base_range);
1355 }
1356 }
1357
1358 let state = &mut self.inner;
1359 state.base_text_exists = new_state.base_text_exists;
1360 if update.base_text_changed {
1361 state.base_text.update(cx, |base_text, cx| {
1362 base_text.set_capability(Capability::ReadWrite, cx);
1363 base_text.set_text(new_state.base_text.clone(), cx);
1364 base_text.set_capability(Capability::ReadOnly, cx);
1365 })
1366 }
1367 state.hunks = new_state.hunks;
1368 if update.base_text_changed || clear_pending_hunks {
1369 if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1370 {
1371 if let Some(range) = &mut changed_range {
1372 range.start = *range.start.min(&first.buffer_range.start, buffer);
1373 range.end = *range.end.max(&last.buffer_range.end, buffer);
1374 } else {
1375 changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1376 }
1377
1378 if let Some(base_text_range) = &mut base_text_changed_range {
1379 base_text_range.start =
1380 base_text_range.start.min(first.diff_base_byte_range.start);
1381 base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end);
1382 } else {
1383 base_text_changed_range =
1384 Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1385 }
1386 }
1387 state.pending_hunks = SumTree::new(buffer);
1388 }
1389
1390 cx.emit(BufferDiffEvent::DiffChanged {
1391 changed_range: changed_range.clone(),
1392 base_text_changed_range,
1393 });
1394 changed_range
1395 }
1396
1397 pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
1398 self.inner.base_text.read(cx).snapshot()
1399 }
1400
1401 pub fn base_text_exists(&self) -> bool {
1402 self.inner.base_text_exists
1403 }
1404
1405 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1406 BufferDiffSnapshot {
1407 inner: BufferDiffInner {
1408 hunks: self.inner.hunks.clone(),
1409 pending_hunks: self.inner.pending_hunks.clone(),
1410 base_text: self.inner.base_text.read(cx).snapshot(),
1411 base_text_exists: self.inner.base_text_exists,
1412 },
1413 secondary_diff: self
1414 .secondary_diff
1415 .as_ref()
1416 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
1417 }
1418 }
1419
1420 /// Used in cases where the change set isn't derived from git.
1421 pub fn set_base_text(
1422 &mut self,
1423 base_text: Option<Arc<str>>,
1424 language: Option<Arc<Language>>,
1425 buffer: text::BufferSnapshot,
1426 cx: &mut Context<Self>,
1427 ) -> oneshot::Receiver<()> {
1428 let (tx, rx) = oneshot::channel();
1429 let complete_on_drop = util::defer(|| {
1430 tx.send(()).ok();
1431 });
1432 cx.spawn(async move |this, cx| {
1433 let Some(state) = this
1434 .update(cx, |this, cx| {
1435 this.update_diff(buffer.clone(), base_text, true, language, cx)
1436 })
1437 .log_err()
1438 else {
1439 return;
1440 };
1441 let state = state.await;
1442 this.update(cx, |this, cx| {
1443 this.set_snapshot(state, &buffer, cx);
1444 })
1445 .log_err();
1446 drop(complete_on_drop)
1447 })
1448 .detach();
1449 rx
1450 }
1451
1452 pub fn base_text_string(&self, cx: &App) -> Option<String> {
1453 self.inner
1454 .base_text_exists
1455 .then(|| self.inner.base_text.read(cx).text())
1456 }
1457
1458 #[cfg(any(test, feature = "test-support"))]
1459 pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) {
1460 let language = self.base_text(cx).language().cloned();
1461 let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
1462 let fut = self.update_diff(buffer.clone(), base_text, false, language, cx);
1463 let snapshot = cx.background_executor().block(fut);
1464 self.set_snapshot(snapshot, &buffer, cx);
1465 }
1466
1467 pub fn base_text_buffer(&self) -> Entity<language::Buffer> {
1468 self.inner.base_text.clone()
1469 }
1470}
1471
1472impl DiffHunk {
1473 pub fn is_created_file(&self) -> bool {
1474 self.diff_base_byte_range == (0..0)
1475 && self.buffer_range.start.is_min()
1476 && self.buffer_range.end.is_max()
1477 }
1478
1479 pub fn status(&self) -> DiffHunkStatus {
1480 let kind = if self.buffer_range.start == self.buffer_range.end {
1481 DiffHunkStatusKind::Deleted
1482 } else if self.diff_base_byte_range.is_empty() {
1483 DiffHunkStatusKind::Added
1484 } else {
1485 DiffHunkStatusKind::Modified
1486 };
1487 DiffHunkStatus {
1488 kind,
1489 secondary: self.secondary_status,
1490 }
1491 }
1492}
1493
1494impl DiffHunkStatus {
1495 pub fn has_secondary_hunk(&self) -> bool {
1496 matches!(
1497 self.secondary,
1498 DiffHunkSecondaryStatus::HasSecondaryHunk
1499 | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1500 | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
1501 )
1502 }
1503
1504 pub fn is_pending(&self) -> bool {
1505 matches!(
1506 self.secondary,
1507 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1508 | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1509 )
1510 }
1511
1512 pub fn is_deleted(&self) -> bool {
1513 self.kind == DiffHunkStatusKind::Deleted
1514 }
1515
1516 pub fn is_added(&self) -> bool {
1517 self.kind == DiffHunkStatusKind::Added
1518 }
1519
1520 pub fn is_modified(&self) -> bool {
1521 self.kind == DiffHunkStatusKind::Modified
1522 }
1523
1524 pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
1525 Self {
1526 kind: DiffHunkStatusKind::Added,
1527 secondary,
1528 }
1529 }
1530
1531 pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
1532 Self {
1533 kind: DiffHunkStatusKind::Modified,
1534 secondary,
1535 }
1536 }
1537
1538 pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
1539 Self {
1540 kind: DiffHunkStatusKind::Deleted,
1541 secondary,
1542 }
1543 }
1544
1545 pub fn deleted_none() -> Self {
1546 Self {
1547 kind: DiffHunkStatusKind::Deleted,
1548 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1549 }
1550 }
1551
1552 pub fn added_none() -> Self {
1553 Self {
1554 kind: DiffHunkStatusKind::Added,
1555 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1556 }
1557 }
1558
1559 pub fn modified_none() -> Self {
1560 Self {
1561 kind: DiffHunkStatusKind::Modified,
1562 secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1563 }
1564 }
1565}
1566
1567#[cfg(any(test, feature = "test-support"))]
1568#[track_caller]
1569pub fn assert_hunks<ExpectedText, HunkIter>(
1570 diff_hunks: HunkIter,
1571 buffer: &text::BufferSnapshot,
1572 diff_base: &str,
1573 // Line range, deleted, added, status
1574 expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
1575) where
1576 HunkIter: Iterator<Item = DiffHunk>,
1577 ExpectedText: AsRef<str>,
1578{
1579 let actual_hunks = diff_hunks
1580 .map(|hunk| {
1581 (
1582 hunk.range.clone(),
1583 &diff_base[hunk.diff_base_byte_range.clone()],
1584 buffer
1585 .text_for_range(hunk.range.clone())
1586 .collect::<String>(),
1587 hunk.status(),
1588 )
1589 })
1590 .collect::<Vec<_>>();
1591
1592 let expected_hunks: Vec<_> = expected_hunks
1593 .iter()
1594 .map(|(line_range, deleted_text, added_text, status)| {
1595 (
1596 Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
1597 deleted_text.as_ref(),
1598 added_text.as_ref().to_string(),
1599 *status,
1600 )
1601 })
1602 .collect();
1603
1604 pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
1605}
1606
1607#[cfg(test)]
1608mod tests {
1609 use std::{fmt::Write as _, sync::mpsc};
1610
1611 use super::*;
1612 use gpui::TestAppContext;
1613 use pretty_assertions::{assert_eq, assert_ne};
1614 use rand::{Rng as _, rngs::StdRng};
1615 use text::{Buffer, BufferId, ReplicaId, Rope};
1616 use unindent::Unindent as _;
1617 use util::test::marked_text_ranges;
1618
1619 #[ctor::ctor]
1620 fn init_logger() {
1621 zlog::init_test();
1622 }
1623
1624 #[gpui::test]
1625 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1626 let diff_base = "
1627 one
1628 two
1629 three
1630 "
1631 .unindent();
1632
1633 let buffer_text = "
1634 one
1635 HELLO
1636 three
1637 "
1638 .unindent();
1639
1640 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1641 let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1642 assert_hunks(
1643 diff.hunks_intersecting_range(
1644 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1645 &buffer,
1646 ),
1647 &buffer,
1648 &diff_base,
1649 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
1650 );
1651
1652 buffer.edit([(0..0, "point five\n")]);
1653 diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1654 assert_hunks(
1655 diff.hunks_intersecting_range(
1656 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1657 &buffer,
1658 ),
1659 &buffer,
1660 &diff_base,
1661 &[
1662 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
1663 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
1664 ],
1665 );
1666
1667 diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
1668 assert_hunks::<&str, _>(
1669 diff.hunks_intersecting_range(
1670 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1671 &buffer,
1672 ),
1673 &buffer,
1674 &diff_base,
1675 &[],
1676 );
1677 }
1678
1679 #[gpui::test]
1680 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1681 let head_text = "
1682 zero
1683 one
1684 two
1685 three
1686 four
1687 five
1688 six
1689 seven
1690 eight
1691 nine
1692 "
1693 .unindent();
1694
1695 let index_text = "
1696 zero
1697 one
1698 TWO
1699 three
1700 FOUR
1701 five
1702 six
1703 seven
1704 eight
1705 NINE
1706 "
1707 .unindent();
1708
1709 let buffer_text = "
1710 zero
1711 one
1712 TWO
1713 three
1714 FOUR
1715 FIVE
1716 six
1717 SEVEN
1718 eight
1719 nine
1720 "
1721 .unindent();
1722
1723 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1724 let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1725 let mut uncommitted_diff =
1726 BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1727 uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
1728
1729 let expected_hunks = vec![
1730 (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
1731 (
1732 4..6,
1733 "four\nfive\n",
1734 "FOUR\nFIVE\n",
1735 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1736 ),
1737 (
1738 7..8,
1739 "seven\n",
1740 "SEVEN\n",
1741 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1742 ),
1743 ];
1744
1745 assert_hunks(
1746 uncommitted_diff.hunks_intersecting_range(
1747 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1748 &buffer,
1749 ),
1750 &buffer,
1751 &head_text,
1752 &expected_hunks,
1753 );
1754 }
1755
1756 #[gpui::test]
1757 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1758 let diff_base = "
1759 one
1760 two
1761 three
1762 four
1763 five
1764 six
1765 seven
1766 eight
1767 nine
1768 ten
1769 "
1770 .unindent();
1771
1772 let buffer_text = "
1773 A
1774 one
1775 B
1776 two
1777 C
1778 three
1779 HELLO
1780 four
1781 five
1782 SIXTEEN
1783 seven
1784 eight
1785 WORLD
1786 nine
1787
1788 ten
1789
1790 "
1791 .unindent();
1792
1793 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
1794 let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx);
1795 assert_eq!(
1796 diff.hunks_intersecting_range(
1797 Anchor::min_max_range_for_buffer(buffer.remote_id()),
1798 &buffer
1799 )
1800 .count(),
1801 8
1802 );
1803
1804 assert_hunks(
1805 diff.hunks_intersecting_range(
1806 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1807 &buffer,
1808 ),
1809 &buffer,
1810 &diff_base,
1811 &[
1812 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
1813 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
1814 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
1815 ],
1816 );
1817 }
1818
1819 #[gpui::test]
1820 async fn test_stage_hunk(cx: &mut TestAppContext) {
1821 struct Example {
1822 name: &'static str,
1823 head_text: String,
1824 index_text: String,
1825 buffer_marked_text: String,
1826 final_index_text: String,
1827 }
1828
1829 let table = [
1830 Example {
1831 name: "uncommitted hunk straddles end of unstaged hunk",
1832 head_text: "
1833 one
1834 two
1835 three
1836 four
1837 five
1838 "
1839 .unindent(),
1840 index_text: "
1841 one
1842 TWO_HUNDRED
1843 three
1844 FOUR_HUNDRED
1845 five
1846 "
1847 .unindent(),
1848 buffer_marked_text: "
1849 ZERO
1850 one
1851 two
1852 «THREE_HUNDRED
1853 FOUR_HUNDRED»
1854 five
1855 SIX
1856 "
1857 .unindent(),
1858 final_index_text: "
1859 one
1860 two
1861 THREE_HUNDRED
1862 FOUR_HUNDRED
1863 five
1864 "
1865 .unindent(),
1866 },
1867 Example {
1868 name: "uncommitted hunk straddles start of unstaged hunk",
1869 head_text: "
1870 one
1871 two
1872 three
1873 four
1874 five
1875 "
1876 .unindent(),
1877 index_text: "
1878 one
1879 TWO_HUNDRED
1880 three
1881 FOUR_HUNDRED
1882 five
1883 "
1884 .unindent(),
1885 buffer_marked_text: "
1886 ZERO
1887 one
1888 «TWO_HUNDRED
1889 THREE_HUNDRED»
1890 four
1891 five
1892 SIX
1893 "
1894 .unindent(),
1895 final_index_text: "
1896 one
1897 TWO_HUNDRED
1898 THREE_HUNDRED
1899 four
1900 five
1901 "
1902 .unindent(),
1903 },
1904 Example {
1905 name: "uncommitted hunk strictly contains unstaged hunks",
1906 head_text: "
1907 one
1908 two
1909 three
1910 four
1911 five
1912 six
1913 seven
1914 "
1915 .unindent(),
1916 index_text: "
1917 one
1918 TWO
1919 THREE
1920 FOUR
1921 FIVE
1922 SIX
1923 seven
1924 "
1925 .unindent(),
1926 buffer_marked_text: "
1927 one
1928 TWO
1929 «THREE_HUNDRED
1930 FOUR
1931 FIVE_HUNDRED»
1932 SIX
1933 seven
1934 "
1935 .unindent(),
1936 final_index_text: "
1937 one
1938 TWO
1939 THREE_HUNDRED
1940 FOUR
1941 FIVE_HUNDRED
1942 SIX
1943 seven
1944 "
1945 .unindent(),
1946 },
1947 Example {
1948 name: "uncommitted deletion hunk",
1949 head_text: "
1950 one
1951 two
1952 three
1953 four
1954 five
1955 "
1956 .unindent(),
1957 index_text: "
1958 one
1959 two
1960 three
1961 four
1962 five
1963 "
1964 .unindent(),
1965 buffer_marked_text: "
1966 one
1967 ˇfive
1968 "
1969 .unindent(),
1970 final_index_text: "
1971 one
1972 five
1973 "
1974 .unindent(),
1975 },
1976 Example {
1977 name: "one unstaged hunk that contains two uncommitted hunks",
1978 head_text: "
1979 one
1980 two
1981
1982 three
1983 four
1984 "
1985 .unindent(),
1986 index_text: "
1987 one
1988 two
1989 three
1990 four
1991 "
1992 .unindent(),
1993 buffer_marked_text: "
1994 «one
1995
1996 three // modified
1997 four»
1998 "
1999 .unindent(),
2000 final_index_text: "
2001 one
2002
2003 three // modified
2004 four
2005 "
2006 .unindent(),
2007 },
2008 Example {
2009 name: "one uncommitted hunk that contains two unstaged hunks",
2010 head_text: "
2011 one
2012 two
2013 three
2014 four
2015 five
2016 "
2017 .unindent(),
2018 index_text: "
2019 ZERO
2020 one
2021 TWO
2022 THREE
2023 FOUR
2024 five
2025 "
2026 .unindent(),
2027 buffer_marked_text: "
2028 «one
2029 TWO_HUNDRED
2030 THREE
2031 FOUR_HUNDRED
2032 five»
2033 "
2034 .unindent(),
2035 final_index_text: "
2036 ZERO
2037 one
2038 TWO_HUNDRED
2039 THREE
2040 FOUR_HUNDRED
2041 five
2042 "
2043 .unindent(),
2044 },
2045 ];
2046
2047 for example in table {
2048 let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2049 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2050 let hunk_range =
2051 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2052
2053 let unstaged_diff =
2054 cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
2055
2056 let uncommitted_diff = cx.new(|cx| {
2057 let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx);
2058 diff.set_secondary_diff(unstaged_diff);
2059 diff
2060 });
2061
2062 uncommitted_diff.update(cx, |diff, cx| {
2063 let hunks = diff
2064 .snapshot(cx)
2065 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2066 .collect::<Vec<_>>();
2067 for hunk in &hunks {
2068 assert_ne!(
2069 hunk.secondary_status,
2070 DiffHunkSecondaryStatus::NoSecondaryHunk
2071 )
2072 }
2073
2074 let new_index_text = diff
2075 .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2076 .unwrap()
2077 .to_string();
2078
2079 let hunks = diff
2080 .snapshot(cx)
2081 .hunks_intersecting_range(hunk_range.clone(), &buffer)
2082 .collect::<Vec<_>>();
2083 for hunk in &hunks {
2084 assert_eq!(
2085 hunk.secondary_status,
2086 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2087 )
2088 }
2089
2090 pretty_assertions::assert_eq!(
2091 new_index_text,
2092 example.final_index_text,
2093 "example: {}",
2094 example.name
2095 );
2096 });
2097 }
2098 }
2099
2100 #[gpui::test]
2101 async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2102 let head_text = "
2103 one
2104 two
2105 three
2106 "
2107 .unindent();
2108 let index_text = head_text.clone();
2109 let buffer_text = "
2110 one
2111 three
2112 "
2113 .unindent();
2114
2115 let buffer = Buffer::new(
2116 ReplicaId::LOCAL,
2117 BufferId::new(1).unwrap(),
2118 buffer_text.clone(),
2119 );
2120 let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2121 let uncommitted_diff = cx.new(|cx| {
2122 let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2123 diff.set_secondary_diff(unstaged_diff.clone());
2124 diff
2125 });
2126
2127 uncommitted_diff.update(cx, |diff, cx| {
2128 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2129
2130 let new_index_text = diff
2131 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
2132 .unwrap()
2133 .to_string();
2134 assert_eq!(new_index_text, buffer_text);
2135
2136 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2137 assert_eq!(
2138 hunk.secondary_status,
2139 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2140 );
2141
2142 let index_text = diff
2143 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
2144 .unwrap()
2145 .to_string();
2146 assert_eq!(index_text, head_text);
2147
2148 let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2149 // optimistically unstaged (fine, could also be HasSecondaryHunk)
2150 assert_eq!(
2151 hunk.secondary_status,
2152 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2153 );
2154 });
2155 }
2156
2157 #[gpui::test]
2158 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
2159 let base_text = "
2160 zero
2161 one
2162 two
2163 three
2164 four
2165 five
2166 six
2167 seven
2168 eight
2169 nine
2170 "
2171 .unindent();
2172
2173 let buffer_text_1 = "
2174 one
2175 three
2176 four
2177 five
2178 SIX
2179 seven
2180 eight
2181 NINE
2182 "
2183 .unindent();
2184
2185 let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
2186
2187 let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2188 let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2189 let (range, base_text_range) =
2190 compare_hunks(&diff_1.inner.hunks, &empty_diff.inner.hunks, &buffer);
2191 let range = range.unwrap();
2192 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
2193 let base_text_range = base_text_range.unwrap();
2194 assert_eq!(
2195 base_text_range.to_point(diff_1.base_text()),
2196 Point::new(0, 0)..Point::new(10, 0)
2197 );
2198
2199 // Edit does affects the diff because it recalculates word diffs.
2200 buffer.edit_via_marked_text(
2201 &"
2202 one
2203 three
2204 four
2205 five
2206 «SIX.5»
2207 seven
2208 eight
2209 NINE
2210 "
2211 .unindent(),
2212 );
2213 let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2214 let (range, base_text_range) =
2215 compare_hunks(&diff_2.inner.hunks, &diff_1.inner.hunks, &buffer);
2216 assert_eq!(
2217 range.unwrap().to_point(&buffer),
2218 Point::new(4, 0)..Point::new(5, 0),
2219 );
2220 assert_eq!(
2221 base_text_range.unwrap().to_point(diff_2.base_text()),
2222 Point::new(6, 0)..Point::new(7, 0),
2223 );
2224
2225 // Edit turns a deletion hunk into a modification.
2226 buffer.edit_via_marked_text(
2227 &"
2228 one
2229 «THREE»
2230 four
2231 five
2232 SIX.5
2233 seven
2234 eight
2235 NINE
2236 "
2237 .unindent(),
2238 );
2239 let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2240 let (range, base_text_range) =
2241 compare_hunks(&diff_3.inner.hunks, &diff_2.inner.hunks, &buffer);
2242 let range = range.unwrap();
2243 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2244 let base_text_range = base_text_range.unwrap();
2245 assert_eq!(
2246 base_text_range.to_point(diff_3.base_text()),
2247 Point::new(2, 0)..Point::new(4, 0)
2248 );
2249
2250 // Edit turns a modification hunk into a deletion.
2251 buffer.edit_via_marked_text(
2252 &"
2253 one
2254 THREE
2255 four
2256 five«»
2257 seven
2258 eight
2259 NINE
2260 "
2261 .unindent(),
2262 );
2263 let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
2264 let (range, base_text_range) =
2265 compare_hunks(&diff_4.inner.hunks, &diff_3.inner.hunks, &buffer);
2266 let range = range.unwrap();
2267 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2268 let base_text_range = base_text_range.unwrap();
2269 assert_eq!(
2270 base_text_range.to_point(diff_4.base_text()),
2271 Point::new(6, 0)..Point::new(7, 0)
2272 );
2273
2274 // Edit introduces a new insertion hunk.
2275 buffer.edit_via_marked_text(
2276 &"
2277 one
2278 THREE
2279 four«
2280 FOUR.5
2281 »five
2282 seven
2283 eight
2284 NINE
2285 "
2286 .unindent(),
2287 );
2288 let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2289 let (range, base_text_range) =
2290 compare_hunks(&diff_5.inner.hunks, &diff_4.inner.hunks, &buffer);
2291 let range = range.unwrap();
2292 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2293 let base_text_range = base_text_range.unwrap();
2294 assert_eq!(
2295 base_text_range.to_point(diff_5.base_text()),
2296 Point::new(5, 0)..Point::new(5, 0)
2297 );
2298
2299 // Edit removes a hunk.
2300 buffer.edit_via_marked_text(
2301 &"
2302 one
2303 THREE
2304 four
2305 FOUR.5
2306 five
2307 seven
2308 eight
2309 «nine»
2310 "
2311 .unindent(),
2312 );
2313 let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
2314 let (range, base_text_range) =
2315 compare_hunks(&diff_6.inner.hunks, &diff_5.inner.hunks, &buffer);
2316 let range = range.unwrap();
2317 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2318 let base_text_range = base_text_range.unwrap();
2319 assert_eq!(
2320 base_text_range.to_point(diff_6.base_text()),
2321 Point::new(9, 0)..Point::new(10, 0)
2322 );
2323 }
2324
2325 #[gpui::test(iterations = 100)]
2326 async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
2327 fn gen_line(rng: &mut StdRng) -> String {
2328 if rng.random_bool(0.2) {
2329 "\n".to_owned()
2330 } else {
2331 let c = rng.random_range('A'..='Z');
2332 format!("{c}{c}{c}\n")
2333 }
2334 }
2335
2336 fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
2337 let mut old_lines = {
2338 let mut old_lines = Vec::new();
2339 let old_lines_iter = head.lines();
2340 for line in old_lines_iter {
2341 assert!(!line.ends_with("\n"));
2342 old_lines.push(line.to_owned());
2343 }
2344 if old_lines.last().is_some_and(|line| line.is_empty()) {
2345 old_lines.pop();
2346 }
2347 old_lines.into_iter()
2348 };
2349 let mut result = String::new();
2350 let unchanged_count = rng.random_range(0..=old_lines.len());
2351 result +=
2352 &old_lines
2353 .by_ref()
2354 .take(unchanged_count)
2355 .fold(String::new(), |mut s, line| {
2356 writeln!(&mut s, "{line}").unwrap();
2357 s
2358 });
2359 while old_lines.len() > 0 {
2360 let deleted_count = rng.random_range(0..=old_lines.len());
2361 let _advance = old_lines
2362 .by_ref()
2363 .take(deleted_count)
2364 .map(|line| line.len() + 1)
2365 .sum::<usize>();
2366 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
2367 let added_count = rng.random_range(minimum_added..=5);
2368 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
2369 result += &addition;
2370
2371 if old_lines.len() > 0 {
2372 let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
2373 if blank_lines == old_lines.len() {
2374 break;
2375 };
2376 let unchanged_count =
2377 rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
2378 result += &old_lines.by_ref().take(unchanged_count).fold(
2379 String::new(),
2380 |mut s, line| {
2381 writeln!(&mut s, "{line}").unwrap();
2382 s
2383 },
2384 );
2385 }
2386 }
2387 result
2388 }
2389
2390 fn uncommitted_diff(
2391 working_copy: &language::BufferSnapshot,
2392 index_text: &Rope,
2393 head_text: String,
2394 cx: &mut TestAppContext,
2395 ) -> Entity<BufferDiff> {
2396 let secondary = cx.new(|cx| {
2397 BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
2398 });
2399 cx.new(|cx| {
2400 let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
2401 diff.secondary_diff = Some(secondary);
2402 diff
2403 })
2404 }
2405
2406 let operations = std::env::var("OPERATIONS")
2407 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2408 .unwrap_or(10);
2409
2410 let rng = &mut rng;
2411 let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
2412 writeln!(&mut s, "{c}{c}{c}").unwrap();
2413 s
2414 });
2415 let working_copy = gen_working_copy(rng, &head_text);
2416 let working_copy = cx.new(|cx| {
2417 language::Buffer::local_normalized(
2418 Rope::from(working_copy.as_str()),
2419 text::LineEnding::default(),
2420 cx,
2421 )
2422 });
2423 let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
2424 let mut index_text = if rng.random() {
2425 Rope::from(head_text.as_str())
2426 } else {
2427 working_copy.as_rope().clone()
2428 };
2429
2430 let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2431 let mut hunks = diff.update(cx, |diff, cx| {
2432 diff.snapshot(cx)
2433 .hunks_intersecting_range(
2434 Anchor::min_max_range_for_buffer(diff.buffer_id),
2435 &working_copy,
2436 )
2437 .collect::<Vec<_>>()
2438 });
2439 if hunks.is_empty() {
2440 return;
2441 }
2442
2443 for _ in 0..operations {
2444 let i = rng.random_range(0..hunks.len());
2445 let hunk = &mut hunks[i];
2446 let hunk_to_change = hunk.clone();
2447 let stage = match hunk.secondary_status {
2448 DiffHunkSecondaryStatus::HasSecondaryHunk => {
2449 hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
2450 true
2451 }
2452 DiffHunkSecondaryStatus::NoSecondaryHunk => {
2453 hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
2454 false
2455 }
2456 _ => unreachable!(),
2457 };
2458
2459 index_text = diff.update(cx, |diff, cx| {
2460 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
2461 .unwrap()
2462 });
2463
2464 diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2465 let found_hunks = diff.update(cx, |diff, cx| {
2466 diff.snapshot(cx)
2467 .hunks_intersecting_range(
2468 Anchor::min_max_range_for_buffer(diff.buffer_id),
2469 &working_copy,
2470 )
2471 .collect::<Vec<_>>()
2472 });
2473 assert_eq!(hunks.len(), found_hunks.len());
2474
2475 for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
2476 assert_eq!(
2477 expected_hunk.buffer_range.to_point(&working_copy),
2478 found_hunk.buffer_range.to_point(&working_copy)
2479 );
2480 assert_eq!(
2481 expected_hunk.diff_base_byte_range,
2482 found_hunk.diff_base_byte_range
2483 );
2484 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
2485 }
2486 hunks = found_hunks;
2487 }
2488 }
2489
2490 #[gpui::test]
2491 async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
2492 let base_text = "
2493 zero
2494 one
2495 two
2496 three
2497 four
2498 five
2499 six
2500 seven
2501 eight
2502 "
2503 .unindent();
2504 let buffer_text = "
2505 zero
2506 ONE
2507 two
2508 NINE
2509 five
2510 seven
2511 "
2512 .unindent();
2513
2514 // zero
2515 // - one
2516 // + ONE
2517 // two
2518 // - three
2519 // - four
2520 // + NINE
2521 // five
2522 // - six
2523 // seven
2524 // + eight
2525
2526 let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2527 let buffer_snapshot = buffer.snapshot();
2528 let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
2529 let expected_results = [
2530 // main buffer row, base text row (right bias), base text row (left bias)
2531 (0, 0, 0),
2532 (1, 2, 1),
2533 (2, 2, 2),
2534 (3, 5, 3),
2535 (4, 5, 5),
2536 (5, 7, 7),
2537 (6, 9, 9),
2538 ];
2539 for (buffer_row, expected_right, expected_left) in expected_results {
2540 assert_eq!(
2541 diff.row_to_base_text_row(buffer_row, Bias::Right, &buffer_snapshot),
2542 expected_right,
2543 "{buffer_row}"
2544 );
2545 assert_eq!(
2546 diff.row_to_base_text_row(buffer_row, Bias::Left, &buffer_snapshot),
2547 expected_left,
2548 "{buffer_row}"
2549 );
2550 }
2551 }
2552
2553 #[gpui::test]
2554 async fn test_changed_ranges(cx: &mut gpui::TestAppContext) {
2555 let base_text = "
2556 one
2557 two
2558 three
2559 four
2560 five
2561 six
2562 "
2563 .unindent();
2564 let buffer_text = "
2565 one
2566 TWO
2567 three
2568 four
2569 FIVE
2570 six
2571 "
2572 .unindent();
2573 let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
2574 let diff = cx.new(|cx| {
2575 BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
2576 });
2577 let (tx, rx) = mpsc::channel();
2578 let subscription =
2579 cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
2580
2581 let snapshot = buffer.update(cx, |buffer, cx| {
2582 buffer.set_text(
2583 "
2584 ONE
2585 TWO
2586 THREE
2587 FOUR
2588 FIVE
2589 SIX
2590 "
2591 .unindent(),
2592 cx,
2593 );
2594 buffer.text_snapshot()
2595 });
2596 let update = diff
2597 .update(cx, |diff, cx| {
2598 diff.update_diff(
2599 snapshot.clone(),
2600 Some(base_text.as_str().into()),
2601 false,
2602 None,
2603 cx,
2604 )
2605 })
2606 .await;
2607 diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx));
2608 cx.run_until_parked();
2609 drop(subscription);
2610 let events = rx.into_iter().collect::<Vec<_>>();
2611 match events.as_slice() {
2612 [
2613 BufferDiffEvent::DiffChanged {
2614 changed_range: _,
2615 base_text_changed_range,
2616 },
2617 ] => {
2618 // TODO(cole) this seems like it should pass but currently fails (see compare_hunks)
2619 // assert_eq!(
2620 // *changed_range,
2621 // Some(Anchor::min_max_range_for_buffer(
2622 // buffer.read_with(cx, |buffer, _| buffer.remote_id())
2623 // ))
2624 // );
2625 assert_eq!(*base_text_changed_range, Some(0..base_text.len()));
2626 }
2627 _ => panic!("unexpected events: {:?}", events),
2628 }
2629 }
2630}