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