buffer_diff.rs

   1use futures::channel::oneshot;
   2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
   3use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
   4use language::{
   5    Capability, Diff, DiffOptions, File, Language, LanguageName, LanguageRegistry,
   6    language_settings::language_settings, word_diff_ranges,
   7};
   8use rope::Rope;
   9use std::{
  10    cmp::Ordering,
  11    future::Future,
  12    iter,
  13    ops::{Range, RangeInclusive},
  14    sync::Arc,
  15};
  16use sum_tree::SumTree;
  17use text::{
  18    Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _,
  19};
  20use util::ResultExt;
  21
  22pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
  23
  24pub struct BufferDiff {
  25    pub buffer_id: BufferId,
  26    inner: BufferDiffInner<Entity<language::Buffer>>,
  27    secondary_diff: Option<Entity<BufferDiff>>,
  28}
  29
  30#[derive(Clone)]
  31pub struct BufferDiffSnapshot {
  32    inner: BufferDiffInner<language::BufferSnapshot>,
  33    secondary_diff: Option<Arc<BufferDiffSnapshot>>,
  34}
  35
  36impl std::fmt::Debug for BufferDiffSnapshot {
  37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  38        f.debug_struct("BufferDiffSnapshot")
  39            .field("inner", &self.inner)
  40            .field("secondary_diff", &self.secondary_diff)
  41            .finish()
  42    }
  43}
  44
  45#[derive(Clone)]
  46pub struct BufferDiffUpdate {
  47    inner: BufferDiffInner<Arc<str>>,
  48    buffer_snapshot: text::BufferSnapshot,
  49    base_text_edits: Option<Diff>,
  50    base_text_changed: bool,
  51}
  52
  53#[derive(Clone)]
  54struct BufferDiffInner<BaseText> {
  55    hunks: SumTree<InternalDiffHunk>,
  56    pending_hunks: SumTree<PendingHunk>,
  57    base_text: BaseText,
  58    base_text_exists: bool,
  59    buffer_snapshot: text::BufferSnapshot,
  60}
  61
  62impl<BaseText> BufferDiffInner<BaseText> {
  63    fn buffer_version(&self) -> &clock::Global {
  64        self.buffer_snapshot.version()
  65    }
  66}
  67
  68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  69pub struct DiffHunkStatus {
  70    pub kind: DiffHunkStatusKind,
  71    pub secondary: DiffHunkSecondaryStatus,
  72}
  73
  74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  75pub enum DiffHunkStatusKind {
  76    Added,
  77    Modified,
  78    Deleted,
  79}
  80
  81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  82/// Diff of Working Copy vs Index
  83/// aka 'is this hunk staged or not'
  84pub enum DiffHunkSecondaryStatus {
  85    /// Unstaged
  86    HasSecondaryHunk,
  87    /// Partially staged
  88    OverlapsWithSecondaryHunk,
  89    /// Staged
  90    NoSecondaryHunk,
  91    /// We are unstaging
  92    SecondaryHunkAdditionPending,
  93    /// We are stagind
  94    SecondaryHunkRemovalPending,
  95}
  96
  97/// A diff hunk resolved to rows in the buffer.
  98#[derive(Debug, Clone, PartialEq, Eq)]
  99pub struct DiffHunk {
 100    /// The buffer range as points.
 101    pub range: Range<Point>,
 102    /// The range in the buffer to which this hunk corresponds.
 103    pub buffer_range: Range<Anchor>,
 104    /// The range in the buffer's diff base text to which this hunk corresponds.
 105    pub diff_base_byte_range: Range<usize>,
 106    pub secondary_status: DiffHunkSecondaryStatus,
 107    // Anchors representing the word diff locations in the active buffer
 108    pub buffer_word_diffs: Vec<Range<Anchor>>,
 109    // Offsets relative to the start of the deleted diff that represent word diff locations
 110    pub base_word_diffs: Vec<Range<usize>>,
 111}
 112
 113/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
 114#[derive(Debug, Clone, PartialEq, Eq)]
 115struct InternalDiffHunk {
 116    buffer_range: Range<Anchor>,
 117    diff_base_byte_range: Range<usize>,
 118    base_word_diffs: Vec<Range<usize>>,
 119    buffer_word_diffs: Vec<Range<Anchor>>,
 120}
 121
 122#[derive(Debug, Clone, PartialEq, Eq)]
 123struct PendingHunk {
 124    buffer_range: Range<Anchor>,
 125    diff_base_byte_range: Range<usize>,
 126    buffer_version: clock::Global,
 127    new_status: DiffHunkSecondaryStatus,
 128}
 129
 130#[derive(Debug, Clone)]
 131pub struct DiffHunkSummary {
 132    buffer_range: Range<Anchor>,
 133    diff_base_byte_range: Range<usize>,
 134}
 135
 136impl sum_tree::Item for InternalDiffHunk {
 137    type Summary = DiffHunkSummary;
 138
 139    fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
 140        DiffHunkSummary {
 141            buffer_range: self.buffer_range.clone(),
 142            diff_base_byte_range: self.diff_base_byte_range.clone(),
 143        }
 144    }
 145}
 146
 147impl sum_tree::Item for PendingHunk {
 148    type Summary = DiffHunkSummary;
 149
 150    fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
 151        DiffHunkSummary {
 152            buffer_range: self.buffer_range.clone(),
 153            diff_base_byte_range: self.diff_base_byte_range.clone(),
 154        }
 155    }
 156}
 157
 158impl sum_tree::Summary for DiffHunkSummary {
 159    type Context<'a> = &'a text::BufferSnapshot;
 160
 161    fn zero(_cx: Self::Context<'_>) -> Self {
 162        DiffHunkSummary {
 163            buffer_range: Anchor::MIN..Anchor::MIN,
 164            diff_base_byte_range: 0..0,
 165        }
 166    }
 167
 168    fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
 169        self.buffer_range.start = *self
 170            .buffer_range
 171            .start
 172            .min(&other.buffer_range.start, buffer);
 173        self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
 174
 175        self.diff_base_byte_range.start = self
 176            .diff_base_byte_range
 177            .start
 178            .min(other.diff_base_byte_range.start);
 179        self.diff_base_byte_range.end = self
 180            .diff_base_byte_range
 181            .end
 182            .max(other.diff_base_byte_range.end);
 183    }
 184}
 185
 186impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
 187    fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
 188        if self
 189            .cmp(&cursor_location.buffer_range.start, buffer)
 190            .is_lt()
 191        {
 192            Ordering::Less
 193        } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
 194            Ordering::Greater
 195        } else {
 196            Ordering::Equal
 197        }
 198    }
 199}
 200
 201impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for usize {
 202    fn cmp(&self, cursor_location: &DiffHunkSummary, _cx: &text::BufferSnapshot) -> Ordering {
 203        if *self < cursor_location.diff_base_byte_range.start {
 204            Ordering::Less
 205        } else if *self > cursor_location.diff_base_byte_range.end {
 206            Ordering::Greater
 207        } else {
 208            Ordering::Equal
 209        }
 210    }
 211}
 212
 213impl std::fmt::Debug for BufferDiffInner<language::BufferSnapshot> {
 214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 215        f.debug_struct("BufferDiffSnapshot")
 216            .field("hunks", &self.hunks)
 217            .field("remote_id", &self.base_text.remote_id())
 218            .finish()
 219    }
 220}
 221
 222impl BufferDiffSnapshot {
 223    #[cfg(test)]
 224    fn new_sync(
 225        buffer: &text::BufferSnapshot,
 226        diff_base: String,
 227        cx: &mut gpui::TestAppContext,
 228    ) -> BufferDiffSnapshot {
 229        let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, buffer, cx));
 230        buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx))
 231    }
 232
 233    pub fn is_empty(&self) -> bool {
 234        self.inner.hunks.is_empty()
 235    }
 236
 237    pub fn base_text_string(&self) -> Option<String> {
 238        self.inner
 239            .base_text_exists
 240            .then(|| self.inner.base_text.text())
 241    }
 242
 243    pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
 244        self.secondary_diff.as_deref()
 245    }
 246
 247    pub fn buffer_version(&self) -> &clock::Global {
 248        self.inner.buffer_version()
 249    }
 250
 251    pub fn original_buffer_snapshot(&self) -> &text::BufferSnapshot {
 252        &self.inner.buffer_snapshot
 253    }
 254
 255    #[ztracing::instrument(skip_all)]
 256    pub fn hunks_intersecting_range<'a>(
 257        &'a self,
 258        range: Range<Anchor>,
 259        buffer: &'a text::BufferSnapshot,
 260    ) -> impl 'a + Iterator<Item = DiffHunk> {
 261        let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
 262        self.inner
 263            .hunks_intersecting_range(range, buffer, unstaged_counterpart)
 264    }
 265
 266    pub fn hunks_intersecting_range_rev<'a>(
 267        &'a self,
 268        range: Range<Anchor>,
 269        buffer: &'a text::BufferSnapshot,
 270    ) -> impl 'a + Iterator<Item = DiffHunk> {
 271        let filter = move |summary: &DiffHunkSummary| {
 272            let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
 273            let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
 274            !before_start && !after_end
 275        };
 276        self.inner.hunks_intersecting_range_rev_impl(filter, buffer)
 277    }
 278
 279    pub fn hunks_intersecting_base_text_range<'a>(
 280        &'a self,
 281        range: Range<usize>,
 282        main_buffer: &'a text::BufferSnapshot,
 283    ) -> impl 'a + Iterator<Item = DiffHunk> {
 284        let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
 285        let filter = move |summary: &DiffHunkSummary| {
 286            let before_start = summary.diff_base_byte_range.end < range.start;
 287            let after_end = summary.diff_base_byte_range.start > range.end;
 288            !before_start && !after_end
 289        };
 290        self.inner
 291            .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart)
 292    }
 293
 294    pub fn hunks_intersecting_base_text_range_rev<'a>(
 295        &'a self,
 296        range: Range<usize>,
 297        main_buffer: &'a text::BufferSnapshot,
 298    ) -> impl 'a + Iterator<Item = DiffHunk> {
 299        let filter = move |summary: &DiffHunkSummary| {
 300            let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt();
 301            let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt();
 302            !before_start && !after_end
 303        };
 304        self.inner
 305            .hunks_intersecting_range_rev_impl(filter, main_buffer)
 306    }
 307
 308    pub fn hunks<'a>(
 309        &'a self,
 310        buffer_snapshot: &'a text::BufferSnapshot,
 311    ) -> impl 'a + Iterator<Item = DiffHunk> {
 312        self.hunks_intersecting_range(
 313            Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
 314            buffer_snapshot,
 315        )
 316    }
 317
 318    pub fn hunks_in_row_range<'a>(
 319        &'a self,
 320        range: Range<u32>,
 321        buffer: &'a text::BufferSnapshot,
 322    ) -> impl 'a + Iterator<Item = DiffHunk> {
 323        let start = buffer.anchor_before(Point::new(range.start, 0));
 324        let end = buffer.anchor_after(Point::new(range.end, 0));
 325        self.hunks_intersecting_range(start..end, buffer)
 326    }
 327
 328    pub fn range_to_hunk_range(
 329        &self,
 330        range: Range<Anchor>,
 331        buffer: &text::BufferSnapshot,
 332    ) -> (Option<Range<Anchor>>, Option<Range<usize>>) {
 333        let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next();
 334        let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next();
 335        let range = first_hunk
 336            .as_ref()
 337            .zip(last_hunk.as_ref())
 338            .map(|(first, last)| first.buffer_range.start..last.buffer_range.end);
 339        let base_text_range = first_hunk
 340            .zip(last_hunk)
 341            .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end);
 342        (range, base_text_range)
 343    }
 344
 345    pub fn base_text(&self) -> &language::BufferSnapshot {
 346        &self.inner.base_text
 347    }
 348
 349    /// If this function returns `true`, the base texts are equal. If this
 350    /// function returns `false`, they might be equal, but might not. This
 351    /// result is used to avoid recalculating diffs in situations where we know
 352    /// nothing has changed.
 353    pub fn base_texts_definitely_eq(&self, other: &Self) -> bool {
 354        if self.inner.base_text_exists != other.inner.base_text_exists {
 355            return false;
 356        }
 357        let left = &self.inner.base_text;
 358        let right = &other.inner.base_text;
 359        let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty());
 360        let (new_id, new_version, new_empty) =
 361            (right.remote_id(), right.version(), right.is_empty());
 362        (new_id == old_id && new_version == old_version) || (new_empty && old_empty)
 363    }
 364
 365    #[allow(unused)]
 366    fn hunk_before_base_text_offset<'a>(
 367        &self,
 368        target: usize,
 369        cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
 370    ) -> Option<&'a InternalDiffHunk> {
 371        cursor.seek_forward(&target, Bias::Left);
 372        if cursor
 373            .item()
 374            .is_none_or(|hunk| target < hunk.diff_base_byte_range.start)
 375        {
 376            cursor.prev();
 377        }
 378        let result = cursor
 379            .item()
 380            .filter(|hunk| target >= hunk.diff_base_byte_range.start);
 381        if cursor.item().is_none() {
 382            cursor.reset();
 383        }
 384        result
 385    }
 386
 387    #[allow(unused)]
 388    fn hunk_before_buffer_anchor<'a>(
 389        &self,
 390        target: Anchor,
 391        cursor: &mut sum_tree::Cursor<'a, '_, InternalDiffHunk, DiffHunkSummary>,
 392        buffer: &text::BufferSnapshot,
 393    ) -> Option<&'a InternalDiffHunk> {
 394        cursor.seek_forward(&target, Bias::Left);
 395        if cursor
 396            .item()
 397            .is_none_or(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_lt())
 398        {
 399            cursor.prev();
 400        }
 401        let result = cursor
 402            .item()
 403            .filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge());
 404        if cursor.item().is_none() {
 405            cursor.reset();
 406        }
 407        result
 408    }
 409
 410    /// Returns a patch mapping the provided main buffer snapshot to the base text of this diff.
 411    ///
 412    /// The returned patch is guaranteed to be accurate for all main buffer points in the provided range,
 413    /// but not necessarily for points outside that range.
 414    pub fn patch_for_buffer_range<'a>(
 415        &'a self,
 416        _range: RangeInclusive<Point>,
 417        buffer: &'a text::BufferSnapshot,
 418    ) -> Patch<Point> {
 419        let original_snapshot = self.original_buffer_snapshot();
 420
 421        let edits_since: Vec<Edit<Point>> = buffer
 422            .edits_since::<Point>(original_snapshot.version())
 423            .collect();
 424        let mut inverted_edits_since = Patch::new(edits_since);
 425        inverted_edits_since.invert();
 426
 427        inverted_edits_since.compose(
 428            self.inner
 429                .hunks
 430                .iter()
 431                .map(|hunk| {
 432                    let old_start = hunk.buffer_range.start.to_point(original_snapshot);
 433                    let old_end = hunk.buffer_range.end.to_point(original_snapshot);
 434                    let new_start = self
 435                        .base_text()
 436                        .offset_to_point(hunk.diff_base_byte_range.start);
 437                    let new_end = self
 438                        .base_text()
 439                        .offset_to_point(hunk.diff_base_byte_range.end);
 440                    Edit {
 441                        old: old_start..old_end,
 442                        new: new_start..new_end,
 443                    }
 444                })
 445                .chain(
 446                    if !self.inner.base_text_exists && self.inner.hunks.is_empty() {
 447                        Some(Edit {
 448                            old: Point::zero()..original_snapshot.max_point(),
 449                            new: Point::zero()..Point::zero(),
 450                        })
 451                    } else {
 452                        None
 453                    },
 454                ),
 455        )
 456    }
 457
 458    /// Returns a patch mapping the base text of this diff to the provided buffer snapshot.
 459    ///
 460    /// The returned patch is guaranteed to be accurate for all base text points in the provided range,
 461    /// but not necessarily for points outside that range.
 462    pub fn patch_for_base_text_range<'a>(
 463        &'a self,
 464        _range: RangeInclusive<Point>,
 465        buffer: &'a text::BufferSnapshot,
 466    ) -> Patch<Point> {
 467        let original_snapshot = self.original_buffer_snapshot();
 468
 469        let mut hunk_edits: Vec<Edit<Point>> = Vec::new();
 470        for hunk in self.inner.hunks.iter() {
 471            let old_start = self
 472                .base_text()
 473                .offset_to_point(hunk.diff_base_byte_range.start);
 474            let old_end = self
 475                .base_text()
 476                .offset_to_point(hunk.diff_base_byte_range.end);
 477            let new_start = hunk.buffer_range.start.to_point(original_snapshot);
 478            let new_end = hunk.buffer_range.end.to_point(original_snapshot);
 479            hunk_edits.push(Edit {
 480                old: old_start..old_end,
 481                new: new_start..new_end,
 482            });
 483        }
 484        if !self.inner.base_text_exists && hunk_edits.is_empty() {
 485            hunk_edits.push(Edit {
 486                old: Point::zero()..Point::zero(),
 487                new: Point::zero()..original_snapshot.max_point(),
 488            })
 489        }
 490        let hunk_patch = Patch::new(hunk_edits);
 491
 492        hunk_patch.compose(buffer.edits_since::<Point>(original_snapshot.version()))
 493    }
 494
 495    pub fn buffer_point_to_base_text_range(
 496        &self,
 497        point: Point,
 498        buffer: &text::BufferSnapshot,
 499    ) -> Range<Point> {
 500        let patch = self.patch_for_buffer_range(point..=point, buffer);
 501        let edit = patch.edit_for_old_position(point);
 502        edit.new
 503    }
 504
 505    pub fn base_text_point_to_buffer_range(
 506        &self,
 507        point: Point,
 508        buffer: &text::BufferSnapshot,
 509    ) -> Range<Point> {
 510        let patch = self.patch_for_base_text_range(point..=point, buffer);
 511        let edit = patch.edit_for_old_position(point);
 512        edit.new
 513    }
 514
 515    pub fn buffer_point_to_base_text_point(
 516        &self,
 517        point: Point,
 518        buffer: &text::BufferSnapshot,
 519    ) -> Point {
 520        let patch = self.patch_for_buffer_range(point..=point, buffer);
 521        let edit = patch.edit_for_old_position(point);
 522        if point == edit.old.end {
 523            edit.new.end
 524        } else {
 525            edit.new.start
 526        }
 527    }
 528
 529    pub fn base_text_point_to_buffer_point(
 530        &self,
 531        point: Point,
 532        buffer: &text::BufferSnapshot,
 533    ) -> Point {
 534        let patch = self.patch_for_base_text_range(point..=point, buffer);
 535        let edit = patch.edit_for_old_position(point);
 536        if point == edit.old.end {
 537            edit.new.end
 538        } else {
 539            edit.new.start
 540        }
 541    }
 542}
 543
 544impl BufferDiffInner<Entity<language::Buffer>> {
 545    /// Returns the new index text and new pending hunks.
 546    fn stage_or_unstage_hunks_impl(
 547        &mut self,
 548        unstaged_diff: &Self,
 549        stage: bool,
 550        hunks: &[DiffHunk],
 551        buffer: &text::BufferSnapshot,
 552        file_exists: bool,
 553        cx: &mut Context<BufferDiff>,
 554    ) -> Option<Rope> {
 555        let head_text = self
 556            .base_text_exists
 557            .then(|| self.base_text.read(cx).as_rope().clone());
 558        let index_text = unstaged_diff
 559            .base_text_exists
 560            .then(|| unstaged_diff.base_text.read(cx).as_rope().clone());
 561
 562        // If the file doesn't exist in either HEAD or the index, then the
 563        // entire file must be either created or deleted in the index.
 564        let (index_text, head_text) = match (index_text, head_text) {
 565            (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
 566            (index_text, head_text) => {
 567                let (new_index_text, new_status) = if stage {
 568                    log::debug!("stage all");
 569                    (
 570                        file_exists.then(|| buffer.as_rope().clone()),
 571                        DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
 572                    )
 573                } else {
 574                    log::debug!("unstage all");
 575                    (
 576                        head_text,
 577                        DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
 578                    )
 579                };
 580
 581                let hunk = PendingHunk {
 582                    buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
 583                    diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
 584                    buffer_version: buffer.version().clone(),
 585                    new_status,
 586                };
 587                self.pending_hunks = SumTree::from_item(hunk, buffer);
 588                return new_index_text;
 589            }
 590        };
 591
 592        let mut pending_hunks = SumTree::new(buffer);
 593        let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
 594
 595        // first, merge new hunks into pending_hunks
 596        for DiffHunk {
 597            buffer_range,
 598            diff_base_byte_range,
 599            secondary_status,
 600            ..
 601        } in hunks.iter().cloned()
 602        {
 603            let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left);
 604            pending_hunks.append(preceding_pending_hunks, buffer);
 605
 606            // Skip all overlapping or adjacent old pending hunks
 607            while old_pending_hunks.item().is_some_and(|old_hunk| {
 608                old_hunk
 609                    .buffer_range
 610                    .start
 611                    .cmp(&buffer_range.end, buffer)
 612                    .is_le()
 613            }) {
 614                old_pending_hunks.next();
 615            }
 616
 617            if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
 618                || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
 619            {
 620                continue;
 621            }
 622
 623            pending_hunks.push(
 624                PendingHunk {
 625                    buffer_range,
 626                    diff_base_byte_range,
 627                    buffer_version: buffer.version().clone(),
 628                    new_status: if stage {
 629                        DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
 630                    } else {
 631                        DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
 632                    },
 633                },
 634                buffer,
 635            );
 636        }
 637        // append the remainder
 638        pending_hunks.append(old_pending_hunks.suffix(), buffer);
 639
 640        let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
 641        unstaged_hunk_cursor.next();
 642
 643        // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
 644        let mut prev_unstaged_hunk_buffer_end = 0;
 645        let mut prev_unstaged_hunk_base_text_end = 0;
 646        let mut edits = Vec::<(Range<usize>, String)>::new();
 647        let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
 648        while let Some(PendingHunk {
 649            buffer_range,
 650            diff_base_byte_range,
 651            new_status,
 652            ..
 653        }) = pending_hunks_iter.next()
 654        {
 655            // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
 656            let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);
 657
 658            if let Some(unstaged_hunk) = skipped_unstaged.last() {
 659                prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
 660                prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
 661            }
 662
 663            // Find where this hunk is in the index if it doesn't overlap
 664            let mut buffer_offset_range = buffer_range.to_offset(buffer);
 665            let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
 666            let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
 667
 668            loop {
 669                // Merge this hunk with any overlapping unstaged hunks.
 670                if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
 671                    let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
 672                    if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
 673                        prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
 674                        prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
 675
 676                        index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
 677                        buffer_offset_range.start = buffer_offset_range
 678                            .start
 679                            .min(unstaged_hunk_offset_range.start);
 680                        buffer_offset_range.end =
 681                            buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
 682
 683                        unstaged_hunk_cursor.next();
 684                        continue;
 685                    }
 686                }
 687
 688                // If any unstaged hunks were merged, then subsequent pending hunks may
 689                // now overlap this hunk. Merge them.
 690                if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
 691                    let next_pending_hunk_offset_range =
 692                        next_pending_hunk.buffer_range.to_offset(buffer);
 693                    if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
 694                        buffer_offset_range.end = buffer_offset_range
 695                            .end
 696                            .max(next_pending_hunk_offset_range.end);
 697                        pending_hunks_iter.next();
 698                        continue;
 699                    }
 700                }
 701
 702                break;
 703            }
 704
 705            let end_overshoot = buffer_offset_range
 706                .end
 707                .saturating_sub(prev_unstaged_hunk_buffer_end);
 708            let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
 709            let index_byte_range = index_start..index_end;
 710
 711            let replacement_text = match new_status {
 712                DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
 713                    log::debug!("staging hunk {:?}", buffer_offset_range);
 714                    buffer
 715                        .text_for_range(buffer_offset_range)
 716                        .collect::<String>()
 717                }
 718                DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
 719                    log::debug!("unstaging hunk {:?}", buffer_offset_range);
 720                    head_text
 721                        .chunks_in_range(diff_base_byte_range.clone())
 722                        .collect::<String>()
 723                }
 724                _ => {
 725                    debug_assert!(false);
 726                    continue;
 727                }
 728            };
 729
 730            edits.push((index_byte_range, replacement_text));
 731        }
 732        drop(pending_hunks_iter);
 733        drop(old_pending_hunks);
 734        self.pending_hunks = pending_hunks;
 735
 736        #[cfg(debug_assertions)] // invariants: non-overlapping and sorted
 737        {
 738            for window in edits.windows(2) {
 739                let (range_a, range_b) = (&window[0].0, &window[1].0);
 740                debug_assert!(range_a.end < range_b.start);
 741            }
 742        }
 743
 744        let mut new_index_text = Rope::new();
 745        let mut index_cursor = index_text.cursor(0);
 746
 747        for (old_range, replacement_text) in edits {
 748            new_index_text.append(index_cursor.slice(old_range.start));
 749            index_cursor.seek_forward(old_range.end);
 750            new_index_text.push(&replacement_text);
 751        }
 752        new_index_text.append(index_cursor.suffix());
 753        Some(new_index_text)
 754    }
 755}
 756
 757impl BufferDiffInner<language::BufferSnapshot> {
 758    fn hunks_intersecting_range<'a>(
 759        &'a self,
 760        range: Range<Anchor>,
 761        buffer: &'a text::BufferSnapshot,
 762        secondary: Option<&'a Self>,
 763    ) -> impl 'a + Iterator<Item = DiffHunk> {
 764        let range = range.to_offset(buffer);
 765        let filter = move |summary: &DiffHunkSummary| {
 766            let summary_range = summary.buffer_range.to_offset(buffer);
 767            let before_start = summary_range.end < range.start;
 768            let after_end = summary_range.start > range.end;
 769            !before_start && !after_end
 770        };
 771        self.hunks_intersecting_range_impl(filter, buffer, secondary)
 772    }
 773
 774    fn hunks_intersecting_range_impl<'a>(
 775        &'a self,
 776        filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
 777        buffer: &'a text::BufferSnapshot,
 778        secondary: Option<&'a Self>,
 779    ) -> impl 'a + Iterator<Item = DiffHunk> {
 780        let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
 781
 782        let anchor_iter = iter::from_fn(move || {
 783            cursor.next();
 784            cursor.item()
 785        })
 786        .flat_map(move |hunk| {
 787            [
 788                (
 789                    &hunk.buffer_range.start,
 790                    (
 791                        hunk.buffer_range.start,
 792                        hunk.diff_base_byte_range.start,
 793                        hunk,
 794                    ),
 795                ),
 796                (
 797                    &hunk.buffer_range.end,
 798                    (hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
 799                ),
 800            ]
 801        });
 802
 803        let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
 804        pending_hunks_cursor.next();
 805
 806        let mut secondary_cursor = None;
 807        if let Some(secondary) = secondary.as_ref() {
 808            let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
 809            cursor.next();
 810            secondary_cursor = Some(cursor);
 811        }
 812
 813        let max_point = buffer.max_point();
 814        let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
 815        iter::from_fn(move || {
 816            loop {
 817                let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
 818                let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
 819
 820                let base_word_diffs = hunk.base_word_diffs.clone();
 821                let buffer_word_diffs = hunk.buffer_word_diffs.clone();
 822
 823                if !start_anchor.is_valid(buffer) {
 824                    continue;
 825                }
 826
 827                if end_point.column > 0 && end_point < max_point {
 828                    end_point.row += 1;
 829                    end_point.column = 0;
 830                    end_anchor = buffer.anchor_before(end_point);
 831                }
 832
 833                let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
 834
 835                let mut has_pending = false;
 836                if start_anchor
 837                    .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
 838                    .is_gt()
 839                {
 840                    pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left);
 841                }
 842
 843                if let Some(pending_hunk) = pending_hunks_cursor.item() {
 844                    let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
 845                    if pending_range.end.column > 0 {
 846                        pending_range.end.row += 1;
 847                        pending_range.end.column = 0;
 848                    }
 849
 850                    if pending_range == (start_point..end_point)
 851                        && !buffer.has_edits_since_in_range(
 852                            &pending_hunk.buffer_version,
 853                            start_anchor..end_anchor,
 854                        )
 855                    {
 856                        has_pending = true;
 857                        secondary_status = pending_hunk.new_status;
 858                    }
 859                }
 860
 861                if let (Some(secondary_cursor), false) = (secondary_cursor.as_mut(), has_pending) {
 862                    if start_anchor
 863                        .cmp(&secondary_cursor.start().buffer_range.start, buffer)
 864                        .is_gt()
 865                    {
 866                        secondary_cursor.seek_forward(&start_anchor, Bias::Left);
 867                    }
 868
 869                    if let Some(secondary_hunk) = secondary_cursor.item() {
 870                        let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
 871                        if secondary_range.end.column > 0 {
 872                            secondary_range.end.row += 1;
 873                            secondary_range.end.column = 0;
 874                        }
 875                        if secondary_range.is_empty()
 876                            && secondary_hunk.diff_base_byte_range.is_empty()
 877                        {
 878                            // ignore
 879                        } else if secondary_range == (start_point..end_point) {
 880                            secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
 881                        } else if secondary_range.start <= end_point {
 882                            secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
 883                        }
 884                    }
 885                }
 886
 887                return Some(DiffHunk {
 888                    range: start_point..end_point,
 889                    diff_base_byte_range: start_base..end_base,
 890                    buffer_range: start_anchor..end_anchor,
 891                    base_word_diffs,
 892                    buffer_word_diffs,
 893                    secondary_status,
 894                });
 895            }
 896        })
 897    }
 898
 899    fn hunks_intersecting_range_rev_impl<'a>(
 900        &'a self,
 901        filter: impl 'a + Fn(&DiffHunkSummary) -> bool,
 902        buffer: &'a text::BufferSnapshot,
 903    ) -> impl 'a + Iterator<Item = DiffHunk> {
 904        let mut cursor = self.hunks.filter::<_, DiffHunkSummary>(buffer, filter);
 905
 906        iter::from_fn(move || {
 907            cursor.prev();
 908
 909            let hunk = cursor.item()?;
 910            let range = hunk.buffer_range.to_point(buffer);
 911
 912            Some(DiffHunk {
 913                range,
 914                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
 915                buffer_range: hunk.buffer_range.clone(),
 916                // The secondary status is not used by callers of this method.
 917                secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
 918                base_word_diffs: hunk.base_word_diffs.clone(),
 919                buffer_word_diffs: hunk.buffer_word_diffs.clone(),
 920            })
 921        })
 922    }
 923}
 924
 925fn build_diff_options(
 926    file: Option<&Arc<dyn File>>,
 927    language: Option<LanguageName>,
 928    language_scope: Option<language::LanguageScope>,
 929    cx: &App,
 930) -> Option<DiffOptions> {
 931    #[cfg(any(test, feature = "test-support"))]
 932    {
 933        if !cx.has_global::<settings::SettingsStore>() {
 934            return Some(DiffOptions {
 935                language_scope,
 936                max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
 937                ..Default::default()
 938            });
 939        }
 940    }
 941
 942    language_settings(language, file, cx)
 943        .word_diff_enabled
 944        .then_some(DiffOptions {
 945            language_scope,
 946            max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
 947            ..Default::default()
 948        })
 949}
 950
 951fn compute_hunks(
 952    diff_base: Option<(Arc<str>, Rope)>,
 953    buffer: &text::BufferSnapshot,
 954    diff_options: Option<DiffOptions>,
 955) -> SumTree<InternalDiffHunk> {
 956    let mut tree = SumTree::new(buffer);
 957
 958    if let Some((diff_base, diff_base_rope)) = diff_base {
 959        let buffer_text = buffer.as_rope().to_string();
 960
 961        let mut options = GitOptions::default();
 962        options.context_lines(0);
 963        let patch = GitPatch::from_buffers(
 964            diff_base.as_bytes(),
 965            None,
 966            buffer_text.as_bytes(),
 967            None,
 968            Some(&mut options),
 969        )
 970        .log_err();
 971
 972        // A common case in Zed is that the empty buffer is represented as just a newline,
 973        // but if we just compute a naive diff you get a "preserved" line in the middle,
 974        // which is a bit odd.
 975        if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
 976            tree.push(
 977                InternalDiffHunk {
 978                    buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
 979                    diff_base_byte_range: 0..diff_base.len() - 1,
 980                    base_word_diffs: Vec::default(),
 981                    buffer_word_diffs: Vec::default(),
 982                },
 983                buffer,
 984            );
 985            return tree;
 986        }
 987
 988        if let Some(patch) = patch {
 989            let mut divergence = 0;
 990            for hunk_index in 0..patch.num_hunks() {
 991                let hunk = process_patch_hunk(
 992                    &patch,
 993                    hunk_index,
 994                    &diff_base_rope,
 995                    buffer,
 996                    &mut divergence,
 997                    diff_options.as_ref(),
 998                );
 999                tree.push(hunk, buffer);
1000            }
1001        }
1002    } else {
1003        tree.push(
1004            InternalDiffHunk {
1005                buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
1006                diff_base_byte_range: 0..0,
1007                base_word_diffs: Vec::default(),
1008                buffer_word_diffs: Vec::default(),
1009            },
1010            buffer,
1011        );
1012    }
1013
1014    tree
1015}
1016
1017fn compare_hunks(
1018    new_hunks: &SumTree<InternalDiffHunk>,
1019    old_hunks: &SumTree<InternalDiffHunk>,
1020    old_snapshot: &text::BufferSnapshot,
1021    new_snapshot: &text::BufferSnapshot,
1022    old_base_text: &text::BufferSnapshot,
1023    new_base_text: &text::BufferSnapshot,
1024) -> DiffChanged {
1025    let mut new_cursor = new_hunks.cursor::<()>(new_snapshot);
1026    let mut old_cursor = old_hunks.cursor::<()>(new_snapshot);
1027    old_cursor.next();
1028    new_cursor.next();
1029    let mut start = None;
1030    let mut end = None;
1031    let mut base_text_start: Option<Anchor> = None;
1032    let mut base_text_end: Option<Anchor> = None;
1033
1034    let mut last_unchanged_new_hunk_end: Option<text::Anchor> = None;
1035    let mut has_changes = false;
1036    let mut extended_end_candidate: Option<text::Anchor> = None;
1037
1038    loop {
1039        match (new_cursor.item(), old_cursor.item()) {
1040            (Some(new_hunk), Some(old_hunk)) => {
1041                match new_hunk
1042                    .buffer_range
1043                    .start
1044                    .cmp(&old_hunk.buffer_range.start, new_snapshot)
1045                {
1046                    Ordering::Less => {
1047                        has_changes = true;
1048                        extended_end_candidate = None;
1049                        start.get_or_insert(new_hunk.buffer_range.start);
1050                        base_text_start.get_or_insert(
1051                            new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1052                        );
1053                        end.replace(new_hunk.buffer_range.end);
1054                        let new_diff_range_end =
1055                            new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1056                        if base_text_end.is_none_or(|base_text_end| {
1057                            new_diff_range_end
1058                                .cmp(&base_text_end, &new_base_text)
1059                                .is_gt()
1060                        }) {
1061                            base_text_end = Some(new_diff_range_end)
1062                        }
1063                        new_cursor.next();
1064                    }
1065                    Ordering::Equal => {
1066                        if new_hunk != old_hunk {
1067                            has_changes = true;
1068                            extended_end_candidate = None;
1069                            start.get_or_insert(new_hunk.buffer_range.start);
1070                            base_text_start.get_or_insert(
1071                                new_base_text.anchor_before(new_hunk.diff_base_byte_range.start),
1072                            );
1073                            if old_hunk
1074                                .buffer_range
1075                                .end
1076                                .cmp(&new_hunk.buffer_range.end, new_snapshot)
1077                                .is_ge()
1078                            {
1079                                end.replace(old_hunk.buffer_range.end);
1080                            } else {
1081                                end.replace(new_hunk.buffer_range.end);
1082                            }
1083
1084                            let old_hunk_diff_base_range_end =
1085                                old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1086                            let new_hunk_diff_base_range_end =
1087                                new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1088
1089                            base_text_end.replace(
1090                                *old_hunk_diff_base_range_end
1091                                    .max(&new_hunk_diff_base_range_end, new_base_text),
1092                            );
1093                        } else {
1094                            if !has_changes {
1095                                last_unchanged_new_hunk_end = Some(new_hunk.buffer_range.end);
1096                            } else if extended_end_candidate.is_none() {
1097                                extended_end_candidate = Some(new_hunk.buffer_range.start);
1098                            }
1099                        }
1100
1101                        new_cursor.next();
1102                        old_cursor.next();
1103                    }
1104                    Ordering::Greater => {
1105                        has_changes = true;
1106                        extended_end_candidate = None;
1107                        start.get_or_insert(old_hunk.buffer_range.start);
1108                        base_text_start.get_or_insert(
1109                            old_base_text.anchor_after(old_hunk.diff_base_byte_range.start),
1110                        );
1111                        end.replace(old_hunk.buffer_range.end);
1112                        let old_diff_range_end =
1113                            old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1114                        if base_text_end.is_none_or(|base_text_end| {
1115                            old_diff_range_end
1116                                .cmp(&base_text_end, new_base_text)
1117                                .is_gt()
1118                        }) {
1119                            base_text_end = Some(old_diff_range_end)
1120                        }
1121                        old_cursor.next();
1122                    }
1123                }
1124            }
1125            (Some(new_hunk), None) => {
1126                has_changes = true;
1127                extended_end_candidate = None;
1128                start.get_or_insert(new_hunk.buffer_range.start);
1129                base_text_start
1130                    .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start));
1131                if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le())
1132                {
1133                    end.replace(new_hunk.buffer_range.end);
1134                }
1135                let new_base_text_end =
1136                    new_base_text.anchor_after(new_hunk.diff_base_byte_range.end);
1137                if base_text_end.is_none_or(|base_text_end| {
1138                    new_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1139                }) {
1140                    base_text_end = Some(new_base_text_end)
1141                }
1142                new_cursor.next();
1143            }
1144            (None, Some(old_hunk)) => {
1145                has_changes = true;
1146                extended_end_candidate = None;
1147                start.get_or_insert(old_hunk.buffer_range.start);
1148                base_text_start
1149                    .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start));
1150                if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le())
1151                {
1152                    end.replace(old_hunk.buffer_range.end);
1153                }
1154                let old_base_text_end =
1155                    old_base_text.anchor_after(old_hunk.diff_base_byte_range.end);
1156                if base_text_end.is_none_or(|base_text_end| {
1157                    old_base_text_end.cmp(&base_text_end, new_base_text).is_gt()
1158                }) {
1159                    base_text_end = Some(old_base_text_end);
1160                }
1161                old_cursor.next();
1162            }
1163            (None, None) => break,
1164        }
1165    }
1166
1167    let changed_range = start.zip(end).map(|(start, end)| start..end);
1168    let base_text_changed_range = base_text_start
1169        .zip(base_text_end)
1170        .map(|(start, end)| (start..end).to_offset(new_base_text));
1171
1172    let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() {
1173        let extended_start = *last_unchanged_new_hunk_end
1174            .unwrap_or(text::Anchor::min_for_buffer(new_snapshot.remote_id()))
1175            .min(&changed_range.start, new_snapshot);
1176        let extended_start = new_snapshot
1177            .anchored_edits_since_in_range::<usize>(
1178                &old_snapshot.version(),
1179                extended_start..changed_range.start,
1180            )
1181            .map(|(_, anchors)| anchors.start)
1182            .min_by(|a, b| a.cmp(b, new_snapshot))
1183            .unwrap_or(changed_range.start);
1184
1185        let extended_end = *extended_end_candidate
1186            .unwrap_or(text::Anchor::max_for_buffer(new_snapshot.remote_id()))
1187            .max(&changed_range.end, new_snapshot);
1188        let extended_end = new_snapshot
1189            .anchored_edits_since_in_range::<usize>(
1190                &old_snapshot.version(),
1191                changed_range.end..extended_end,
1192            )
1193            .map(|(_, anchors)| anchors.end)
1194            .max_by(|a, b| a.cmp(b, new_snapshot))
1195            .unwrap_or(changed_range.end);
1196
1197        Some(extended_start..extended_end)
1198    } else {
1199        None
1200    };
1201
1202    DiffChanged {
1203        changed_range,
1204        base_text_changed_range,
1205        extended_range,
1206    }
1207}
1208
1209fn process_patch_hunk(
1210    patch: &GitPatch<'_>,
1211    hunk_index: usize,
1212    diff_base: &Rope,
1213    buffer: &text::BufferSnapshot,
1214    buffer_row_divergence: &mut i64,
1215    diff_options: Option<&DiffOptions>,
1216) -> InternalDiffHunk {
1217    let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
1218    assert!(line_item_count > 0);
1219
1220    let mut first_deletion_buffer_row: Option<u32> = None;
1221    let mut buffer_row_range: Option<Range<u32>> = None;
1222    let mut diff_base_byte_range: Option<Range<usize>> = None;
1223    let mut first_addition_old_row: Option<u32> = None;
1224
1225    for line_index in 0..line_item_count {
1226        let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
1227        let kind = line.origin_value();
1228        let content_offset = line.content_offset() as isize;
1229        let content_len = line.content().len() as isize;
1230        match kind {
1231            GitDiffLineType::Addition => {
1232                if first_addition_old_row.is_none() {
1233                    first_addition_old_row = Some(
1234                        (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
1235                    );
1236                }
1237                *buffer_row_divergence += 1;
1238                let row = line.new_lineno().unwrap().saturating_sub(1);
1239
1240                match &mut buffer_row_range {
1241                    Some(Range { end, .. }) => *end = row + 1,
1242                    None => buffer_row_range = Some(row..row + 1),
1243                }
1244            }
1245            GitDiffLineType::Deletion => {
1246                let end = content_offset + content_len;
1247
1248                match &mut diff_base_byte_range {
1249                    Some(head_byte_range) => head_byte_range.end = end as usize,
1250                    None => diff_base_byte_range = Some(content_offset as usize..end as usize),
1251                }
1252
1253                if first_deletion_buffer_row.is_none() {
1254                    let old_row = line.old_lineno().unwrap().saturating_sub(1);
1255                    let row = old_row as i64 + *buffer_row_divergence;
1256                    first_deletion_buffer_row = Some(row as u32);
1257                }
1258
1259                *buffer_row_divergence -= 1;
1260            }
1261            _ => {}
1262        }
1263    }
1264
1265    let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
1266        // Pure deletion hunk without addition.
1267        let row = first_deletion_buffer_row.unwrap();
1268        row..row
1269    });
1270    let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
1271        // Pure addition hunk without deletion.
1272        let row = first_addition_old_row.unwrap();
1273        let offset = diff_base.point_to_offset(Point::new(row, 0));
1274        offset..offset
1275    });
1276
1277    let start = Point::new(buffer_row_range.start, 0);
1278    let end = Point::new(buffer_row_range.end, 0);
1279    let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
1280
1281    let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
1282
1283    let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
1284        && !buffer_row_range.is_empty()
1285        && base_line_count == buffer_row_range.len()
1286        && diff_options.max_word_diff_line_count >= base_line_count
1287    {
1288        let base_text: String = diff_base
1289            .chunks_in_range(diff_base_byte_range.clone())
1290            .collect();
1291
1292        let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
1293
1294        let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
1295            &base_text,
1296            &buffer_text,
1297            DiffOptions {
1298                language_scope: diff_options.language_scope.clone(),
1299                ..*diff_options
1300            },
1301        );
1302
1303        let buffer_start_offset = buffer_range.start.to_offset(buffer);
1304        let buffer_word_diffs = buffer_word_diffs_relative
1305            .into_iter()
1306            .map(|range| {
1307                let start = buffer.anchor_after(buffer_start_offset + range.start);
1308                let end = buffer.anchor_after(buffer_start_offset + range.end);
1309                start..end
1310            })
1311            .collect();
1312
1313        (base_word_diffs, buffer_word_diffs)
1314    } else {
1315        (Vec::default(), Vec::default())
1316    };
1317
1318    InternalDiffHunk {
1319        buffer_range,
1320        diff_base_byte_range,
1321        base_word_diffs,
1322        buffer_word_diffs,
1323    }
1324}
1325
1326impl std::fmt::Debug for BufferDiff {
1327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1328        f.debug_struct("BufferChangeSet")
1329            .field("buffer_id", &self.buffer_id)
1330            .finish()
1331    }
1332}
1333
1334#[derive(Clone, Debug, Default)]
1335pub struct DiffChanged {
1336    pub changed_range: Option<Range<text::Anchor>>,
1337    pub base_text_changed_range: Option<Range<usize>>,
1338    pub extended_range: Option<Range<text::Anchor>>,
1339}
1340
1341#[derive(Clone, Debug)]
1342pub enum BufferDiffEvent {
1343    DiffChanged(DiffChanged),
1344    LanguageChanged,
1345    HunksStagedOrUnstaged(Option<Rope>),
1346}
1347
1348impl EventEmitter<BufferDiffEvent> for BufferDiff {}
1349
1350impl BufferDiff {
1351    pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
1352        let base_text = cx.new(|cx| {
1353            let mut buffer = language::Buffer::local("", cx);
1354            buffer.set_capability(Capability::ReadOnly, cx);
1355            buffer
1356        });
1357
1358        BufferDiff {
1359            buffer_id: buffer.remote_id(),
1360            inner: BufferDiffInner {
1361                base_text,
1362                hunks: SumTree::new(buffer),
1363                pending_hunks: SumTree::new(buffer),
1364                base_text_exists: false,
1365                buffer_snapshot: buffer.clone(),
1366            },
1367            secondary_diff: None,
1368        }
1369    }
1370
1371    pub fn new_unchanged(buffer: &text::BufferSnapshot, cx: &mut Context<Self>) -> Self {
1372        let base_text = buffer.text();
1373        let base_text = cx.new(|cx| {
1374            let mut buffer = language::Buffer::local(base_text, cx);
1375            buffer.set_capability(Capability::ReadOnly, cx);
1376            buffer
1377        });
1378
1379        BufferDiff {
1380            buffer_id: buffer.remote_id(),
1381            inner: BufferDiffInner {
1382                base_text,
1383                hunks: SumTree::new(buffer),
1384                pending_hunks: SumTree::new(buffer),
1385                base_text_exists: true,
1386                buffer_snapshot: buffer.clone(),
1387            },
1388            secondary_diff: None,
1389        }
1390    }
1391
1392    #[cfg(any(test, feature = "test-support"))]
1393    pub fn new_with_base_text(
1394        base_text: &str,
1395        buffer: &text::BufferSnapshot,
1396        cx: &mut Context<Self>,
1397    ) -> Self {
1398        let mut this = BufferDiff::new(&buffer, cx);
1399        let mut base_text = base_text.to_owned();
1400        text::LineEnding::normalize(&mut base_text);
1401        let inner = cx.foreground_executor().block_on(this.update_diff(
1402            buffer.clone(),
1403            Some(Arc::from(base_text)),
1404            Some(false),
1405            None,
1406            cx,
1407        ));
1408        this.set_snapshot(inner, &buffer, cx).detach();
1409        this
1410    }
1411
1412    pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
1413        self.secondary_diff = Some(diff);
1414    }
1415
1416    pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
1417        self.secondary_diff.clone()
1418    }
1419
1420    pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
1421        if self.secondary_diff.is_some() {
1422            self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
1423                buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
1424                diff_base_byte_range: 0..0,
1425            });
1426            let changed_range = Some(Anchor::min_max_range_for_buffer(self.buffer_id));
1427            let base_text_range = Some(0..self.base_text(cx).len());
1428            cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1429                changed_range: changed_range.clone(),
1430                base_text_changed_range: base_text_range,
1431                extended_range: changed_range,
1432            }));
1433        }
1434    }
1435
1436    pub fn stage_or_unstage_hunks(
1437        &mut self,
1438        stage: bool,
1439        hunks: &[DiffHunk],
1440        buffer: &text::BufferSnapshot,
1441        file_exists: bool,
1442        cx: &mut Context<Self>,
1443    ) -> Option<Rope> {
1444        let new_index_text = self
1445            .secondary_diff
1446            .as_ref()?
1447            .update(cx, |secondary_diff, cx| {
1448                self.inner.stage_or_unstage_hunks_impl(
1449                    &secondary_diff.inner,
1450                    stage,
1451                    hunks,
1452                    buffer,
1453                    file_exists,
1454                    cx,
1455                )
1456            });
1457
1458        cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
1459            new_index_text.clone(),
1460        ));
1461        if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1462            let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1463            let base_text_changed_range =
1464                Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1465            cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1466                changed_range: changed_range.clone(),
1467                base_text_changed_range,
1468                extended_range: changed_range,
1469            }));
1470        }
1471        new_index_text
1472    }
1473
1474    pub fn stage_or_unstage_all_hunks(
1475        &mut self,
1476        stage: bool,
1477        buffer: &text::BufferSnapshot,
1478        file_exists: bool,
1479        cx: &mut Context<Self>,
1480    ) {
1481        let hunks = self
1482            .snapshot(cx)
1483            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
1484            .collect::<Vec<_>>();
1485        let Some(secondary) = self.secondary_diff.clone() else {
1486            return;
1487        };
1488        let secondary = secondary.read(cx).inner.clone();
1489        self.inner
1490            .stage_or_unstage_hunks_impl(&secondary, stage, &hunks, buffer, file_exists, cx);
1491        if let Some((first, last)) = hunks.first().zip(hunks.last()) {
1492            let changed_range = Some(first.buffer_range.start..last.buffer_range.end);
1493            let base_text_changed_range =
1494                Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1495            cx.emit(BufferDiffEvent::DiffChanged(DiffChanged {
1496                changed_range: changed_range.clone(),
1497                base_text_changed_range,
1498                extended_range: changed_range,
1499            }));
1500        }
1501    }
1502
1503    pub fn update_diff(
1504        &self,
1505        buffer: text::BufferSnapshot,
1506        base_text: Option<Arc<str>>,
1507        base_text_change: Option<bool>,
1508        language: Option<Arc<Language>>,
1509        cx: &App,
1510    ) -> Task<BufferDiffUpdate> {
1511        let prev_base_text = self.base_text(cx).as_rope().clone();
1512        let base_text_changed = base_text_change.is_some();
1513        let compute_base_text_edits = base_text_change == Some(true);
1514        let diff_options = build_diff_options(
1515            None,
1516            language.as_ref().map(|l| l.name()),
1517            language.as_ref().map(|l| l.default_scope()),
1518            cx,
1519        );
1520        let buffer_snapshot = buffer.clone();
1521
1522        let base_text_diff_task = if base_text_changed && compute_base_text_edits {
1523            base_text
1524                .as_ref()
1525                .map(|new_text| self.inner.base_text.read(cx).diff(new_text.clone(), cx))
1526        } else {
1527            None
1528        };
1529
1530        let hunk_task = cx.background_executor().spawn({
1531            let buffer_snapshot = buffer_snapshot.clone();
1532            async move {
1533                let base_text_rope = if let Some(base_text) = &base_text {
1534                    if base_text_changed {
1535                        Rope::from(base_text.as_ref())
1536                    } else {
1537                        prev_base_text
1538                    }
1539                } else {
1540                    Rope::new()
1541                };
1542                let base_text_exists = base_text.is_some();
1543                let hunks = compute_hunks(
1544                    base_text
1545                        .clone()
1546                        .map(|base_text| (base_text, base_text_rope.clone())),
1547                    &buffer,
1548                    diff_options,
1549                );
1550                let base_text = base_text.unwrap_or_default();
1551                BufferDiffInner {
1552                    base_text,
1553                    hunks,
1554                    base_text_exists,
1555                    pending_hunks: SumTree::new(&buffer),
1556                    buffer_snapshot,
1557                }
1558            }
1559        });
1560
1561        cx.background_executor().spawn(async move {
1562            let (inner, base_text_edits) = match base_text_diff_task {
1563                Some(diff_task) => {
1564                    let (inner, diff) = futures::join!(hunk_task, diff_task);
1565                    (inner, Some(diff))
1566                }
1567                None => (hunk_task.await, None),
1568            };
1569
1570            BufferDiffUpdate {
1571                inner,
1572                buffer_snapshot,
1573                base_text_edits,
1574                base_text_changed,
1575            }
1576        })
1577    }
1578
1579    #[ztracing::instrument(skip_all)]
1580    pub fn language_changed(
1581        &mut self,
1582        language: Option<Arc<Language>>,
1583        language_registry: Option<Arc<LanguageRegistry>>,
1584        cx: &mut Context<Self>,
1585    ) {
1586        let fut = self.inner.base_text.update(cx, |base_text, cx| {
1587            if let Some(language_registry) = language_registry {
1588                base_text.set_language_registry(language_registry);
1589            }
1590            base_text.set_language(language, cx);
1591            base_text.parsing_idle()
1592        });
1593        cx.spawn(async move |this, cx| {
1594            fut.await;
1595            this.update(cx, |_, cx| {
1596                cx.emit(BufferDiffEvent::LanguageChanged);
1597            })
1598            .ok();
1599        })
1600        .detach();
1601    }
1602
1603    fn set_snapshot_with_secondary_inner(
1604        &mut self,
1605        update: BufferDiffUpdate,
1606        buffer: &text::BufferSnapshot,
1607        secondary_diff_change: Option<Range<Anchor>>,
1608        clear_pending_hunks: bool,
1609        cx: &mut Context<Self>,
1610    ) -> impl Future<Output = DiffChanged> + use<> {
1611        log::debug!("set snapshot with secondary {secondary_diff_change:?}");
1612
1613        let old_snapshot = self.snapshot(cx);
1614        let new_state = update.inner;
1615        let base_text_changed = update.base_text_changed;
1616
1617        let state = &mut self.inner;
1618        state.base_text_exists = new_state.base_text_exists;
1619        let should_compare_hunks = update.base_text_edits.is_some() || !base_text_changed;
1620        let parsing_idle = if let Some(diff) = update.base_text_edits {
1621            state.base_text.update(cx, |base_text, cx| {
1622                base_text.set_capability(Capability::ReadWrite, cx);
1623                base_text.apply_diff(diff, cx);
1624                base_text.set_capability(Capability::ReadOnly, cx);
1625                Some(base_text.parsing_idle())
1626            })
1627        } else if update.base_text_changed {
1628            state.base_text.update(cx, |base_text, cx| {
1629                base_text.set_capability(Capability::ReadWrite, cx);
1630                base_text.set_text(new_state.base_text.clone(), cx);
1631                base_text.set_capability(Capability::ReadOnly, cx);
1632                Some(base_text.parsing_idle())
1633            })
1634        } else {
1635            None
1636        };
1637
1638        let old_buffer_snapshot = &old_snapshot.inner.buffer_snapshot;
1639        let old_base_snapshot = &old_snapshot.inner.base_text;
1640        let new_base_snapshot = state.base_text.read(cx).snapshot();
1641        let DiffChanged {
1642            mut changed_range,
1643            mut base_text_changed_range,
1644            mut extended_range,
1645        } = match (state.base_text_exists, new_state.base_text_exists) {
1646            (false, false) => DiffChanged::default(),
1647            (true, true) if should_compare_hunks => compare_hunks(
1648                &new_state.hunks,
1649                &old_snapshot.inner.hunks,
1650                old_buffer_snapshot,
1651                buffer,
1652                old_base_snapshot,
1653                &new_base_snapshot,
1654            ),
1655            _ => {
1656                let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id);
1657                let full_base_range = 0..new_state.base_text.len();
1658                DiffChanged {
1659                    changed_range: Some(full_range.clone()),
1660                    base_text_changed_range: Some(full_base_range),
1661                    extended_range: Some(full_range),
1662                }
1663            }
1664        };
1665        state.hunks = new_state.hunks;
1666        state.buffer_snapshot = update.buffer_snapshot;
1667
1668        if base_text_changed || clear_pending_hunks {
1669            if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
1670            {
1671                let pending_range = first.buffer_range.start..last.buffer_range.end;
1672                if let Some(range) = &mut changed_range {
1673                    range.start = *range.start.min(&pending_range.start, buffer);
1674                    range.end = *range.end.max(&pending_range.end, buffer);
1675                } else {
1676                    changed_range = Some(pending_range.clone());
1677                }
1678
1679                if let Some(base_text_range) = base_text_changed_range.as_mut() {
1680                    base_text_range.start =
1681                        base_text_range.start.min(first.diff_base_byte_range.start);
1682                    base_text_range.end = base_text_range.end.max(last.diff_base_byte_range.end);
1683                } else {
1684                    base_text_changed_range =
1685                        Some(first.diff_base_byte_range.start..last.diff_base_byte_range.end);
1686                }
1687
1688                if let Some(ext) = &mut extended_range {
1689                    ext.start = *ext.start.min(&pending_range.start, buffer);
1690                    ext.end = *ext.end.max(&pending_range.end, buffer);
1691                } else {
1692                    extended_range = Some(pending_range);
1693                }
1694            }
1695            state.pending_hunks = SumTree::new(buffer);
1696        }
1697
1698        if let Some(secondary_changed_range) = secondary_diff_change
1699            && let (Some(secondary_hunk_range), Some(secondary_base_range)) =
1700                old_snapshot.range_to_hunk_range(secondary_changed_range, buffer)
1701        {
1702            if let Some(range) = &mut changed_range {
1703                range.start = *secondary_hunk_range.start.min(&range.start, buffer);
1704                range.end = *secondary_hunk_range.end.max(&range.end, buffer);
1705            } else {
1706                changed_range = Some(secondary_hunk_range.clone());
1707            }
1708
1709            if let Some(base_text_range) = base_text_changed_range.as_mut() {
1710                base_text_range.start = secondary_base_range.start.min(base_text_range.start);
1711                base_text_range.end = secondary_base_range.end.max(base_text_range.end);
1712            } else {
1713                base_text_changed_range = Some(secondary_base_range);
1714            }
1715
1716            if let Some(ext) = &mut extended_range {
1717                ext.start = *ext.start.min(&secondary_hunk_range.start, buffer);
1718                ext.end = *ext.end.max(&secondary_hunk_range.end, buffer);
1719            } else {
1720                extended_range = Some(secondary_hunk_range);
1721            }
1722        }
1723
1724        async move {
1725            if let Some(parsing_idle) = parsing_idle {
1726                parsing_idle.await;
1727            }
1728            DiffChanged {
1729                changed_range,
1730                base_text_changed_range,
1731                extended_range,
1732            }
1733        }
1734    }
1735
1736    pub fn set_snapshot(
1737        &mut self,
1738        new_state: BufferDiffUpdate,
1739        buffer: &text::BufferSnapshot,
1740        cx: &mut Context<Self>,
1741    ) -> Task<Option<Range<Anchor>>> {
1742        self.set_snapshot_with_secondary(new_state, buffer, None, false, cx)
1743    }
1744
1745    pub fn set_snapshot_with_secondary(
1746        &mut self,
1747        update: BufferDiffUpdate,
1748        buffer: &text::BufferSnapshot,
1749        secondary_diff_change: Option<Range<Anchor>>,
1750        clear_pending_hunks: bool,
1751        cx: &mut Context<Self>,
1752    ) -> Task<Option<Range<Anchor>>> {
1753        let fut = self.set_snapshot_with_secondary_inner(
1754            update,
1755            buffer,
1756            secondary_diff_change,
1757            clear_pending_hunks,
1758            cx,
1759        );
1760
1761        cx.spawn(async move |this, cx| {
1762            let change = fut.await;
1763            this.update(cx, |_, cx| {
1764                cx.emit(BufferDiffEvent::DiffChanged(change.clone()));
1765            })
1766            .ok();
1767            change.changed_range
1768        })
1769    }
1770
1771    pub fn base_text(&self, cx: &App) -> language::BufferSnapshot {
1772        self.inner.base_text.read(cx).snapshot()
1773    }
1774
1775    pub fn base_text_exists(&self) -> bool {
1776        self.inner.base_text_exists
1777    }
1778
1779    pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
1780        BufferDiffSnapshot {
1781            inner: BufferDiffInner {
1782                hunks: self.inner.hunks.clone(),
1783                pending_hunks: self.inner.pending_hunks.clone(),
1784                base_text: self.inner.base_text.read(cx).snapshot(),
1785                base_text_exists: self.inner.base_text_exists,
1786                buffer_snapshot: self.inner.buffer_snapshot.clone(),
1787            },
1788            secondary_diff: self.secondary_diff.as_ref().map(|diff| {
1789                debug_assert!(diff.read(cx).secondary_diff.is_none());
1790                Arc::new(diff.read(cx).snapshot(cx))
1791            }),
1792        }
1793    }
1794
1795    /// Used in cases where the change set isn't derived from git.
1796    pub fn set_base_text(
1797        &mut self,
1798        base_text: Option<Arc<str>>,
1799        language: Option<Arc<Language>>,
1800        buffer: text::BufferSnapshot,
1801        cx: &mut Context<Self>,
1802    ) -> oneshot::Receiver<()> {
1803        let (tx, rx) = oneshot::channel();
1804        let complete_on_drop = util::defer(|| {
1805            tx.send(()).ok();
1806        });
1807        cx.spawn(async move |this, cx| {
1808            let Some(state) = this
1809                .update(cx, |this, cx| {
1810                    this.update_diff(buffer.clone(), base_text, Some(false), language, cx)
1811                })
1812                .log_err()
1813            else {
1814                return;
1815            };
1816            let state = state.await;
1817            if let Some(task) = this
1818                .update(cx, |this, cx| this.set_snapshot(state, &buffer, cx))
1819                .log_err()
1820            {
1821                task.await;
1822            }
1823            drop(complete_on_drop)
1824        })
1825        .detach();
1826        rx
1827    }
1828
1829    pub fn base_text_string(&self, cx: &App) -> Option<String> {
1830        self.inner
1831            .base_text_exists
1832            .then(|| self.inner.base_text.read(cx).text())
1833    }
1834
1835    #[cfg(any(test, feature = "test-support"))]
1836    pub fn recalculate_diff_sync(&mut self, buffer: &text::BufferSnapshot, cx: &mut Context<Self>) {
1837        let language = self.base_text(cx).language().cloned();
1838        let base_text = self.base_text_string(cx).map(|s| s.as_str().into());
1839        let fut = self.update_diff(buffer.clone(), base_text, None, language, cx);
1840        let fg_executor = cx.foreground_executor().clone();
1841        let snapshot = fg_executor.block_on(fut);
1842        let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx);
1843        let change = fg_executor.block_on(fut);
1844        cx.emit(BufferDiffEvent::DiffChanged(change));
1845    }
1846
1847    pub fn base_text_buffer(&self) -> Entity<language::Buffer> {
1848        self.inner.base_text.clone()
1849    }
1850}
1851
1852impl DiffHunk {
1853    pub fn is_created_file(&self) -> bool {
1854        self.diff_base_byte_range == (0..0)
1855            && self.buffer_range.start.is_min()
1856            && self.buffer_range.end.is_max()
1857    }
1858
1859    pub fn status(&self) -> DiffHunkStatus {
1860        let kind = if self.buffer_range.start == self.buffer_range.end {
1861            DiffHunkStatusKind::Deleted
1862        } else if self.diff_base_byte_range.is_empty() {
1863            DiffHunkStatusKind::Added
1864        } else {
1865            DiffHunkStatusKind::Modified
1866        };
1867        DiffHunkStatus {
1868            kind,
1869            secondary: self.secondary_status,
1870        }
1871    }
1872}
1873
1874impl DiffHunkStatus {
1875    pub fn has_secondary_hunk(&self) -> bool {
1876        matches!(
1877            self.secondary,
1878            DiffHunkSecondaryStatus::HasSecondaryHunk
1879                | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1880                | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
1881        )
1882    }
1883
1884    pub fn is_pending(&self) -> bool {
1885        matches!(
1886            self.secondary,
1887            DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1888                | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1889        )
1890    }
1891
1892    pub fn is_deleted(&self) -> bool {
1893        self.kind == DiffHunkStatusKind::Deleted
1894    }
1895
1896    pub fn is_added(&self) -> bool {
1897        self.kind == DiffHunkStatusKind::Added
1898    }
1899
1900    pub fn is_modified(&self) -> bool {
1901        self.kind == DiffHunkStatusKind::Modified
1902    }
1903
1904    pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
1905        Self {
1906            kind: DiffHunkStatusKind::Added,
1907            secondary,
1908        }
1909    }
1910
1911    pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
1912        Self {
1913            kind: DiffHunkStatusKind::Modified,
1914            secondary,
1915        }
1916    }
1917
1918    pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
1919        Self {
1920            kind: DiffHunkStatusKind::Deleted,
1921            secondary,
1922        }
1923    }
1924
1925    pub fn deleted_none() -> Self {
1926        Self {
1927            kind: DiffHunkStatusKind::Deleted,
1928            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1929        }
1930    }
1931
1932    pub fn added_none() -> Self {
1933        Self {
1934            kind: DiffHunkStatusKind::Added,
1935            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1936        }
1937    }
1938
1939    pub fn modified_none() -> Self {
1940        Self {
1941            kind: DiffHunkStatusKind::Modified,
1942            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1943        }
1944    }
1945}
1946
1947#[cfg(any(test, feature = "test-support"))]
1948#[track_caller]
1949pub fn assert_hunks<ExpectedText, HunkIter>(
1950    diff_hunks: HunkIter,
1951    buffer: &text::BufferSnapshot,
1952    diff_base: &str,
1953    // Line range, deleted, added, status
1954    expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
1955) where
1956    HunkIter: Iterator<Item = DiffHunk>,
1957    ExpectedText: AsRef<str>,
1958{
1959    let actual_hunks = diff_hunks
1960        .map(|hunk| {
1961            (
1962                hunk.range.clone(),
1963                &diff_base[hunk.diff_base_byte_range.clone()],
1964                buffer
1965                    .text_for_range(hunk.range.clone())
1966                    .collect::<String>(),
1967                hunk.status(),
1968            )
1969        })
1970        .collect::<Vec<_>>();
1971
1972    let expected_hunks: Vec<_> = expected_hunks
1973        .iter()
1974        .map(|(line_range, deleted_text, added_text, status)| {
1975            (
1976                Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
1977                deleted_text.as_ref(),
1978                added_text.as_ref().to_string(),
1979                *status,
1980            )
1981        })
1982        .collect();
1983
1984    pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
1985}
1986
1987#[cfg(test)]
1988mod tests {
1989    use std::{fmt::Write as _, sync::mpsc};
1990
1991    use super::*;
1992    use gpui::TestAppContext;
1993    use pretty_assertions::{assert_eq, assert_ne};
1994    use rand::{Rng as _, rngs::StdRng};
1995    use text::{Buffer, BufferId, ReplicaId, Rope};
1996    use unindent::Unindent as _;
1997    use util::test::marked_text_ranges;
1998
1999    #[ctor::ctor]
2000    fn init_logger() {
2001        zlog::init_test();
2002    }
2003
2004    #[gpui::test]
2005    async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
2006        let diff_base = "
2007            one
2008            two
2009            three
2010        "
2011        .unindent();
2012
2013        let buffer_text = "
2014            one
2015            HELLO
2016            three
2017        "
2018        .unindent();
2019
2020        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2021        let mut diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2022        assert_hunks(
2023            diff.hunks_intersecting_range(
2024                Anchor::min_max_range_for_buffer(buffer.remote_id()),
2025                &buffer,
2026            ),
2027            &buffer,
2028            &diff_base,
2029            &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
2030        );
2031
2032        buffer.edit([(0..0, "point five\n")]);
2033        diff = BufferDiffSnapshot::new_sync(&buffer, diff_base.clone(), cx);
2034        assert_hunks(
2035            diff.hunks_intersecting_range(
2036                Anchor::min_max_range_for_buffer(buffer.remote_id()),
2037                &buffer,
2038            ),
2039            &buffer,
2040            &diff_base,
2041            &[
2042                (0..1, "", "point five\n", DiffHunkStatus::added_none()),
2043                (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
2044            ],
2045        );
2046
2047        diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2048        assert_hunks::<&str, _>(
2049            diff.hunks_intersecting_range(
2050                Anchor::min_max_range_for_buffer(buffer.remote_id()),
2051                &buffer,
2052            ),
2053            &buffer,
2054            &diff_base,
2055            &[],
2056        );
2057    }
2058
2059    #[gpui::test]
2060    async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
2061        let head_text = "
2062            zero
2063            one
2064            two
2065            three
2066            four
2067            five
2068            six
2069            seven
2070            eight
2071            nine
2072        "
2073        .unindent();
2074
2075        let index_text = "
2076            zero
2077            one
2078            TWO
2079            three
2080            FOUR
2081            five
2082            six
2083            seven
2084            eight
2085            NINE
2086        "
2087        .unindent();
2088
2089        let buffer_text = "
2090            zero
2091            one
2092            TWO
2093            three
2094            FOUR
2095            FIVE
2096            six
2097            SEVEN
2098            eight
2099            nine
2100        "
2101        .unindent();
2102
2103        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2104        let unstaged_diff = BufferDiffSnapshot::new_sync(&buffer, index_text, cx);
2105        let mut uncommitted_diff = BufferDiffSnapshot::new_sync(&buffer, head_text.clone(), cx);
2106        uncommitted_diff.secondary_diff = Some(Arc::new(unstaged_diff));
2107
2108        let expected_hunks = vec![
2109            (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
2110            (
2111                4..6,
2112                "four\nfive\n",
2113                "FOUR\nFIVE\n",
2114                DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
2115            ),
2116            (
2117                7..8,
2118                "seven\n",
2119                "SEVEN\n",
2120                DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
2121            ),
2122        ];
2123
2124        assert_hunks(
2125            uncommitted_diff.hunks_intersecting_range(
2126                Anchor::min_max_range_for_buffer(buffer.remote_id()),
2127                &buffer,
2128            ),
2129            &buffer,
2130            &head_text,
2131            &expected_hunks,
2132        );
2133    }
2134
2135    #[gpui::test]
2136    async fn test_buffer_diff_range(cx: &mut TestAppContext) {
2137        let diff_base = "
2138            one
2139            two
2140            three
2141            four
2142            five
2143            six
2144            seven
2145            eight
2146            nine
2147            ten
2148        "
2149        .unindent();
2150
2151        let buffer_text = "
2152            A
2153            one
2154            B
2155            two
2156            C
2157            three
2158            HELLO
2159            four
2160            five
2161            SIXTEEN
2162            seven
2163            eight
2164            WORLD
2165            nine
2166
2167            ten
2168
2169        "
2170        .unindent();
2171
2172        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2173        let diff = BufferDiffSnapshot::new_sync(buffer.snapshot(), diff_base.clone(), cx);
2174        assert_eq!(
2175            diff.hunks_intersecting_range(
2176                Anchor::min_max_range_for_buffer(buffer.remote_id()),
2177                &buffer
2178            )
2179            .count(),
2180            8
2181        );
2182
2183        assert_hunks(
2184            diff.hunks_intersecting_range(
2185                buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
2186                &buffer,
2187            ),
2188            &buffer,
2189            &diff_base,
2190            &[
2191                (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
2192                (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
2193                (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
2194            ],
2195        );
2196    }
2197
2198    #[gpui::test]
2199    async fn test_stage_hunk(cx: &mut TestAppContext) {
2200        struct Example {
2201            name: &'static str,
2202            head_text: String,
2203            index_text: String,
2204            buffer_marked_text: String,
2205            final_index_text: String,
2206        }
2207
2208        let table = [
2209            Example {
2210                name: "uncommitted hunk straddles end of unstaged hunk",
2211                head_text: "
2212                    one
2213                    two
2214                    three
2215                    four
2216                    five
2217                "
2218                .unindent(),
2219                index_text: "
2220                    one
2221                    TWO_HUNDRED
2222                    three
2223                    FOUR_HUNDRED
2224                    five
2225                "
2226                .unindent(),
2227                buffer_marked_text: "
2228                    ZERO
2229                    one
2230                    two
2231                    «THREE_HUNDRED
2232                    FOUR_HUNDRED»
2233                    five
2234                    SIX
2235                "
2236                .unindent(),
2237                final_index_text: "
2238                    one
2239                    two
2240                    THREE_HUNDRED
2241                    FOUR_HUNDRED
2242                    five
2243                "
2244                .unindent(),
2245            },
2246            Example {
2247                name: "uncommitted hunk straddles start of unstaged hunk",
2248                head_text: "
2249                    one
2250                    two
2251                    three
2252                    four
2253                    five
2254                "
2255                .unindent(),
2256                index_text: "
2257                    one
2258                    TWO_HUNDRED
2259                    three
2260                    FOUR_HUNDRED
2261                    five
2262                "
2263                .unindent(),
2264                buffer_marked_text: "
2265                    ZERO
2266                    one
2267                    «TWO_HUNDRED
2268                    THREE_HUNDRED»
2269                    four
2270                    five
2271                    SIX
2272                "
2273                .unindent(),
2274                final_index_text: "
2275                    one
2276                    TWO_HUNDRED
2277                    THREE_HUNDRED
2278                    four
2279                    five
2280                "
2281                .unindent(),
2282            },
2283            Example {
2284                name: "uncommitted hunk strictly contains unstaged hunks",
2285                head_text: "
2286                    one
2287                    two
2288                    three
2289                    four
2290                    five
2291                    six
2292                    seven
2293                "
2294                .unindent(),
2295                index_text: "
2296                    one
2297                    TWO
2298                    THREE
2299                    FOUR
2300                    FIVE
2301                    SIX
2302                    seven
2303                "
2304                .unindent(),
2305                buffer_marked_text: "
2306                    one
2307                    TWO
2308                    «THREE_HUNDRED
2309                    FOUR
2310                    FIVE_HUNDRED»
2311                    SIX
2312                    seven
2313                "
2314                .unindent(),
2315                final_index_text: "
2316                    one
2317                    TWO
2318                    THREE_HUNDRED
2319                    FOUR
2320                    FIVE_HUNDRED
2321                    SIX
2322                    seven
2323                "
2324                .unindent(),
2325            },
2326            Example {
2327                name: "uncommitted deletion hunk",
2328                head_text: "
2329                    one
2330                    two
2331                    three
2332                    four
2333                    five
2334                "
2335                .unindent(),
2336                index_text: "
2337                    one
2338                    two
2339                    three
2340                    four
2341                    five
2342                "
2343                .unindent(),
2344                buffer_marked_text: "
2345                    one
2346                    ˇfive
2347                "
2348                .unindent(),
2349                final_index_text: "
2350                    one
2351                    five
2352                "
2353                .unindent(),
2354            },
2355            Example {
2356                name: "one unstaged hunk that contains two uncommitted hunks",
2357                head_text: "
2358                    one
2359                    two
2360
2361                    three
2362                    four
2363                "
2364                .unindent(),
2365                index_text: "
2366                    one
2367                    two
2368                    three
2369                    four
2370                "
2371                .unindent(),
2372                buffer_marked_text: "
2373                    «one
2374
2375                    three // modified
2376                    four»
2377                "
2378                .unindent(),
2379                final_index_text: "
2380                    one
2381
2382                    three // modified
2383                    four
2384                "
2385                .unindent(),
2386            },
2387            Example {
2388                name: "one uncommitted hunk that contains two unstaged hunks",
2389                head_text: "
2390                    one
2391                    two
2392                    three
2393                    four
2394                    five
2395                "
2396                .unindent(),
2397                index_text: "
2398                    ZERO
2399                    one
2400                    TWO
2401                    THREE
2402                    FOUR
2403                    five
2404                "
2405                .unindent(),
2406                buffer_marked_text: "
2407                    «one
2408                    TWO_HUNDRED
2409                    THREE
2410                    FOUR_HUNDRED
2411                    five»
2412                "
2413                .unindent(),
2414                final_index_text: "
2415                    ZERO
2416                    one
2417                    TWO_HUNDRED
2418                    THREE
2419                    FOUR_HUNDRED
2420                    five
2421                "
2422                .unindent(),
2423            },
2424        ];
2425
2426        for example in table {
2427            let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
2428            let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2429            let hunk_range =
2430                buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
2431
2432            let unstaged_diff =
2433                cx.new(|cx| BufferDiff::new_with_base_text(&example.index_text, &buffer, cx));
2434
2435            let uncommitted_diff = cx.new(|cx| {
2436                let mut diff = BufferDiff::new_with_base_text(&example.head_text, &buffer, cx);
2437                diff.set_secondary_diff(unstaged_diff);
2438                diff
2439            });
2440
2441            uncommitted_diff.update(cx, |diff, cx| {
2442                let hunks = diff
2443                    .snapshot(cx)
2444                    .hunks_intersecting_range(hunk_range.clone(), &buffer)
2445                    .collect::<Vec<_>>();
2446                for hunk in &hunks {
2447                    assert_ne!(
2448                        hunk.secondary_status,
2449                        DiffHunkSecondaryStatus::NoSecondaryHunk
2450                    )
2451                }
2452
2453                let new_index_text = diff
2454                    .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
2455                    .unwrap()
2456                    .to_string();
2457
2458                let hunks = diff
2459                    .snapshot(cx)
2460                    .hunks_intersecting_range(hunk_range.clone(), &buffer)
2461                    .collect::<Vec<_>>();
2462                for hunk in &hunks {
2463                    assert_eq!(
2464                        hunk.secondary_status,
2465                        DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2466                    )
2467                }
2468
2469                pretty_assertions::assert_eq!(
2470                    new_index_text,
2471                    example.final_index_text,
2472                    "example: {}",
2473                    example.name
2474                );
2475            });
2476        }
2477    }
2478
2479    #[gpui::test]
2480    async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) {
2481        // This test reproduces a crash where staging all hunks would cause an underflow
2482        // when there's one large unstaged hunk containing multiple uncommitted hunks.
2483        let head_text = "
2484            aaa
2485            bbb
2486            ccc
2487            ddd
2488            eee
2489            fff
2490            ggg
2491            hhh
2492            iii
2493            jjj
2494            kkk
2495            lll
2496        "
2497        .unindent();
2498
2499        let index_text = "
2500            aaa
2501            bbb
2502            CCC-index
2503            DDD-index
2504            EEE-index
2505            FFF-index
2506            GGG-index
2507            HHH-index
2508            III-index
2509            JJJ-index
2510            kkk
2511            lll
2512        "
2513        .unindent();
2514
2515        let buffer_text = "
2516            aaa
2517            bbb
2518            ccc-modified
2519            ddd
2520            eee-modified
2521            fff
2522            ggg
2523            hhh-modified
2524            iii
2525            jjj
2526            kkk
2527            lll
2528        "
2529        .unindent();
2530
2531        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
2532
2533        let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2534        let uncommitted_diff = cx.new(|cx| {
2535            let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2536            diff.set_secondary_diff(unstaged_diff);
2537            diff
2538        });
2539
2540        uncommitted_diff.update(cx, |diff, cx| {
2541            diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
2542        });
2543    }
2544
2545    #[gpui::test]
2546    async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
2547        let head_text = "
2548            one
2549            two
2550            three
2551        "
2552        .unindent();
2553        let index_text = head_text.clone();
2554        let buffer_text = "
2555            one
2556            three
2557        "
2558        .unindent();
2559
2560        let buffer = Buffer::new(
2561            ReplicaId::LOCAL,
2562            BufferId::new(1).unwrap(),
2563            buffer_text.clone(),
2564        );
2565        let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&index_text, &buffer, cx));
2566        let uncommitted_diff = cx.new(|cx| {
2567            let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
2568            diff.set_secondary_diff(unstaged_diff.clone());
2569            diff
2570        });
2571
2572        uncommitted_diff.update(cx, |diff, cx| {
2573            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2574
2575            let new_index_text = diff
2576                .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
2577                .unwrap()
2578                .to_string();
2579            assert_eq!(new_index_text, buffer_text);
2580
2581            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2582            assert_eq!(
2583                hunk.secondary_status,
2584                DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
2585            );
2586
2587            let index_text = diff
2588                .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
2589                .unwrap()
2590                .to_string();
2591            assert_eq!(index_text, head_text);
2592
2593            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
2594            // optimistically unstaged (fine, could also be HasSecondaryHunk)
2595            assert_eq!(
2596                hunk.secondary_status,
2597                DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
2598            );
2599        });
2600    }
2601
2602    #[gpui::test]
2603    async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
2604        let base_text = "
2605            zero
2606            one
2607            two
2608            three
2609            four
2610            five
2611            six
2612            seven
2613            eight
2614            nine
2615        "
2616        .unindent();
2617
2618        let buffer_text_1 = "
2619            one
2620            three
2621            four
2622            five
2623            SIX
2624            seven
2625            eight
2626            NINE
2627        "
2628        .unindent();
2629
2630        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
2631
2632        let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
2633        let diff_1 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2634        let DiffChanged {
2635            changed_range,
2636            base_text_changed_range,
2637            extended_range: _,
2638        } = compare_hunks(
2639            &diff_1.inner.hunks,
2640            &empty_diff.inner.hunks,
2641            &buffer,
2642            &buffer,
2643            &diff_1.base_text(),
2644            &diff_1.base_text(),
2645        );
2646        let range = changed_range.unwrap();
2647        assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
2648        let base_text_range = base_text_changed_range.unwrap();
2649        assert_eq!(
2650            base_text_range.to_point(diff_1.base_text()),
2651            Point::new(0, 0)..Point::new(10, 0)
2652        );
2653
2654        // Edit does affects the diff because it recalculates word diffs.
2655        buffer.edit_via_marked_text(
2656            &"
2657                one
2658                three
2659                four
2660                five
2661                «SIX.5»
2662                seven
2663                eight
2664                NINE
2665            "
2666            .unindent(),
2667        );
2668        let diff_2 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2669        let DiffChanged {
2670            changed_range,
2671            base_text_changed_range,
2672            extended_range: _,
2673        } = compare_hunks(
2674            &diff_2.inner.hunks,
2675            &diff_1.inner.hunks,
2676            &buffer,
2677            &buffer,
2678            diff_2.base_text(),
2679            diff_2.base_text(),
2680        );
2681        assert_eq!(
2682            changed_range.unwrap().to_point(&buffer),
2683            Point::new(4, 0)..Point::new(5, 0),
2684        );
2685        assert_eq!(
2686            base_text_changed_range
2687                .unwrap()
2688                .to_point(diff_2.base_text()),
2689            Point::new(6, 0)..Point::new(7, 0),
2690        );
2691
2692        // Edit turns a deletion hunk into a modification.
2693        buffer.edit_via_marked_text(
2694            &"
2695                one
2696                «THREE»
2697                four
2698                five
2699                SIX.5
2700                seven
2701                eight
2702                NINE
2703            "
2704            .unindent(),
2705        );
2706        let diff_3 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2707        let DiffChanged {
2708            changed_range,
2709            base_text_changed_range,
2710            extended_range: _,
2711        } = compare_hunks(
2712            &diff_3.inner.hunks,
2713            &diff_2.inner.hunks,
2714            &buffer,
2715            &buffer,
2716            diff_3.base_text(),
2717            diff_3.base_text(),
2718        );
2719        let range = changed_range.unwrap();
2720        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
2721        let base_text_range = base_text_changed_range.unwrap();
2722        assert_eq!(
2723            base_text_range.to_point(diff_3.base_text()),
2724            Point::new(2, 0)..Point::new(4, 0)
2725        );
2726
2727        // Edit turns a modification hunk into a deletion.
2728        buffer.edit_via_marked_text(
2729            &"
2730                one
2731                THREE
2732                four
2733                five«»
2734                seven
2735                eight
2736                NINE
2737            "
2738            .unindent(),
2739        );
2740        let diff_4 = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
2741        let DiffChanged {
2742            changed_range,
2743            base_text_changed_range,
2744            extended_range: _,
2745        } = compare_hunks(
2746            &diff_4.inner.hunks,
2747            &diff_3.inner.hunks,
2748            &buffer,
2749            &buffer,
2750            diff_4.base_text(),
2751            diff_4.base_text(),
2752        );
2753        let range = changed_range.unwrap();
2754        assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
2755        let base_text_range = base_text_changed_range.unwrap();
2756        assert_eq!(
2757            base_text_range.to_point(diff_4.base_text()),
2758            Point::new(6, 0)..Point::new(7, 0)
2759        );
2760
2761        // Edit introduces a new insertion hunk.
2762        buffer.edit_via_marked_text(
2763            &"
2764                one
2765                THREE
2766                four«
2767                FOUR.5
2768                »five
2769                seven
2770                eight
2771                NINE
2772            "
2773            .unindent(),
2774        );
2775        let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2776        let DiffChanged {
2777            changed_range,
2778            base_text_changed_range,
2779            extended_range: _,
2780        } = compare_hunks(
2781            &diff_5.inner.hunks,
2782            &diff_4.inner.hunks,
2783            &buffer,
2784            &buffer,
2785            diff_5.base_text(),
2786            diff_5.base_text(),
2787        );
2788        let range = changed_range.unwrap();
2789        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2790        let base_text_range = base_text_changed_range.unwrap();
2791        assert_eq!(
2792            base_text_range.to_point(diff_5.base_text()),
2793            Point::new(5, 0)..Point::new(5, 0)
2794        );
2795
2796        // Edit removes a hunk.
2797        buffer.edit_via_marked_text(
2798            &"
2799                one
2800                THREE
2801                four
2802                FOUR.5
2803                five
2804                seven
2805                eight
2806                «nine»
2807            "
2808            .unindent(),
2809        );
2810        let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2811        let DiffChanged {
2812            changed_range,
2813            base_text_changed_range,
2814            extended_range: _,
2815        } = compare_hunks(
2816            &diff_6.inner.hunks,
2817            &diff_5.inner.hunks,
2818            &buffer,
2819            &buffer,
2820            diff_6.base_text(),
2821            diff_6.base_text(),
2822        );
2823        let range = changed_range.unwrap();
2824        assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2825        let base_text_range = base_text_changed_range.unwrap();
2826        assert_eq!(
2827            base_text_range.to_point(diff_6.base_text()),
2828            Point::new(9, 0)..Point::new(10, 0)
2829        );
2830
2831        buffer.edit_via_marked_text(
2832            &"
2833                one
2834                THREE
2835                four«»
2836                five
2837                seven
2838                eight
2839                «NINE»
2840            "
2841            .unindent(),
2842        );
2843
2844        let diff_7 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2845        let DiffChanged {
2846            changed_range,
2847            base_text_changed_range,
2848            extended_range: _,
2849        } = compare_hunks(
2850            &diff_7.inner.hunks,
2851            &diff_6.inner.hunks,
2852            &buffer,
2853            &buffer,
2854            diff_7.base_text(),
2855            diff_7.base_text(),
2856        );
2857        let range = changed_range.unwrap();
2858        assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0));
2859        let base_text_range = base_text_changed_range.unwrap();
2860        assert_eq!(
2861            base_text_range.to_point(diff_7.base_text()),
2862            Point::new(5, 0)..Point::new(10, 0)
2863        );
2864
2865        buffer.edit_via_marked_text(
2866            &"
2867                one
2868                THREE
2869                four
2870                five«»seven
2871                eight
2872                NINE
2873            "
2874            .unindent(),
2875        );
2876
2877        let diff_8 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
2878        let DiffChanged {
2879            changed_range,
2880            base_text_changed_range,
2881            extended_range: _,
2882        } = compare_hunks(
2883            &diff_8.inner.hunks,
2884            &diff_7.inner.hunks,
2885            &buffer,
2886            &buffer,
2887            diff_8.base_text(),
2888            diff_8.base_text(),
2889        );
2890        let range = changed_range.unwrap();
2891        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4));
2892        let base_text_range = base_text_changed_range.unwrap();
2893        assert_eq!(
2894            base_text_range.to_point(diff_8.base_text()),
2895            Point::new(5, 0)..Point::new(8, 0)
2896        );
2897    }
2898
2899    #[gpui::test(iterations = 100)]
2900    async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
2901        fn gen_line(rng: &mut StdRng) -> String {
2902            if rng.random_bool(0.2) {
2903                "\n".to_owned()
2904            } else {
2905                let c = rng.random_range('A'..='Z');
2906                format!("{c}{c}{c}\n")
2907            }
2908        }
2909
2910        fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
2911            let mut old_lines = {
2912                let mut old_lines = Vec::new();
2913                let old_lines_iter = head.lines();
2914                for line in old_lines_iter {
2915                    assert!(!line.ends_with("\n"));
2916                    old_lines.push(line.to_owned());
2917                }
2918                if old_lines.last().is_some_and(|line| line.is_empty()) {
2919                    old_lines.pop();
2920                }
2921                old_lines.into_iter()
2922            };
2923            let mut result = String::new();
2924            let unchanged_count = rng.random_range(0..=old_lines.len());
2925            result +=
2926                &old_lines
2927                    .by_ref()
2928                    .take(unchanged_count)
2929                    .fold(String::new(), |mut s, line| {
2930                        writeln!(&mut s, "{line}").unwrap();
2931                        s
2932                    });
2933            while old_lines.len() > 0 {
2934                let deleted_count = rng.random_range(0..=old_lines.len());
2935                let _advance = old_lines
2936                    .by_ref()
2937                    .take(deleted_count)
2938                    .map(|line| line.len() + 1)
2939                    .sum::<usize>();
2940                let minimum_added = if deleted_count == 0 { 1 } else { 0 };
2941                let added_count = rng.random_range(minimum_added..=5);
2942                let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
2943                result += &addition;
2944
2945                if old_lines.len() > 0 {
2946                    let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
2947                    if blank_lines == old_lines.len() {
2948                        break;
2949                    };
2950                    let unchanged_count =
2951                        rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
2952                    result += &old_lines.by_ref().take(unchanged_count).fold(
2953                        String::new(),
2954                        |mut s, line| {
2955                            writeln!(&mut s, "{line}").unwrap();
2956                            s
2957                        },
2958                    );
2959                }
2960            }
2961            result
2962        }
2963
2964        fn uncommitted_diff(
2965            working_copy: &language::BufferSnapshot,
2966            index_text: &Rope,
2967            head_text: String,
2968            cx: &mut TestAppContext,
2969        ) -> Entity<BufferDiff> {
2970            let secondary = cx.new(|cx| {
2971                BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
2972            });
2973            cx.new(|cx| {
2974                let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
2975                diff.secondary_diff = Some(secondary);
2976                diff
2977            })
2978        }
2979
2980        let operations = std::env::var("OPERATIONS")
2981            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2982            .unwrap_or(10);
2983
2984        let rng = &mut rng;
2985        let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
2986            writeln!(&mut s, "{c}{c}{c}").unwrap();
2987            s
2988        });
2989        let working_copy = gen_working_copy(rng, &head_text);
2990        let working_copy = cx.new(|cx| {
2991            language::Buffer::local_normalized(
2992                Rope::from(working_copy.as_str()),
2993                text::LineEnding::default(),
2994                cx,
2995            )
2996        });
2997        let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
2998        let mut index_text = if rng.random() {
2999            Rope::from(head_text.as_str())
3000        } else {
3001            working_copy.as_rope().clone()
3002        };
3003
3004        let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3005        let mut hunks = diff.update(cx, |diff, cx| {
3006            diff.snapshot(cx)
3007                .hunks_intersecting_range(
3008                    Anchor::min_max_range_for_buffer(diff.buffer_id),
3009                    &working_copy,
3010                )
3011                .collect::<Vec<_>>()
3012        });
3013        if hunks.is_empty() {
3014            return;
3015        }
3016
3017        for _ in 0..operations {
3018            let i = rng.random_range(0..hunks.len());
3019            let hunk = &mut hunks[i];
3020            let hunk_to_change = hunk.clone();
3021            let stage = match hunk.secondary_status {
3022                DiffHunkSecondaryStatus::HasSecondaryHunk => {
3023                    hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
3024                    true
3025                }
3026                DiffHunkSecondaryStatus::NoSecondaryHunk => {
3027                    hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
3028                    false
3029                }
3030                _ => unreachable!(),
3031            };
3032
3033            index_text = diff.update(cx, |diff, cx| {
3034                diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
3035                    .unwrap()
3036            });
3037
3038            diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
3039            let found_hunks = diff.update(cx, |diff, cx| {
3040                diff.snapshot(cx)
3041                    .hunks_intersecting_range(
3042                        Anchor::min_max_range_for_buffer(diff.buffer_id),
3043                        &working_copy,
3044                    )
3045                    .collect::<Vec<_>>()
3046            });
3047            assert_eq!(hunks.len(), found_hunks.len());
3048
3049            for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
3050                assert_eq!(
3051                    expected_hunk.buffer_range.to_point(&working_copy),
3052                    found_hunk.buffer_range.to_point(&working_copy)
3053                );
3054                assert_eq!(
3055                    expected_hunk.diff_base_byte_range,
3056                    found_hunk.diff_base_byte_range
3057                );
3058                assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
3059            }
3060            hunks = found_hunks;
3061        }
3062    }
3063
3064    #[gpui::test]
3065    async fn test_changed_ranges(cx: &mut gpui::TestAppContext) {
3066        let base_text = "
3067            one
3068            two
3069            three
3070            four
3071            five
3072            six
3073        "
3074        .unindent();
3075        let buffer_text = "
3076            one
3077            TWO
3078            three
3079            four
3080            FIVE
3081            six
3082        "
3083        .unindent();
3084        let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
3085        let diff = cx.new(|cx| {
3086            BufferDiff::new_with_base_text(&base_text, &buffer.read(cx).text_snapshot(), cx)
3087        });
3088        cx.run_until_parked();
3089        let (tx, rx) = mpsc::channel();
3090        let subscription =
3091            cx.update(|cx| cx.subscribe(&diff, move |_, event, _| tx.send(event.clone()).unwrap()));
3092
3093        let snapshot = buffer.update(cx, |buffer, cx| {
3094            buffer.set_text(
3095                "
3096                ONE
3097                TWO
3098                THREE
3099                FOUR
3100                FIVE
3101                SIX
3102            "
3103                .unindent(),
3104                cx,
3105            );
3106            buffer.text_snapshot()
3107        });
3108        let update = diff
3109            .update(cx, |diff, cx| {
3110                diff.update_diff(
3111                    snapshot.clone(),
3112                    Some(base_text.as_str().into()),
3113                    None,
3114                    None,
3115                    cx,
3116                )
3117            })
3118            .await;
3119        diff.update(cx, |diff, cx| diff.set_snapshot(update, &snapshot, cx))
3120            .await;
3121        cx.run_until_parked();
3122        drop(subscription);
3123        let events = rx.into_iter().collect::<Vec<_>>();
3124        match events.as_slice() {
3125            [
3126                BufferDiffEvent::DiffChanged(DiffChanged {
3127                    changed_range: _,
3128                    base_text_changed_range,
3129                    extended_range: _,
3130                }),
3131            ] => {
3132                // TODO(cole) this seems like it should pass but currently fails (see compare_hunks)
3133                // assert_eq!(
3134                //     *changed_range,
3135                //     Some(Anchor::min_max_range_for_buffer(
3136                //         buffer.read_with(cx, |buffer, _| buffer.remote_id())
3137                //     ))
3138                // );
3139                assert_eq!(*base_text_changed_range, Some(0..base_text.len()));
3140            }
3141            _ => panic!("unexpected events: {:?}", events),
3142        }
3143    }
3144
3145    #[gpui::test]
3146    async fn test_extended_range(cx: &mut TestAppContext) {
3147        let base_text = "
3148            aaa
3149            bbb
3150
3151
3152
3153
3154
3155            ccc
3156            ddd
3157        "
3158        .unindent();
3159
3160        let buffer_text = "
3161            aaa
3162            bbb
3163
3164
3165
3166
3167
3168            CCC
3169            ddd
3170        "
3171        .unindent();
3172
3173        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
3174        let old_buffer = buffer.snapshot().clone();
3175        let diff_a = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
3176
3177        buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")]);
3178        let diff_b = BufferDiffSnapshot::new_sync(&buffer, base_text, cx);
3179
3180        let DiffChanged {
3181            changed_range,
3182            base_text_changed_range: _,
3183            extended_range,
3184        } = compare_hunks(
3185            &diff_b.inner.hunks,
3186            &diff_a.inner.hunks,
3187            &old_buffer,
3188            &buffer,
3189            &diff_a.base_text(),
3190            &diff_a.base_text(),
3191        );
3192
3193        let changed_range = changed_range.unwrap();
3194        assert_eq!(
3195            changed_range.to_point(&buffer),
3196            Point::new(7, 0)..Point::new(9, 0),
3197            "changed_range should span from old hunk position to new hunk end"
3198        );
3199
3200        let extended_range = extended_range.unwrap();
3201        assert_eq!(
3202            extended_range.start.to_point(&buffer),
3203            Point::new(1, 3),
3204            "extended_range.start should extend to include the edit outside changed_range"
3205        );
3206        assert_eq!(
3207            extended_range.end.to_point(&buffer),
3208            Point::new(9, 0),
3209            "extended_range.end should collapse to changed_range.end when no edits in end margin"
3210        );
3211
3212        let base_text_2 = "
3213            one
3214            two
3215            three
3216            four
3217            five
3218            six
3219            seven
3220            eight
3221        "
3222        .unindent();
3223
3224        let buffer_text_2 = "
3225            ONE
3226            two
3227            THREE
3228            four
3229            FIVE
3230            six
3231            SEVEN
3232            eight
3233        "
3234        .unindent();
3235
3236        let mut buffer_2 = Buffer::new(ReplicaId::LOCAL, BufferId::new(2).unwrap(), buffer_text_2);
3237        let old_buffer_2 = buffer_2.snapshot().clone();
3238        let diff_2a = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2.clone(), cx);
3239
3240        buffer_2.edit([(Point::new(4, 0)..Point::new(4, 4), "FIVE_CHANGED")]);
3241        let diff_2b = BufferDiffSnapshot::new_sync(&buffer_2, base_text_2, cx);
3242
3243        let DiffChanged {
3244            changed_range,
3245            base_text_changed_range: _,
3246            extended_range,
3247        } = compare_hunks(
3248            &diff_2b.inner.hunks,
3249            &diff_2a.inner.hunks,
3250            &old_buffer_2,
3251            &buffer_2,
3252            &diff_2a.base_text(),
3253            &diff_2a.base_text(),
3254        );
3255
3256        let changed_range = changed_range.unwrap();
3257        assert_eq!(
3258            changed_range.to_point(&buffer_2),
3259            Point::new(4, 0)..Point::new(5, 0),
3260            "changed_range should be just the hunk that changed (FIVE)"
3261        );
3262
3263        let extended_range = extended_range.unwrap();
3264        assert_eq!(
3265            extended_range.to_point(&buffer_2),
3266            Point::new(4, 0)..Point::new(5, 0),
3267            "extended_range should equal changed_range when edit is within the hunk"
3268        );
3269    }
3270
3271    #[gpui::test]
3272    async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) {
3273        // Use a shared base text buffer so that anchors from old and new snapshots
3274        // share the same remote_id and resolve correctly across versions.
3275        let initial_base = "aaa\nbbb\nccc\nddd\neee\n";
3276        let mut base_text_buffer = Buffer::new(
3277            ReplicaId::LOCAL,
3278            BufferId::new(99).unwrap(),
3279            initial_base.to_string(),
3280        );
3281
3282        // --- Scenario 1: Base text gains a line, producing a new deletion hunk ---
3283        //
3284        // Buffer has a modification (ccc → CCC). When the base text gains
3285        // a new line "XXX" after "aaa", the diff now also contains a
3286        // deletion for that line, and the modification hunk shifts in the
3287        // base text.
3288        let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n";
3289        let buffer = Buffer::new(
3290            ReplicaId::LOCAL,
3291            BufferId::new(1).unwrap(),
3292            buffer_text_1.to_string(),
3293        );
3294
3295        let old_base_snapshot_1 = base_text_buffer.snapshot().clone();
3296        let old_hunks_1 = compute_hunks(
3297            Some((Arc::from(initial_base), Rope::from(initial_base))),
3298            buffer.snapshot(),
3299            None,
3300        );
3301
3302        // Insert "XXX\n" after "aaa\n" in the base text.
3303        base_text_buffer.edit([(4..4, "XXX\n")]);
3304        let new_base_str_1: Arc<str> = Arc::from(base_text_buffer.text().as_str());
3305        let new_base_snapshot_1 = base_text_buffer.snapshot();
3306
3307        let new_hunks_1 = compute_hunks(
3308            Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))),
3309            buffer.snapshot(),
3310            None,
3311        );
3312
3313        let DiffChanged {
3314            changed_range,
3315            base_text_changed_range,
3316            extended_range: _,
3317        } = compare_hunks(
3318            &new_hunks_1,
3319            &old_hunks_1,
3320            &buffer.snapshot(),
3321            &buffer.snapshot(),
3322            &old_base_snapshot_1,
3323            &new_base_snapshot_1,
3324        );
3325
3326        // The new deletion hunk (XXX) starts at buffer row 1 and the
3327        // modification hunk (ccc → CCC) now has a different
3328        // diff_base_byte_range, so the changed range spans both.
3329        let range = changed_range.unwrap();
3330        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),);
3331        let base_range = base_text_changed_range.unwrap();
3332        assert_eq!(
3333            base_range.to_point(&new_base_snapshot_1),
3334            Point::new(1, 0)..Point::new(4, 0),
3335        );
3336
3337        // --- Scenario 2: Base text changes to match the buffer (hunk disappears) ---
3338        //
3339        // Start fresh with a simple base text.
3340        let simple_base = "one\ntwo\nthree\n";
3341        let mut base_buf_2 = Buffer::new(
3342            ReplicaId::LOCAL,
3343            BufferId::new(100).unwrap(),
3344            simple_base.to_string(),
3345        );
3346
3347        let buffer_text_2 = "one\nTWO\nthree\n";
3348        let buffer_2 = Buffer::new(
3349            ReplicaId::LOCAL,
3350            BufferId::new(2).unwrap(),
3351            buffer_text_2.to_string(),
3352        );
3353
3354        let old_base_snapshot_2 = base_buf_2.snapshot().clone();
3355        let old_hunks_2 = compute_hunks(
3356            Some((Arc::from(simple_base), Rope::from(simple_base))),
3357            buffer_2.snapshot(),
3358            None,
3359        );
3360
3361        // The base text is edited so "two" becomes "TWO", now matching the buffer.
3362        base_buf_2.edit([(4..7, "TWO")]);
3363        let new_base_str_2: Arc<str> = Arc::from(base_buf_2.text().as_str());
3364        let new_base_snapshot_2 = base_buf_2.snapshot();
3365
3366        let new_hunks_2 = compute_hunks(
3367            Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))),
3368            buffer_2.snapshot(),
3369            None,
3370        );
3371
3372        let DiffChanged {
3373            changed_range,
3374            base_text_changed_range,
3375            extended_range: _,
3376        } = compare_hunks(
3377            &new_hunks_2,
3378            &old_hunks_2,
3379            &buffer_2.snapshot(),
3380            &buffer_2.snapshot(),
3381            &old_base_snapshot_2,
3382            &new_base_snapshot_2,
3383        );
3384
3385        // The old modification hunk (two → TWO) is now gone because the
3386        // base text matches the buffer. The changed range covers where the
3387        // old hunk used to be.
3388        let range = changed_range.unwrap();
3389        assert_eq!(
3390            range.to_point(&buffer_2),
3391            Point::new(1, 0)..Point::new(2, 0),
3392        );
3393        let base_range = base_text_changed_range.unwrap();
3394        // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8).
3395        // anchor_after(4) is right-biased at the start of the deleted "two",
3396        // so after the edit replacing "two" with "TWO" it resolves past the
3397        // insertion to Point(1, 3).
3398        assert_eq!(
3399            base_range.to_point(&new_base_snapshot_2),
3400            Point::new(1, 3)..Point::new(2, 0),
3401        );
3402
3403        // --- Scenario 3: Base text edit changes one hunk but not another ---
3404        //
3405        // Two modification hunks exist. Only one of them is resolved by
3406        // the base text change; the other remains identical.
3407        let base_3 = "aaa\nbbb\nccc\nddd\neee\n";
3408        let mut base_buf_3 = Buffer::new(
3409            ReplicaId::LOCAL,
3410            BufferId::new(101).unwrap(),
3411            base_3.to_string(),
3412        );
3413
3414        let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n";
3415        let buffer_3 = Buffer::new(
3416            ReplicaId::LOCAL,
3417            BufferId::new(3).unwrap(),
3418            buffer_text_3.to_string(),
3419        );
3420
3421        let old_base_snapshot_3 = base_buf_3.snapshot().clone();
3422        let old_hunks_3 = compute_hunks(
3423            Some((Arc::from(base_3), Rope::from(base_3))),
3424            buffer_3.snapshot(),
3425            None,
3426        );
3427
3428        // Change "ddd" to "DDD" in the base text so that hunk disappears,
3429        // but "bbb" stays, so its hunk remains.
3430        base_buf_3.edit([(12..15, "DDD")]);
3431        let new_base_str_3: Arc<str> = Arc::from(base_buf_3.text().as_str());
3432        let new_base_snapshot_3 = base_buf_3.snapshot();
3433
3434        let new_hunks_3 = compute_hunks(
3435            Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))),
3436            buffer_3.snapshot(),
3437            None,
3438        );
3439
3440        let DiffChanged {
3441            changed_range,
3442            base_text_changed_range,
3443            extended_range: _,
3444        } = compare_hunks(
3445            &new_hunks_3,
3446            &old_hunks_3,
3447            &buffer_3.snapshot(),
3448            &buffer_3.snapshot(),
3449            &old_base_snapshot_3,
3450            &new_base_snapshot_3,
3451        );
3452
3453        // Only the second hunk (ddd → DDD) disappeared; the first hunk
3454        // (bbb → BBB) is unchanged, so the changed range covers only line 3.
3455        let range = changed_range.unwrap();
3456        assert_eq!(
3457            range.to_point(&buffer_3),
3458            Point::new(3, 0)..Point::new(4, 0),
3459        );
3460        let base_range = base_text_changed_range.unwrap();
3461        // anchor_after(12) is right-biased at the start of deleted "ddd",
3462        // so after the edit replacing "ddd" with "DDD" it resolves past
3463        // the insertion to Point(3, 3).
3464        assert_eq!(
3465            base_range.to_point(&new_base_snapshot_3),
3466            Point::new(3, 3)..Point::new(4, 0),
3467        );
3468
3469        // --- Scenario 4: Both buffer and base text change simultaneously ---
3470        //
3471        // The buffer gains an edit that introduces a new hunk while the
3472        // base text also changes.
3473        let base_4 = "alpha\nbeta\ngamma\ndelta\n";
3474        let mut base_buf_4 = Buffer::new(
3475            ReplicaId::LOCAL,
3476            BufferId::new(102).unwrap(),
3477            base_4.to_string(),
3478        );
3479
3480        let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n";
3481        let mut buffer_4 = Buffer::new(
3482            ReplicaId::LOCAL,
3483            BufferId::new(4).unwrap(),
3484            buffer_text_4.to_string(),
3485        );
3486
3487        let old_base_snapshot_4 = base_buf_4.snapshot().clone();
3488        let old_buffer_snapshot_4 = buffer_4.snapshot().clone();
3489        let old_hunks_4 = compute_hunks(
3490            Some((Arc::from(base_4), Rope::from(base_4))),
3491            buffer_4.snapshot(),
3492            None,
3493        );
3494
3495        // Edit the buffer: change "delta" to "DELTA" (new modification hunk).
3496        buffer_4.edit_via_marked_text(
3497            &"
3498                alpha
3499                BETA
3500                gamma
3501                «DELTA»
3502            "
3503            .unindent(),
3504        );
3505
3506        // Edit the base text: change "beta" to "BETA" (resolves that hunk).
3507        base_buf_4.edit([(6..10, "BETA")]);
3508        let new_base_str_4: Arc<str> = Arc::from(base_buf_4.text().as_str());
3509        let new_base_snapshot_4 = base_buf_4.snapshot();
3510
3511        let new_hunks_4 = compute_hunks(
3512            Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))),
3513            buffer_4.snapshot(),
3514            None,
3515        );
3516
3517        let DiffChanged {
3518            changed_range,
3519            base_text_changed_range,
3520            extended_range: _,
3521        } = compare_hunks(
3522            &new_hunks_4,
3523            &old_hunks_4,
3524            &old_buffer_snapshot_4,
3525            &buffer_4.snapshot(),
3526            &old_base_snapshot_4,
3527            &new_base_snapshot_4,
3528        );
3529
3530        // The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3)
3531        // appeared, so the changed range spans from line 1 through line 4.
3532        let range = changed_range.unwrap();
3533        assert_eq!(
3534            range.to_point(&buffer_4),
3535            Point::new(1, 0)..Point::new(4, 0),
3536        );
3537        let base_range = base_text_changed_range.unwrap();
3538        // The old BETA hunk's base range started at byte 6 ("beta"). After
3539        // the base text edit replacing "beta" with "BETA", anchor_after(6)
3540        // resolves past the insertion to Point(1, 4).
3541        assert_eq!(
3542            base_range.to_point(&new_base_snapshot_4),
3543            Point::new(1, 4)..Point::new(4, 0),
3544        );
3545    }
3546}