1use futures::{channel::oneshot, future::OptionFuture};
2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
3use gpui::{App, AsyncApp, Context, Entity, EventEmitter};
4use language::{Language, LanguageRegistry};
5use rope::Rope;
6use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
7use sum_tree::SumTree;
8use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
9use util::ResultExt;
10
11pub struct BufferDiff {
12 pub buffer_id: BufferId,
13 inner: BufferDiffInner,
14 secondary_diff: Option<Entity<BufferDiff>>,
15}
16
17#[derive(Clone)]
18pub struct BufferDiffSnapshot {
19 inner: BufferDiffInner,
20 secondary_diff: Option<Box<BufferDiffSnapshot>>,
21}
22
23#[derive(Clone)]
24struct BufferDiffInner {
25 hunks: SumTree<InternalDiffHunk>,
26 base_text: Option<language::BufferSnapshot>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum DiffHunkStatus {
31 Added(DiffHunkSecondaryStatus),
32 Modified(DiffHunkSecondaryStatus),
33 Removed(DiffHunkSecondaryStatus),
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum DiffHunkSecondaryStatus {
38 HasSecondaryHunk,
39 OverlapsWithSecondaryHunk,
40 None,
41}
42
43// to stage a hunk:
44// - assume hunk starts out as not staged
45// - hunk exists with the same buffer range in the unstaged diff and the uncommitted diff
46// - we want to construct a "version" of the file that
47// - starts from the index base text
48// - has the single hunk applied to it
49// - the hunk is the one from the UNSTAGED diff, so that the diff base offset range is correct to apply to that diff base
50// - write that new version of the file into the index
51
52// to unstage a hunk
53// - no hunk in the unstaged diff intersects this hunk from the uncommitted diff
54// - we want to compute the hunk that
55// - we can apply to the index text
56// - at the end of applying it,
57
58/// A diff hunk resolved to rows in the buffer.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct DiffHunk {
61 /// The buffer range, expressed in terms of rows.
62 pub row_range: Range<u32>,
63 /// The range in the buffer to which this hunk corresponds.
64 pub buffer_range: Range<Anchor>,
65 /// The range in the buffer's diff base text to which this hunk corresponds.
66 pub diff_base_byte_range: Range<usize>,
67 pub secondary_status: DiffHunkSecondaryStatus,
68}
69
70/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
71#[derive(Debug, Clone, PartialEq, Eq)]
72struct InternalDiffHunk {
73 buffer_range: Range<Anchor>,
74 diff_base_byte_range: Range<usize>,
75}
76
77impl sum_tree::Item for InternalDiffHunk {
78 type Summary = DiffHunkSummary;
79
80 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
81 DiffHunkSummary {
82 buffer_range: self.buffer_range.clone(),
83 }
84 }
85}
86
87#[derive(Debug, Default, Clone)]
88pub struct DiffHunkSummary {
89 buffer_range: Range<Anchor>,
90}
91
92impl sum_tree::Summary for DiffHunkSummary {
93 type Context = text::BufferSnapshot;
94
95 fn zero(_cx: &Self::Context) -> Self {
96 Default::default()
97 }
98
99 fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
100 self.buffer_range.start = self
101 .buffer_range
102 .start
103 .min(&other.buffer_range.start, buffer);
104 self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
105 }
106}
107
108impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor {
109 fn cmp(
110 &self,
111 cursor_location: &DiffHunkSummary,
112 buffer: &text::BufferSnapshot,
113 ) -> cmp::Ordering {
114 self.cmp(&cursor_location.buffer_range.end, buffer)
115 }
116}
117
118impl std::fmt::Debug for BufferDiffInner {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("BufferDiffSnapshot")
121 .field("hunks", &self.hunks)
122 .finish()
123 }
124}
125
126impl BufferDiffSnapshot {
127 pub fn is_empty(&self) -> bool {
128 self.inner.hunks.is_empty()
129 }
130
131 pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
132 self.secondary_diff.as_deref()
133 }
134
135 pub fn hunks_intersecting_range<'a>(
136 &'a self,
137 range: Range<Anchor>,
138 buffer: &'a text::BufferSnapshot,
139 ) -> impl 'a + Iterator<Item = DiffHunk> {
140 let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
141 self.inner
142 .hunks_intersecting_range(range, buffer, unstaged_counterpart)
143 }
144
145 pub fn hunks_intersecting_range_rev<'a>(
146 &'a self,
147 range: Range<Anchor>,
148 buffer: &'a text::BufferSnapshot,
149 ) -> impl 'a + Iterator<Item = DiffHunk> {
150 self.inner.hunks_intersecting_range_rev(range, buffer)
151 }
152
153 pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
154 self.inner.base_text.as_ref()
155 }
156
157 pub fn base_texts_eq(&self, other: &Self) -> bool {
158 match (other.base_text(), self.base_text()) {
159 (None, None) => true,
160 (None, Some(_)) => false,
161 (Some(_), None) => false,
162 (Some(old), Some(new)) => {
163 let (old_id, old_empty) = (old.remote_id(), old.is_empty());
164 let (new_id, new_empty) = (new.remote_id(), new.is_empty());
165 new_id == old_id || (new_empty && old_empty)
166 }
167 }
168 }
169}
170
171impl BufferDiffInner {
172 fn hunks_intersecting_range<'a>(
173 &'a self,
174 range: Range<Anchor>,
175 buffer: &'a text::BufferSnapshot,
176 secondary: Option<&'a Self>,
177 ) -> impl 'a + Iterator<Item = DiffHunk> {
178 let range = range.to_offset(buffer);
179
180 let mut cursor = self
181 .hunks
182 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
183 let summary_range = summary.buffer_range.to_offset(buffer);
184 let before_start = summary_range.end < range.start;
185 let after_end = summary_range.start > range.end;
186 !before_start && !after_end
187 });
188
189 let anchor_iter = iter::from_fn(move || {
190 cursor.next(buffer);
191 cursor.item()
192 })
193 .flat_map(move |hunk| {
194 [
195 (
196 &hunk.buffer_range.start,
197 (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
198 ),
199 (
200 &hunk.buffer_range.end,
201 (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
202 ),
203 ]
204 });
205
206 let mut secondary_cursor = secondary.as_ref().map(|diff| {
207 let mut cursor = diff.hunks.cursor::<DiffHunkSummary>(buffer);
208 cursor.next(buffer);
209 cursor
210 });
211
212 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
213 iter::from_fn(move || loop {
214 let (start_point, (start_anchor, start_base)) = summaries.next()?;
215 let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
216
217 if !start_anchor.is_valid(buffer) {
218 continue;
219 }
220
221 if end_point.column > 0 {
222 end_point.row += 1;
223 end_point.column = 0;
224 end_anchor = buffer.anchor_before(end_point);
225 }
226
227 let mut secondary_status = DiffHunkSecondaryStatus::None;
228 if let Some(secondary_cursor) = secondary_cursor.as_mut() {
229 if start_anchor
230 .cmp(&secondary_cursor.start().buffer_range.start, buffer)
231 .is_gt()
232 {
233 secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer);
234 }
235
236 if let Some(secondary_hunk) = secondary_cursor.item() {
237 let secondary_range = secondary_hunk.buffer_range.to_point(buffer);
238 if secondary_range == (start_point..end_point) {
239 secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
240 } else if secondary_range.start <= end_point {
241 secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
242 }
243 }
244 }
245
246 return Some(DiffHunk {
247 row_range: start_point.row..end_point.row,
248 diff_base_byte_range: start_base..end_base,
249 buffer_range: start_anchor..end_anchor,
250 secondary_status,
251 });
252 })
253 }
254
255 fn hunks_intersecting_range_rev<'a>(
256 &'a self,
257 range: Range<Anchor>,
258 buffer: &'a text::BufferSnapshot,
259 ) -> impl 'a + Iterator<Item = DiffHunk> {
260 let mut cursor = self
261 .hunks
262 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
263 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
264 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
265 !before_start && !after_end
266 });
267
268 iter::from_fn(move || {
269 cursor.prev(buffer);
270
271 let hunk = cursor.item()?;
272 let range = hunk.buffer_range.to_point(buffer);
273 let end_row = if range.end.column > 0 {
274 range.end.row + 1
275 } else {
276 range.end.row
277 };
278
279 Some(DiffHunk {
280 row_range: range.start.row..end_row,
281 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
282 buffer_range: hunk.buffer_range.clone(),
283 // The secondary status is not used by callers of this method.
284 secondary_status: DiffHunkSecondaryStatus::None,
285 })
286 })
287 }
288
289 fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
290 let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
291 let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
292 old_cursor.next(new_snapshot);
293 new_cursor.next(new_snapshot);
294 let mut start = None;
295 let mut end = None;
296
297 loop {
298 match (new_cursor.item(), old_cursor.item()) {
299 (Some(new_hunk), Some(old_hunk)) => {
300 match new_hunk
301 .buffer_range
302 .start
303 .cmp(&old_hunk.buffer_range.start, new_snapshot)
304 {
305 cmp::Ordering::Less => {
306 start.get_or_insert(new_hunk.buffer_range.start);
307 end.replace(new_hunk.buffer_range.end);
308 new_cursor.next(new_snapshot);
309 }
310 cmp::Ordering::Equal => {
311 if new_hunk != old_hunk {
312 start.get_or_insert(new_hunk.buffer_range.start);
313 if old_hunk
314 .buffer_range
315 .end
316 .cmp(&new_hunk.buffer_range.end, new_snapshot)
317 .is_ge()
318 {
319 end.replace(old_hunk.buffer_range.end);
320 } else {
321 end.replace(new_hunk.buffer_range.end);
322 }
323 }
324
325 new_cursor.next(new_snapshot);
326 old_cursor.next(new_snapshot);
327 }
328 cmp::Ordering::Greater => {
329 start.get_or_insert(old_hunk.buffer_range.start);
330 end.replace(old_hunk.buffer_range.end);
331 old_cursor.next(new_snapshot);
332 }
333 }
334 }
335 (Some(new_hunk), None) => {
336 start.get_or_insert(new_hunk.buffer_range.start);
337 end.replace(new_hunk.buffer_range.end);
338 new_cursor.next(new_snapshot);
339 }
340 (None, Some(old_hunk)) => {
341 start.get_or_insert(old_hunk.buffer_range.start);
342 end.replace(old_hunk.buffer_range.end);
343 old_cursor.next(new_snapshot);
344 }
345 (None, None) => break,
346 }
347 }
348
349 start.zip(end).map(|(start, end)| start..end)
350 }
351}
352
353fn compute_hunks(
354 diff_base: Option<Arc<String>>,
355 buffer: text::BufferSnapshot,
356) -> SumTree<InternalDiffHunk> {
357 let mut tree = SumTree::new(&buffer);
358
359 if let Some(diff_base) = diff_base {
360 let buffer_text = buffer.as_rope().to_string();
361
362 let mut options = GitOptions::default();
363 options.context_lines(0);
364 let patch = GitPatch::from_buffers(
365 diff_base.as_bytes(),
366 None,
367 buffer_text.as_bytes(),
368 None,
369 Some(&mut options),
370 )
371 .log_err();
372
373 // A common case in Zed is that the empty buffer is represented as just a newline,
374 // but if we just compute a naive diff you get a "preserved" line in the middle,
375 // which is a bit odd.
376 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
377 tree.push(
378 InternalDiffHunk {
379 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
380 diff_base_byte_range: 0..diff_base.len() - 1,
381 },
382 &buffer,
383 );
384 return tree;
385 }
386
387 if let Some(patch) = patch {
388 let mut divergence = 0;
389 for hunk_index in 0..patch.num_hunks() {
390 let hunk = process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence);
391 tree.push(hunk, &buffer);
392 }
393 }
394 }
395
396 tree
397}
398
399fn process_patch_hunk(
400 patch: &GitPatch<'_>,
401 hunk_index: usize,
402 buffer: &text::BufferSnapshot,
403 buffer_row_divergence: &mut i64,
404) -> InternalDiffHunk {
405 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
406 assert!(line_item_count > 0);
407
408 let mut first_deletion_buffer_row: Option<u32> = None;
409 let mut buffer_row_range: Option<Range<u32>> = None;
410 let mut diff_base_byte_range: Option<Range<usize>> = None;
411
412 for line_index in 0..line_item_count {
413 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
414 let kind = line.origin_value();
415 let content_offset = line.content_offset() as isize;
416 let content_len = line.content().len() as isize;
417
418 if kind == GitDiffLineType::Addition {
419 *buffer_row_divergence += 1;
420 let row = line.new_lineno().unwrap().saturating_sub(1);
421
422 match &mut buffer_row_range {
423 Some(buffer_row_range) => buffer_row_range.end = row + 1,
424 None => buffer_row_range = Some(row..row + 1),
425 }
426 }
427
428 if kind == GitDiffLineType::Deletion {
429 let end = content_offset + content_len;
430
431 match &mut diff_base_byte_range {
432 Some(head_byte_range) => head_byte_range.end = end as usize,
433 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
434 }
435
436 if first_deletion_buffer_row.is_none() {
437 let old_row = line.old_lineno().unwrap().saturating_sub(1);
438 let row = old_row as i64 + *buffer_row_divergence;
439 first_deletion_buffer_row = Some(row as u32);
440 }
441
442 *buffer_row_divergence -= 1;
443 }
444 }
445
446 //unwrap_or deletion without addition
447 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
448 //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
449 let row = first_deletion_buffer_row.unwrap();
450 row..row
451 });
452
453 //unwrap_or addition without deletion
454 let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
455
456 let start = Point::new(buffer_row_range.start, 0);
457 let end = Point::new(buffer_row_range.end, 0);
458 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
459 InternalDiffHunk {
460 buffer_range,
461 diff_base_byte_range,
462 }
463}
464
465impl std::fmt::Debug for BufferDiff {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467 f.debug_struct("BufferChangeSet")
468 .field("buffer_id", &self.buffer_id)
469 .field("snapshot", &self.inner)
470 .finish()
471 }
472}
473
474pub enum BufferDiffEvent {
475 DiffChanged {
476 changed_range: Option<Range<text::Anchor>>,
477 },
478 LanguageChanged,
479}
480
481impl EventEmitter<BufferDiffEvent> for BufferDiff {}
482
483impl BufferDiff {
484 #[cfg(test)]
485 fn build_sync(
486 buffer: text::BufferSnapshot,
487 diff_base: String,
488 cx: &mut gpui::TestAppContext,
489 ) -> BufferDiffInner {
490 let snapshot =
491 cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
492 cx.executor().block(snapshot)
493 }
494
495 fn build(
496 buffer: text::BufferSnapshot,
497 diff_base: Option<Arc<String>>,
498 language: Option<Arc<Language>>,
499 language_registry: Option<Arc<LanguageRegistry>>,
500 cx: &mut App,
501 ) -> impl Future<Output = BufferDiffInner> {
502 let base_text_snapshot = diff_base.as_ref().map(|base_text| {
503 language::Buffer::build_snapshot(
504 Rope::from(base_text.as_str()),
505 language.clone(),
506 language_registry.clone(),
507 cx,
508 )
509 });
510 let base_text_snapshot = cx
511 .background_executor()
512 .spawn(OptionFuture::from(base_text_snapshot));
513
514 let hunks = cx.background_executor().spawn({
515 let buffer = buffer.clone();
516 async move { compute_hunks(diff_base, buffer) }
517 });
518
519 async move {
520 let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
521 BufferDiffInner { base_text, hunks }
522 }
523 }
524
525 fn build_with_base_buffer(
526 buffer: text::BufferSnapshot,
527 diff_base: Option<Arc<String>>,
528 diff_base_buffer: Option<language::BufferSnapshot>,
529 cx: &App,
530 ) -> impl Future<Output = BufferDiffInner> {
531 cx.background_executor().spawn(async move {
532 BufferDiffInner {
533 hunks: compute_hunks(diff_base, buffer),
534 base_text: diff_base_buffer,
535 }
536 })
537 }
538
539 fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner {
540 BufferDiffInner {
541 hunks: SumTree::new(buffer),
542 base_text: None,
543 }
544 }
545
546 pub fn build_with_single_insertion(
547 insertion_present_in_secondary_diff: bool,
548 cx: &mut App,
549 ) -> BufferDiffSnapshot {
550 let base_text = language::Buffer::build_empty_snapshot(cx);
551 let hunks = SumTree::from_item(
552 InternalDiffHunk {
553 buffer_range: Anchor::MIN..Anchor::MAX,
554 diff_base_byte_range: 0..0,
555 },
556 &base_text,
557 );
558 BufferDiffSnapshot {
559 inner: BufferDiffInner {
560 hunks: hunks.clone(),
561 base_text: Some(base_text.clone()),
562 },
563 secondary_diff: if insertion_present_in_secondary_diff {
564 Some(Box::new(BufferDiffSnapshot {
565 inner: BufferDiffInner {
566 hunks,
567 base_text: Some(base_text),
568 },
569 secondary_diff: None,
570 }))
571 } else {
572 None
573 },
574 }
575 }
576
577 pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
578 self.secondary_diff = Some(diff);
579 }
580
581 pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
582 Some(self.secondary_diff.as_ref()?.clone())
583 }
584
585 pub fn range_to_hunk_range(
586 &self,
587 range: Range<Anchor>,
588 buffer: &text::BufferSnapshot,
589 cx: &App,
590 ) -> Option<Range<Anchor>> {
591 let start = self
592 .hunks_intersecting_range(range.clone(), &buffer, cx)
593 .next()?
594 .buffer_range
595 .start;
596 let end = self
597 .hunks_intersecting_range_rev(range.clone(), &buffer)
598 .next()?
599 .buffer_range
600 .end;
601 Some(start..end)
602 }
603
604 #[allow(clippy::too_many_arguments)]
605 pub async fn update_diff(
606 this: Entity<BufferDiff>,
607 buffer: text::BufferSnapshot,
608 base_text: Option<Arc<String>>,
609 base_text_changed: bool,
610 language_changed: bool,
611 language: Option<Arc<Language>>,
612 language_registry: Option<Arc<LanguageRegistry>>,
613 cx: &mut AsyncApp,
614 ) -> anyhow::Result<Option<Range<Anchor>>> {
615 let snapshot = if base_text_changed || language_changed {
616 cx.update(|cx| {
617 Self::build(
618 buffer.clone(),
619 base_text,
620 language.clone(),
621 language_registry.clone(),
622 cx,
623 )
624 })?
625 .await
626 } else {
627 this.read_with(cx, |this, cx| {
628 Self::build_with_base_buffer(
629 buffer.clone(),
630 base_text,
631 this.base_text().cloned(),
632 cx,
633 )
634 })?
635 .await
636 };
637
638 this.update(cx, |this, _| this.set_state(snapshot, &buffer))
639 }
640
641 pub fn update_diff_from(
642 &mut self,
643 buffer: &text::BufferSnapshot,
644 other: &Entity<Self>,
645 cx: &mut Context<Self>,
646 ) -> Option<Range<Anchor>> {
647 let other = other.read(cx).inner.clone();
648 self.set_state(other, buffer)
649 }
650
651 fn set_state(
652 &mut self,
653 inner: BufferDiffInner,
654 buffer: &text::BufferSnapshot,
655 ) -> Option<Range<Anchor>> {
656 let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) {
657 (None, None) => None,
658 (Some(old), Some(new)) if old.remote_id() == new.remote_id() => {
659 inner.compare(&self.inner, buffer)
660 }
661 _ => Some(text::Anchor::MIN..text::Anchor::MAX),
662 };
663 self.inner = inner;
664 changed_range
665 }
666
667 pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
668 self.inner.base_text.as_ref()
669 }
670
671 pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
672 BufferDiffSnapshot {
673 inner: self.inner.clone(),
674 secondary_diff: self
675 .secondary_diff
676 .as_ref()
677 .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
678 }
679 }
680
681 pub fn hunks_intersecting_range<'a>(
682 &'a self,
683 range: Range<text::Anchor>,
684 buffer_snapshot: &'a text::BufferSnapshot,
685 cx: &'a App,
686 ) -> impl 'a + Iterator<Item = DiffHunk> {
687 let unstaged_counterpart = self
688 .secondary_diff
689 .as_ref()
690 .map(|diff| &diff.read(cx).inner);
691 self.inner
692 .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
693 }
694
695 pub fn hunks_intersecting_range_rev<'a>(
696 &'a self,
697 range: Range<text::Anchor>,
698 buffer_snapshot: &'a text::BufferSnapshot,
699 ) -> impl 'a + Iterator<Item = DiffHunk> {
700 self.inner
701 .hunks_intersecting_range_rev(range, buffer_snapshot)
702 }
703
704 pub fn hunks_in_row_range<'a>(
705 &'a self,
706 range: Range<u32>,
707 buffer: &'a text::BufferSnapshot,
708 cx: &'a App,
709 ) -> impl 'a + Iterator<Item = DiffHunk> {
710 let start = buffer.anchor_before(Point::new(range.start, 0));
711 let end = buffer.anchor_after(Point::new(range.end, 0));
712 self.hunks_intersecting_range(start..end, buffer, cx)
713 }
714
715 /// Used in cases where the change set isn't derived from git.
716 pub fn set_base_text(
717 &mut self,
718 base_buffer: Entity<language::Buffer>,
719 buffer: text::BufferSnapshot,
720 cx: &mut Context<Self>,
721 ) -> oneshot::Receiver<()> {
722 let (tx, rx) = oneshot::channel();
723 let this = cx.weak_entity();
724 let base_buffer = base_buffer.read(cx);
725 let language_registry = base_buffer.language_registry();
726 let base_buffer = base_buffer.snapshot();
727 let base_text = Arc::new(base_buffer.text());
728
729 let snapshot = BufferDiff::build(
730 buffer.clone(),
731 Some(base_text),
732 base_buffer.language().cloned(),
733 language_registry,
734 cx,
735 );
736 let complete_on_drop = util::defer(|| {
737 tx.send(()).ok();
738 });
739 cx.spawn(|_, mut cx| async move {
740 let snapshot = snapshot.await;
741 let Some(this) = this.upgrade() else {
742 return;
743 };
744 this.update(&mut cx, |this, _| {
745 this.set_state(snapshot, &buffer);
746 })
747 .log_err();
748 drop(complete_on_drop)
749 })
750 .detach();
751 rx
752 }
753
754 #[cfg(any(test, feature = "test-support"))]
755 pub fn base_text_string(&self) -> Option<String> {
756 self.inner.base_text.as_ref().map(|buffer| buffer.text())
757 }
758
759 pub fn new(buffer: &text::BufferSnapshot) -> Self {
760 BufferDiff {
761 buffer_id: buffer.remote_id(),
762 inner: BufferDiff::build_empty(buffer),
763 secondary_diff: None,
764 }
765 }
766
767 #[cfg(any(test, feature = "test-support"))]
768 pub fn new_with_base_text(
769 base_text: &str,
770 buffer: &Entity<language::Buffer>,
771 cx: &mut App,
772 ) -> Self {
773 let mut base_text = base_text.to_owned();
774 text::LineEnding::normalize(&mut base_text);
775 let snapshot = BufferDiff::build(
776 buffer.read(cx).text_snapshot(),
777 Some(base_text.into()),
778 None,
779 None,
780 cx,
781 );
782 let snapshot = cx.background_executor().block(snapshot);
783 BufferDiff {
784 buffer_id: buffer.read(cx).remote_id(),
785 inner: snapshot,
786 secondary_diff: None,
787 }
788 }
789
790 #[cfg(any(test, feature = "test-support"))]
791 pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
792 let base_text = self
793 .inner
794 .base_text
795 .as_ref()
796 .map(|base_text| base_text.text());
797 let snapshot = BufferDiff::build_with_base_buffer(
798 buffer.clone(),
799 base_text.clone().map(Arc::new),
800 self.inner.base_text.clone(),
801 cx,
802 );
803 let snapshot = cx.background_executor().block(snapshot);
804 let changed_range = self.set_state(snapshot, &buffer);
805 cx.emit(BufferDiffEvent::DiffChanged { changed_range });
806 }
807}
808
809impl DiffHunk {
810 pub fn status(&self) -> DiffHunkStatus {
811 if self.buffer_range.start == self.buffer_range.end {
812 DiffHunkStatus::Removed(self.secondary_status)
813 } else if self.diff_base_byte_range.is_empty() {
814 DiffHunkStatus::Added(self.secondary_status)
815 } else {
816 DiffHunkStatus::Modified(self.secondary_status)
817 }
818 }
819}
820
821impl DiffHunkStatus {
822 pub fn is_removed(&self) -> bool {
823 matches!(self, DiffHunkStatus::Removed(_))
824 }
825
826 #[cfg(any(test, feature = "test-support"))]
827 pub fn removed() -> Self {
828 DiffHunkStatus::Removed(DiffHunkSecondaryStatus::None)
829 }
830
831 #[cfg(any(test, feature = "test-support"))]
832 pub fn added() -> Self {
833 DiffHunkStatus::Added(DiffHunkSecondaryStatus::None)
834 }
835
836 #[cfg(any(test, feature = "test-support"))]
837 pub fn modified() -> Self {
838 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None)
839 }
840}
841
842/// Range (crossing new lines), old, new
843#[cfg(any(test, feature = "test-support"))]
844#[track_caller]
845pub fn assert_hunks<Iter>(
846 diff_hunks: Iter,
847 buffer: &text::BufferSnapshot,
848 diff_base: &str,
849 expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
850) where
851 Iter: Iterator<Item = DiffHunk>,
852{
853 let actual_hunks = diff_hunks
854 .map(|hunk| {
855 (
856 hunk.row_range.clone(),
857 &diff_base[hunk.diff_base_byte_range.clone()],
858 buffer
859 .text_for_range(
860 Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
861 )
862 .collect::<String>(),
863 hunk.status(),
864 )
865 })
866 .collect::<Vec<_>>();
867
868 let expected_hunks: Vec<_> = expected_hunks
869 .iter()
870 .map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
871 .collect();
872
873 assert_eq!(actual_hunks, expected_hunks);
874}
875
876#[cfg(test)]
877mod tests {
878 use std::assert_eq;
879
880 use super::*;
881 use gpui::TestAppContext;
882 use text::{Buffer, BufferId};
883 use unindent::Unindent as _;
884
885 #[gpui::test]
886 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
887 let diff_base = "
888 one
889 two
890 three
891 "
892 .unindent();
893
894 let buffer_text = "
895 one
896 HELLO
897 three
898 "
899 .unindent();
900
901 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
902 let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
903 assert_hunks(
904 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
905 &buffer,
906 &diff_base,
907 &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified())],
908 );
909
910 buffer.edit([(0..0, "point five\n")]);
911 diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
912 assert_hunks(
913 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
914 &buffer,
915 &diff_base,
916 &[
917 (0..1, "", "point five\n", DiffHunkStatus::added()),
918 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified()),
919 ],
920 );
921
922 diff = BufferDiff::build_empty(&buffer);
923 assert_hunks(
924 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
925 &buffer,
926 &diff_base,
927 &[],
928 );
929 }
930
931 #[gpui::test]
932 async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
933 let head_text = "
934 zero
935 one
936 two
937 three
938 four
939 five
940 six
941 seven
942 eight
943 nine
944 "
945 .unindent();
946
947 let index_text = "
948 zero
949 one
950 TWO
951 three
952 FOUR
953 five
954 six
955 seven
956 eight
957 NINE
958 "
959 .unindent();
960
961 let buffer_text = "
962 zero
963 one
964 TWO
965 three
966 FOUR
967 FIVE
968 six
969 SEVEN
970 eight
971 nine
972 "
973 .unindent();
974
975 let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
976 let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx);
977
978 let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
979
980 let expected_hunks = vec![
981 (
982 2..3,
983 "two\n",
984 "TWO\n",
985 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None),
986 ),
987 (
988 4..6,
989 "four\nfive\n",
990 "FOUR\nFIVE\n",
991 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
992 ),
993 (
994 7..8,
995 "seven\n",
996 "SEVEN\n",
997 DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
998 ),
999 ];
1000
1001 assert_hunks(
1002 uncommitted_diff.hunks_intersecting_range(
1003 Anchor::MIN..Anchor::MAX,
1004 &buffer,
1005 Some(&unstaged_diff),
1006 ),
1007 &buffer,
1008 &head_text,
1009 &expected_hunks,
1010 );
1011 }
1012
1013 #[gpui::test]
1014 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1015 let diff_base = Arc::new(
1016 "
1017 one
1018 two
1019 three
1020 four
1021 five
1022 six
1023 seven
1024 eight
1025 nine
1026 ten
1027 "
1028 .unindent(),
1029 );
1030
1031 let buffer_text = "
1032 A
1033 one
1034 B
1035 two
1036 C
1037 three
1038 HELLO
1039 four
1040 five
1041 SIXTEEN
1042 seven
1043 eight
1044 WORLD
1045 nine
1046
1047 ten
1048
1049 "
1050 .unindent();
1051
1052 let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1053 let diff = cx
1054 .update(|cx| {
1055 BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx)
1056 })
1057 .await;
1058 assert_eq!(
1059 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None)
1060 .count(),
1061 8
1062 );
1063
1064 assert_hunks(
1065 diff.hunks_intersecting_range(
1066 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1067 &buffer,
1068 None,
1069 ),
1070 &buffer,
1071 &diff_base,
1072 &[
1073 (6..7, "", "HELLO\n", DiffHunkStatus::added()),
1074 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified()),
1075 (12..13, "", "WORLD\n", DiffHunkStatus::added()),
1076 ],
1077 );
1078 }
1079
1080 #[gpui::test]
1081 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
1082 let base_text = "
1083 zero
1084 one
1085 two
1086 three
1087 four
1088 five
1089 six
1090 seven
1091 eight
1092 nine
1093 "
1094 .unindent();
1095
1096 let buffer_text_1 = "
1097 one
1098 three
1099 four
1100 five
1101 SIX
1102 seven
1103 eight
1104 NINE
1105 "
1106 .unindent();
1107
1108 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
1109
1110 let empty_diff = BufferDiff::build_empty(&buffer);
1111 let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1112 let range = diff_1.compare(&empty_diff, &buffer).unwrap();
1113 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
1114
1115 // Edit does not affect the diff.
1116 buffer.edit_via_marked_text(
1117 &"
1118 one
1119 three
1120 four
1121 five
1122 «SIX.5»
1123 seven
1124 eight
1125 NINE
1126 "
1127 .unindent(),
1128 );
1129 let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1130 assert_eq!(None, diff_2.compare(&diff_1, &buffer));
1131
1132 // Edit turns a deletion hunk into a modification.
1133 buffer.edit_via_marked_text(
1134 &"
1135 one
1136 «THREE»
1137 four
1138 five
1139 SIX.5
1140 seven
1141 eight
1142 NINE
1143 "
1144 .unindent(),
1145 );
1146 let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1147 let range = diff_3.compare(&diff_2, &buffer).unwrap();
1148 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
1149
1150 // Edit turns a modification hunk into a deletion.
1151 buffer.edit_via_marked_text(
1152 &"
1153 one
1154 THREE
1155 four
1156 five«»
1157 seven
1158 eight
1159 NINE
1160 "
1161 .unindent(),
1162 );
1163 let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1164 let range = diff_4.compare(&diff_3, &buffer).unwrap();
1165 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
1166
1167 // Edit introduces a new insertion hunk.
1168 buffer.edit_via_marked_text(
1169 &"
1170 one
1171 THREE
1172 four«
1173 FOUR.5
1174 »five
1175 seven
1176 eight
1177 NINE
1178 "
1179 .unindent(),
1180 );
1181 let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx);
1182 let range = diff_5.compare(&diff_4, &buffer).unwrap();
1183 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
1184
1185 // Edit removes a hunk.
1186 buffer.edit_via_marked_text(
1187 &"
1188 one
1189 THREE
1190 four
1191 FOUR.5
1192 five
1193 seven
1194 eight
1195 «nine»
1196 "
1197 .unindent(),
1198 );
1199 let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx);
1200 let range = diff_6.compare(&diff_5, &buffer).unwrap();
1201 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
1202 }
1203}