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