buffer_diff.rs

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