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