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