buffer_diff.rs

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