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