diff.rs

  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}