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