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