1use futures::{channel::oneshot, future::OptionFuture};
2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
3use gpui::{App, 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, BufferId, OffsetRangeExt, Point};
9use util::ResultExt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum DiffHunkStatus {
13 Added,
14 Modified,
15 Removed,
16}
17
18/// A diff hunk resolved to rows in the buffer.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DiffHunk {
21 /// The buffer range, expressed in terms of rows.
22 pub row_range: Range<u32>,
23 /// The range in the buffer to which this hunk corresponds.
24 pub buffer_range: Range<Anchor>,
25 /// The range in the buffer's diff base text to which this hunk corresponds.
26 pub diff_base_byte_range: Range<usize>,
27}
28
29/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
30#[derive(Debug, Clone, PartialEq, Eq)]
31struct InternalDiffHunk {
32 buffer_range: Range<Anchor>,
33 diff_base_byte_range: Range<usize>,
34}
35
36impl sum_tree::Item for InternalDiffHunk {
37 type Summary = DiffHunkSummary;
38
39 fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
40 DiffHunkSummary {
41 buffer_range: self.buffer_range.clone(),
42 }
43 }
44}
45
46#[derive(Debug, Default, Clone)]
47pub struct DiffHunkSummary {
48 buffer_range: Range<Anchor>,
49}
50
51impl sum_tree::Summary for DiffHunkSummary {
52 type Context = text::BufferSnapshot;
53
54 fn zero(_cx: &Self::Context) -> Self {
55 Default::default()
56 }
57
58 fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
59 self.buffer_range.start = self
60 .buffer_range
61 .start
62 .min(&other.buffer_range.start, buffer);
63 self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
64 }
65}
66
67#[derive(Clone)]
68pub struct BufferDiffSnapshot {
69 hunks: SumTree<InternalDiffHunk>,
70 pub base_text: Option<language::BufferSnapshot>,
71}
72
73impl std::fmt::Debug for BufferDiffSnapshot {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("BufferDiffSnapshot")
76 .field("hunks", &self.hunks)
77 .finish()
78 }
79}
80
81impl BufferDiffSnapshot {
82 pub fn new(buffer: &text::BufferSnapshot) -> BufferDiffSnapshot {
83 BufferDiffSnapshot {
84 hunks: SumTree::new(buffer),
85 base_text: None,
86 }
87 }
88
89 pub fn new_with_single_insertion(cx: &mut App) -> Self {
90 let base_text = language::Buffer::build_empty_snapshot(cx);
91 Self {
92 hunks: SumTree::from_item(
93 InternalDiffHunk {
94 buffer_range: Anchor::MIN..Anchor::MAX,
95 diff_base_byte_range: 0..0,
96 },
97 &base_text,
98 ),
99 base_text: Some(base_text),
100 }
101 }
102
103 #[cfg(any(test, feature = "test-support"))]
104 pub fn build_sync(
105 buffer: text::BufferSnapshot,
106 diff_base: String,
107 cx: &mut gpui::TestAppContext,
108 ) -> Self {
109 let snapshot =
110 cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
111 cx.executor().block(snapshot)
112 }
113
114 pub fn build(
115 buffer: text::BufferSnapshot,
116 diff_base: Option<Arc<String>>,
117 language: Option<Arc<Language>>,
118 language_registry: Option<Arc<LanguageRegistry>>,
119 cx: &mut App,
120 ) -> impl Future<Output = Self> {
121 let base_text_snapshot = diff_base.as_ref().map(|base_text| {
122 language::Buffer::build_snapshot(
123 Rope::from(base_text.as_str()),
124 language.clone(),
125 language_registry.clone(),
126 cx,
127 )
128 });
129 let base_text_snapshot = cx
130 .background_executor()
131 .spawn(OptionFuture::from(base_text_snapshot));
132
133 let hunks = cx.background_executor().spawn({
134 let buffer = buffer.clone();
135 async move { Self::recalculate_hunks(diff_base, buffer) }
136 });
137
138 async move {
139 let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
140 Self { base_text, hunks }
141 }
142 }
143
144 pub fn build_with_base_buffer(
145 buffer: text::BufferSnapshot,
146 diff_base: Option<Arc<String>>,
147 diff_base_buffer: Option<language::BufferSnapshot>,
148 cx: &App,
149 ) -> impl Future<Output = Self> {
150 cx.background_executor().spawn({
151 let buffer = buffer.clone();
152 async move {
153 let hunks = Self::recalculate_hunks(diff_base, buffer);
154 Self {
155 hunks,
156 base_text: diff_base_buffer,
157 }
158 }
159 })
160 }
161
162 fn recalculate_hunks(
163 diff_base: Option<Arc<String>>,
164 buffer: text::BufferSnapshot,
165 ) -> SumTree<InternalDiffHunk> {
166 let mut tree = SumTree::new(&buffer);
167
168 if let Some(diff_base) = diff_base {
169 let buffer_text = buffer.as_rope().to_string();
170 let patch = Self::diff(&diff_base, &buffer_text);
171
172 // A common case in Zed is that the empty buffer is represented as just a newline,
173 // but if we just compute a naive diff you get a "preserved" line in the middle,
174 // which is a bit odd.
175 if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
176 tree.push(
177 InternalDiffHunk {
178 buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
179 diff_base_byte_range: 0..diff_base.len() - 1,
180 },
181 &buffer,
182 );
183 return tree;
184 }
185
186 if let Some(patch) = patch {
187 let mut divergence = 0;
188 for hunk_index in 0..patch.num_hunks() {
189 let hunk =
190 Self::process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence);
191 tree.push(hunk, &buffer);
192 }
193 }
194 }
195
196 tree
197 }
198
199 pub fn is_empty(&self) -> bool {
200 self.hunks.is_empty()
201 }
202
203 pub fn hunks_in_row_range<'a>(
204 &'a self,
205 range: Range<u32>,
206 buffer: &'a text::BufferSnapshot,
207 ) -> impl 'a + Iterator<Item = DiffHunk> {
208 let start = buffer.anchor_before(Point::new(range.start, 0));
209 let end = buffer.anchor_after(Point::new(range.end, 0));
210
211 self.hunks_intersecting_range(start..end, buffer)
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 range = range.to_offset(buffer);
220
221 let mut cursor = self
222 .hunks
223 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
224 let summary_range = summary.buffer_range.to_offset(buffer);
225 let before_start = summary_range.end < range.start;
226 let after_end = summary_range.start > range.end;
227 !before_start && !after_end
228 });
229
230 let anchor_iter = iter::from_fn(move || {
231 cursor.next(buffer);
232 cursor.item()
233 })
234 .flat_map(move |hunk| {
235 [
236 (
237 &hunk.buffer_range.start,
238 (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
239 ),
240 (
241 &hunk.buffer_range.end,
242 (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
243 ),
244 ]
245 });
246
247 let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
248 iter::from_fn(move || loop {
249 let (start_point, (start_anchor, start_base)) = summaries.next()?;
250 let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
251
252 if !start_anchor.is_valid(buffer) {
253 continue;
254 }
255
256 if end_point.column > 0 {
257 end_point.row += 1;
258 end_point.column = 0;
259 end_anchor = buffer.anchor_before(end_point);
260 }
261
262 return Some(DiffHunk {
263 row_range: start_point.row..end_point.row,
264 diff_base_byte_range: start_base..end_base,
265 buffer_range: start_anchor..end_anchor,
266 });
267 })
268 }
269
270 pub fn hunks_intersecting_range_rev<'a>(
271 &'a self,
272 range: Range<Anchor>,
273 buffer: &'a text::BufferSnapshot,
274 ) -> impl 'a + Iterator<Item = DiffHunk> {
275 let mut cursor = self
276 .hunks
277 .filter::<_, DiffHunkSummary>(buffer, move |summary| {
278 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
279 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
280 !before_start && !after_end
281 });
282
283 iter::from_fn(move || {
284 cursor.prev(buffer);
285
286 let hunk = cursor.item()?;
287 let range = hunk.buffer_range.to_point(buffer);
288 let end_row = if range.end.column > 0 {
289 range.end.row + 1
290 } else {
291 range.end.row
292 };
293
294 Some(DiffHunk {
295 row_range: range.start.row..end_row,
296 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
297 buffer_range: hunk.buffer_range.clone(),
298 })
299 })
300 }
301
302 pub fn compare(
303 &self,
304 old: &Self,
305 new_snapshot: &text::BufferSnapshot,
306 ) -> Option<Range<Anchor>> {
307 let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
308 let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
309 old_cursor.next(new_snapshot);
310 new_cursor.next(new_snapshot);
311 let mut start = None;
312 let mut end = None;
313
314 loop {
315 match (new_cursor.item(), old_cursor.item()) {
316 (Some(new_hunk), Some(old_hunk)) => {
317 match new_hunk
318 .buffer_range
319 .start
320 .cmp(&old_hunk.buffer_range.start, new_snapshot)
321 {
322 cmp::Ordering::Less => {
323 start.get_or_insert(new_hunk.buffer_range.start);
324 end.replace(new_hunk.buffer_range.end);
325 new_cursor.next(new_snapshot);
326 }
327 cmp::Ordering::Equal => {
328 if new_hunk != old_hunk {
329 start.get_or_insert(new_hunk.buffer_range.start);
330 if old_hunk
331 .buffer_range
332 .end
333 .cmp(&new_hunk.buffer_range.end, new_snapshot)
334 .is_ge()
335 {
336 end.replace(old_hunk.buffer_range.end);
337 } else {
338 end.replace(new_hunk.buffer_range.end);
339 }
340 }
341
342 new_cursor.next(new_snapshot);
343 old_cursor.next(new_snapshot);
344 }
345 cmp::Ordering::Greater => {
346 start.get_or_insert(old_hunk.buffer_range.start);
347 end.replace(old_hunk.buffer_range.end);
348 old_cursor.next(new_snapshot);
349 }
350 }
351 }
352 (Some(new_hunk), None) => {
353 start.get_or_insert(new_hunk.buffer_range.start);
354 end.replace(new_hunk.buffer_range.end);
355 new_cursor.next(new_snapshot);
356 }
357 (None, Some(old_hunk)) => {
358 start.get_or_insert(old_hunk.buffer_range.start);
359 end.replace(old_hunk.buffer_range.end);
360 old_cursor.next(new_snapshot);
361 }
362 (None, None) => break,
363 }
364 }
365
366 start.zip(end).map(|(start, end)| start..end)
367 }
368
369 #[cfg(test)]
370 fn clear(&mut self, buffer: &text::BufferSnapshot) {
371 self.hunks = SumTree::new(buffer);
372 }
373
374 #[cfg(test)]
375 fn hunks<'a>(&'a self, text: &'a text::BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
376 let start = text.anchor_before(Point::new(0, 0));
377 let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
378 self.hunks_intersecting_range(start..end, text)
379 }
380
381 fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
382 let mut options = GitOptions::default();
383 options.context_lines(0);
384
385 let patch = GitPatch::from_buffers(
386 head.as_bytes(),
387 None,
388 current.as_bytes(),
389 None,
390 Some(&mut options),
391 );
392
393 match patch {
394 Ok(patch) => Some(patch),
395
396 Err(err) => {
397 log::error!("`GitPatch::from_buffers` failed: {}", err);
398 None
399 }
400 }
401 }
402
403 fn process_patch_hunk(
404 patch: &GitPatch<'_>,
405 hunk_index: usize,
406 buffer: &text::BufferSnapshot,
407 buffer_row_divergence: &mut i64,
408 ) -> InternalDiffHunk {
409 let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
410 assert!(line_item_count > 0);
411
412 let mut first_deletion_buffer_row: Option<u32> = None;
413 let mut buffer_row_range: Option<Range<u32>> = None;
414 let mut diff_base_byte_range: Option<Range<usize>> = None;
415
416 for line_index in 0..line_item_count {
417 let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
418 let kind = line.origin_value();
419 let content_offset = line.content_offset() as isize;
420 let content_len = line.content().len() as isize;
421
422 if kind == GitDiffLineType::Addition {
423 *buffer_row_divergence += 1;
424 let row = line.new_lineno().unwrap().saturating_sub(1);
425
426 match &mut buffer_row_range {
427 Some(buffer_row_range) => buffer_row_range.end = row + 1,
428 None => buffer_row_range = Some(row..row + 1),
429 }
430 }
431
432 if kind == GitDiffLineType::Deletion {
433 let end = content_offset + content_len;
434
435 match &mut diff_base_byte_range {
436 Some(head_byte_range) => head_byte_range.end = end as usize,
437 None => diff_base_byte_range = Some(content_offset as usize..end as usize),
438 }
439
440 if first_deletion_buffer_row.is_none() {
441 let old_row = line.old_lineno().unwrap().saturating_sub(1);
442 let row = old_row as i64 + *buffer_row_divergence;
443 first_deletion_buffer_row = Some(row as u32);
444 }
445
446 *buffer_row_divergence -= 1;
447 }
448 }
449
450 //unwrap_or deletion without addition
451 let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
452 //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
453 let row = first_deletion_buffer_row.unwrap();
454 row..row
455 });
456
457 //unwrap_or addition without deletion
458 let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
459
460 let start = Point::new(buffer_row_range.start, 0);
461 let end = Point::new(buffer_row_range.end, 0);
462 let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
463 InternalDiffHunk {
464 buffer_range,
465 diff_base_byte_range,
466 }
467 }
468}
469
470pub struct BufferDiff {
471 pub buffer_id: BufferId,
472 pub snapshot: BufferDiffSnapshot,
473 pub unstaged_diff: Option<Entity<BufferDiff>>,
474}
475
476impl std::fmt::Debug for BufferDiff {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 f.debug_struct("BufferChangeSet")
479 .field("buffer_id", &self.buffer_id)
480 .field("snapshot", &self.snapshot)
481 .finish()
482 }
483}
484
485pub enum BufferDiffEvent {
486 DiffChanged { changed_range: Range<text::Anchor> },
487 LanguageChanged,
488}
489
490impl EventEmitter<BufferDiffEvent> for BufferDiff {}
491
492impl BufferDiff {
493 pub fn set_state(
494 &mut self,
495 snapshot: BufferDiffSnapshot,
496 buffer: &text::BufferSnapshot,
497 cx: &mut Context<Self>,
498 ) {
499 if let Some(base_text) = snapshot.base_text.as_ref() {
500 let changed_range = if Some(base_text.remote_id())
501 != self
502 .snapshot
503 .base_text
504 .as_ref()
505 .map(|buffer| buffer.remote_id())
506 {
507 Some(text::Anchor::MIN..text::Anchor::MAX)
508 } else {
509 snapshot.compare(&self.snapshot, buffer)
510 };
511 if let Some(changed_range) = changed_range {
512 cx.emit(BufferDiffEvent::DiffChanged { changed_range });
513 }
514 }
515 self.snapshot = snapshot;
516 }
517
518 pub fn diff_hunks_intersecting_range<'a>(
519 &'a self,
520 range: Range<text::Anchor>,
521 buffer_snapshot: &'a text::BufferSnapshot,
522 ) -> impl 'a + Iterator<Item = DiffHunk> {
523 self.snapshot
524 .hunks_intersecting_range(range, buffer_snapshot)
525 }
526
527 pub fn diff_hunks_intersecting_range_rev<'a>(
528 &'a self,
529 range: Range<text::Anchor>,
530 buffer_snapshot: &'a text::BufferSnapshot,
531 ) -> impl 'a + Iterator<Item = DiffHunk> {
532 self.snapshot
533 .hunks_intersecting_range_rev(range, buffer_snapshot)
534 }
535
536 /// Used in cases where the change set isn't derived from git.
537 pub fn set_base_text(
538 &mut self,
539 base_buffer: Entity<language::Buffer>,
540 buffer: text::BufferSnapshot,
541 cx: &mut Context<Self>,
542 ) -> oneshot::Receiver<()> {
543 let (tx, rx) = oneshot::channel();
544 let this = cx.weak_entity();
545 let base_buffer = base_buffer.read(cx);
546 let language_registry = base_buffer.language_registry();
547 let base_buffer = base_buffer.snapshot();
548 let base_text = Arc::new(base_buffer.text());
549
550 let snapshot = BufferDiffSnapshot::build(
551 buffer.clone(),
552 Some(base_text),
553 base_buffer.language().cloned(),
554 language_registry,
555 cx,
556 );
557 let complete_on_drop = util::defer(|| {
558 tx.send(()).ok();
559 });
560 cx.spawn(|_, mut cx| async move {
561 let snapshot = snapshot.await;
562 let Some(this) = this.upgrade() else {
563 return;
564 };
565 this.update(&mut cx, |this, cx| {
566 this.set_state(snapshot, &buffer, cx);
567 })
568 .log_err();
569 drop(complete_on_drop)
570 })
571 .detach();
572 rx
573 }
574
575 #[cfg(any(test, feature = "test-support"))]
576 pub fn base_text_string(&self) -> Option<String> {
577 self.snapshot.base_text.as_ref().map(|buffer| buffer.text())
578 }
579
580 pub fn new(buffer: &Entity<language::Buffer>, cx: &mut App) -> Self {
581 BufferDiff {
582 buffer_id: buffer.read(cx).remote_id(),
583 snapshot: BufferDiffSnapshot::new(&buffer.read(cx)),
584 unstaged_diff: None,
585 }
586 }
587
588 #[cfg(any(test, feature = "test-support"))]
589 pub fn new_with_base_text(
590 base_text: &str,
591 buffer: &Entity<language::Buffer>,
592 cx: &mut App,
593 ) -> Self {
594 let mut base_text = base_text.to_owned();
595 text::LineEnding::normalize(&mut base_text);
596 let snapshot = BufferDiffSnapshot::build(
597 buffer.read(cx).text_snapshot(),
598 Some(base_text.into()),
599 None,
600 None,
601 cx,
602 );
603 let snapshot = cx.background_executor().block(snapshot);
604 BufferDiff {
605 buffer_id: buffer.read(cx).remote_id(),
606 snapshot,
607 unstaged_diff: None,
608 }
609 }
610
611 #[cfg(any(test, feature = "test-support"))]
612 pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
613 let base_text = self
614 .snapshot
615 .base_text
616 .as_ref()
617 .map(|base_text| base_text.text());
618 let snapshot = BufferDiffSnapshot::build_with_base_buffer(
619 buffer.clone(),
620 base_text.clone().map(Arc::new),
621 self.snapshot.base_text.clone(),
622 cx,
623 );
624 let snapshot = cx.background_executor().block(snapshot);
625 self.set_state(snapshot, &buffer, cx);
626 }
627}
628
629/// Range (crossing new lines), old, new
630#[cfg(any(test, feature = "test-support"))]
631#[track_caller]
632pub fn assert_hunks<Iter>(
633 diff_hunks: Iter,
634 buffer: &text::BufferSnapshot,
635 diff_base: &str,
636 expected_hunks: &[(Range<u32>, &str, &str)],
637) where
638 Iter: Iterator<Item = DiffHunk>,
639{
640 let actual_hunks = diff_hunks
641 .map(|hunk| {
642 (
643 hunk.row_range.clone(),
644 &diff_base[hunk.diff_base_byte_range],
645 buffer
646 .text_for_range(
647 Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
648 )
649 .collect::<String>(),
650 )
651 })
652 .collect::<Vec<_>>();
653
654 let expected_hunks: Vec<_> = expected_hunks
655 .iter()
656 .map(|(r, s, h)| (r.clone(), *s, h.to_string()))
657 .collect();
658
659 assert_eq!(actual_hunks, expected_hunks);
660}
661
662#[cfg(test)]
663mod tests {
664 use std::assert_eq;
665
666 use super::*;
667 use gpui::TestAppContext;
668 use text::{Buffer, BufferId};
669 use unindent::Unindent as _;
670
671 #[gpui::test]
672 async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
673 let diff_base = "
674 one
675 two
676 three
677 "
678 .unindent();
679
680 let buffer_text = "
681 one
682 HELLO
683 three
684 "
685 .unindent();
686
687 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
688 let mut diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
689 assert_hunks(
690 diff.hunks(&buffer),
691 &buffer,
692 &diff_base,
693 &[(1..2, "two\n", "HELLO\n")],
694 );
695
696 buffer.edit([(0..0, "point five\n")]);
697 diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
698 assert_hunks(
699 diff.hunks(&buffer),
700 &buffer,
701 &diff_base,
702 &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
703 );
704
705 diff.clear(&buffer);
706 assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
707 }
708
709 #[gpui::test]
710 async fn test_buffer_diff_range(cx: &mut TestAppContext) {
711 let diff_base = Arc::new(
712 "
713 one
714 two
715 three
716 four
717 five
718 six
719 seven
720 eight
721 nine
722 ten
723 "
724 .unindent(),
725 );
726
727 let buffer_text = "
728 A
729 one
730 B
731 two
732 C
733 three
734 HELLO
735 four
736 five
737 SIXTEEN
738 seven
739 eight
740 WORLD
741 nine
742
743 ten
744
745 "
746 .unindent();
747
748 let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
749 let diff = cx
750 .update(|cx| {
751 BufferDiffSnapshot::build(
752 buffer.snapshot(),
753 Some(diff_base.clone()),
754 None,
755 None,
756 cx,
757 )
758 })
759 .await;
760 assert_eq!(diff.hunks(&buffer).count(), 8);
761
762 assert_hunks(
763 diff.hunks_in_row_range(7..12, &buffer),
764 &buffer,
765 &diff_base,
766 &[
767 (6..7, "", "HELLO\n"),
768 (9..10, "six\n", "SIXTEEN\n"),
769 (12..13, "", "WORLD\n"),
770 ],
771 );
772 }
773
774 #[gpui::test]
775 async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
776 let base_text = "
777 zero
778 one
779 two
780 three
781 four
782 five
783 six
784 seven
785 eight
786 nine
787 "
788 .unindent();
789
790 let buffer_text_1 = "
791 one
792 three
793 four
794 five
795 SIX
796 seven
797 eight
798 NINE
799 "
800 .unindent();
801
802 let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
803
804 let empty_diff = BufferDiffSnapshot::new(&buffer);
805 let diff_1 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
806 let range = diff_1.compare(&empty_diff, &buffer).unwrap();
807 assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
808
809 // Edit does not affect the diff.
810 buffer.edit_via_marked_text(
811 &"
812 one
813 three
814 four
815 five
816 «SIX.5»
817 seven
818 eight
819 NINE
820 "
821 .unindent(),
822 );
823 let diff_2 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
824 assert_eq!(None, diff_2.compare(&diff_1, &buffer));
825
826 // Edit turns a deletion hunk into a modification.
827 buffer.edit_via_marked_text(
828 &"
829 one
830 «THREE»
831 four
832 five
833 SIX.5
834 seven
835 eight
836 NINE
837 "
838 .unindent(),
839 );
840 let diff_3 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
841 let range = diff_3.compare(&diff_2, &buffer).unwrap();
842 assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
843
844 // Edit turns a modification hunk into a deletion.
845 buffer.edit_via_marked_text(
846 &"
847 one
848 THREE
849 four
850 five«»
851 seven
852 eight
853 NINE
854 "
855 .unindent(),
856 );
857 let diff_4 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
858 let range = diff_4.compare(&diff_3, &buffer).unwrap();
859 assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
860
861 // Edit introduces a new insertion hunk.
862 buffer.edit_via_marked_text(
863 &"
864 one
865 THREE
866 four«
867 FOUR.5
868 »five
869 seven
870 eight
871 NINE
872 "
873 .unindent(),
874 );
875 let diff_5 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text.clone(), cx);
876 let range = diff_5.compare(&diff_4, &buffer).unwrap();
877 assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
878
879 // Edit removes a hunk.
880 buffer.edit_via_marked_text(
881 &"
882 one
883 THREE
884 four
885 FOUR.5
886 five
887 seven
888 eight
889 «nine»
890 "
891 .unindent(),
892 );
893 let diff_6 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text, cx);
894 let range = diff_6.compare(&diff_5, &buffer).unwrap();
895 assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
896 }
897}