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 = 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    /// Used in cases where the change set isn't derived from git.
1162    pub fn set_base_text(
1163        &mut self,
1164        base_text: Option<Arc<String>>,
1165        language: Option<Arc<Language>>,
1166        language_registry: Option<Arc<LanguageRegistry>>,
1167        buffer: text::BufferSnapshot,
1168        cx: &mut Context<Self>,
1169    ) -> oneshot::Receiver<()> {
1170        let (tx, rx) = oneshot::channel();
1171        let this = cx.weak_entity();
1172
1173        let snapshot = BufferDiffSnapshot::new_with_base_text(
1174            buffer.clone(),
1175            base_text,
1176            language,
1177            language_registry,
1178            cx,
1179        );
1180        let complete_on_drop = util::defer(|| {
1181            tx.send(()).ok();
1182        });
1183        cx.spawn(async move |_, cx| {
1184            let snapshot = snapshot.await;
1185            let Some(this) = this.upgrade() else {
1186                return;
1187            };
1188            this.update(cx, |this, cx| {
1189                this.set_snapshot(snapshot, &buffer, cx);
1190            })
1191            .log_err();
1192            drop(complete_on_drop)
1193        })
1194        .detach();
1195        rx
1196    }
1197
1198    pub fn base_text_string(&self) -> Option<String> {
1199        self.inner
1200            .base_text_exists
1201            .then(|| self.inner.base_text.text())
1202    }
1203
1204    #[cfg(any(test, feature = "test-support"))]
1205    pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
1206        let base_text = self.base_text_string().map(Arc::new);
1207        let snapshot = BufferDiffSnapshot::new_with_base_buffer(
1208            buffer.clone(),
1209            base_text,
1210            self.inner.base_text.clone(),
1211            cx,
1212        );
1213        let snapshot = cx.background_executor().block(snapshot);
1214        self.set_snapshot(snapshot, &buffer, cx);
1215    }
1216}
1217
1218impl DiffHunk {
1219    pub fn is_created_file(&self) -> bool {
1220        self.diff_base_byte_range == (0..0) && self.buffer_range == (Anchor::MIN..Anchor::MAX)
1221    }
1222
1223    pub fn status(&self) -> DiffHunkStatus {
1224        let kind = if self.buffer_range.start == self.buffer_range.end {
1225            DiffHunkStatusKind::Deleted
1226        } else if self.diff_base_byte_range.is_empty() {
1227            DiffHunkStatusKind::Added
1228        } else {
1229            DiffHunkStatusKind::Modified
1230        };
1231        DiffHunkStatus {
1232            kind,
1233            secondary: self.secondary_status,
1234        }
1235    }
1236}
1237
1238impl DiffHunkStatus {
1239    pub fn has_secondary_hunk(&self) -> bool {
1240        matches!(
1241            self.secondary,
1242            DiffHunkSecondaryStatus::HasSecondaryHunk
1243                | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1244                | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
1245        )
1246    }
1247
1248    pub fn is_pending(&self) -> bool {
1249        matches!(
1250            self.secondary,
1251            DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1252                | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1253        )
1254    }
1255
1256    pub fn is_deleted(&self) -> bool {
1257        self.kind == DiffHunkStatusKind::Deleted
1258    }
1259
1260    pub fn is_added(&self) -> bool {
1261        self.kind == DiffHunkStatusKind::Added
1262    }
1263
1264    pub fn is_modified(&self) -> bool {
1265        self.kind == DiffHunkStatusKind::Modified
1266    }
1267
1268    pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
1269        Self {
1270            kind: DiffHunkStatusKind::Added,
1271            secondary,
1272        }
1273    }
1274
1275    pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
1276        Self {
1277            kind: DiffHunkStatusKind::Modified,
1278            secondary,
1279        }
1280    }
1281
1282    pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
1283        Self {
1284            kind: DiffHunkStatusKind::Deleted,
1285            secondary,
1286        }
1287    }
1288
1289    pub fn deleted_none() -> Self {
1290        Self {
1291            kind: DiffHunkStatusKind::Deleted,
1292            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1293        }
1294    }
1295
1296    pub fn added_none() -> Self {
1297        Self {
1298            kind: DiffHunkStatusKind::Added,
1299            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1300        }
1301    }
1302
1303    pub fn modified_none() -> Self {
1304        Self {
1305            kind: DiffHunkStatusKind::Modified,
1306            secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
1307        }
1308    }
1309}
1310
1311#[cfg(any(test, feature = "test-support"))]
1312#[track_caller]
1313pub fn assert_hunks<ExpectedText, HunkIter>(
1314    diff_hunks: HunkIter,
1315    buffer: &text::BufferSnapshot,
1316    diff_base: &str,
1317    // Line range, deleted, added, status
1318    expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
1319) where
1320    HunkIter: Iterator<Item = DiffHunk>,
1321    ExpectedText: AsRef<str>,
1322{
1323    let actual_hunks = diff_hunks
1324        .map(|hunk| {
1325            (
1326                hunk.range.clone(),
1327                &diff_base[hunk.diff_base_byte_range.clone()],
1328                buffer
1329                    .text_for_range(hunk.range.clone())
1330                    .collect::<String>(),
1331                hunk.status(),
1332            )
1333        })
1334        .collect::<Vec<_>>();
1335
1336    let expected_hunks: Vec<_> = expected_hunks
1337        .iter()
1338        .map(|(line_range, deleted_text, added_text, status)| {
1339            (
1340                Point::new(line_range.start, 0)..Point::new(line_range.end, 0),
1341                deleted_text.as_ref(),
1342                added_text.as_ref().to_string(),
1343                *status,
1344            )
1345        })
1346        .collect();
1347
1348    pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
1349}
1350
1351#[cfg(test)]
1352mod tests {
1353    use std::fmt::Write as _;
1354
1355    use super::*;
1356    use gpui::TestAppContext;
1357    use pretty_assertions::{assert_eq, assert_ne};
1358    use rand::{Rng as _, rngs::StdRng};
1359    use text::{Buffer, BufferId, Rope};
1360    use unindent::Unindent as _;
1361    use util::test::marked_text_ranges;
1362
1363    #[ctor::ctor]
1364    fn init_logger() {
1365        zlog::init_test();
1366    }
1367
1368    #[gpui::test]
1369    async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1370        let diff_base = "
1371            one
1372            two
1373            three
1374        "
1375        .unindent();
1376
1377        let buffer_text = "
1378            one
1379            HELLO
1380            three
1381        "
1382        .unindent();
1383
1384        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1385        let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1386        assert_hunks(
1387            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1388            &buffer,
1389            &diff_base,
1390            &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
1391        );
1392
1393        buffer.edit([(0..0, "point five\n")]);
1394        diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
1395        assert_hunks(
1396            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1397            &buffer,
1398            &diff_base,
1399            &[
1400                (0..1, "", "point five\n", DiffHunkStatus::added_none()),
1401                (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
1402            ],
1403        );
1404
1405        diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
1406        assert_hunks::<&str, _>(
1407            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1408            &buffer,
1409            &diff_base,
1410            &[],
1411        );
1412    }
1413
1414    #[gpui::test]
1415    async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1416        let head_text = "
1417            zero
1418            one
1419            two
1420            three
1421            four
1422            five
1423            six
1424            seven
1425            eight
1426            nine
1427        "
1428        .unindent();
1429
1430        let index_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 buffer_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 = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1459        let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1460        let mut uncommitted_diff =
1461            BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1462        uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
1463
1464        let expected_hunks = vec![
1465            (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
1466            (
1467                4..6,
1468                "four\nfive\n",
1469                "FOUR\nFIVE\n",
1470                DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1471            ),
1472            (
1473                7..8,
1474                "seven\n",
1475                "SEVEN\n",
1476                DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1477            ),
1478        ];
1479
1480        assert_hunks(
1481            uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
1482            &buffer,
1483            &head_text,
1484            &expected_hunks,
1485        );
1486    }
1487
1488    #[gpui::test]
1489    async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1490        let diff_base = Arc::new(
1491            "
1492            one
1493            two
1494            three
1495            four
1496            five
1497            six
1498            seven
1499            eight
1500            nine
1501            ten
1502        "
1503            .unindent(),
1504        );
1505
1506        let buffer_text = "
1507            A
1508            one
1509            B
1510            two
1511            C
1512            three
1513            HELLO
1514            four
1515            five
1516            SIXTEEN
1517            seven
1518            eight
1519            WORLD
1520            nine
1521
1522            ten
1523
1524        "
1525        .unindent();
1526
1527        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1528        let diff = cx
1529            .update(|cx| {
1530                BufferDiffSnapshot::new_with_base_text(
1531                    buffer.snapshot(),
1532                    Some(diff_base.clone()),
1533                    None,
1534                    None,
1535                    cx,
1536                )
1537            })
1538            .await;
1539        assert_eq!(
1540            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer)
1541                .count(),
1542            8
1543        );
1544
1545        assert_hunks(
1546            diff.hunks_intersecting_range(
1547                buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1548                &buffer,
1549            ),
1550            &buffer,
1551            &diff_base,
1552            &[
1553                (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
1554                (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
1555                (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
1556            ],
1557        );
1558    }
1559
1560    #[gpui::test]
1561    async fn test_stage_hunk(cx: &mut TestAppContext) {
1562        struct Example {
1563            name: &'static str,
1564            head_text: String,
1565            index_text: String,
1566            buffer_marked_text: String,
1567            final_index_text: String,
1568        }
1569
1570        let table = [
1571            Example {
1572                name: "uncommitted hunk straddles end of unstaged hunk",
1573                head_text: "
1574                    one
1575                    two
1576                    three
1577                    four
1578                    five
1579                "
1580                .unindent(),
1581                index_text: "
1582                    one
1583                    TWO_HUNDRED
1584                    three
1585                    FOUR_HUNDRED
1586                    five
1587                "
1588                .unindent(),
1589                buffer_marked_text: "
1590                    ZERO
1591                    one
1592                    two
1593                    «THREE_HUNDRED
1594                    FOUR_HUNDRED»
1595                    five
1596                    SIX
1597                "
1598                .unindent(),
1599                final_index_text: "
1600                    one
1601                    two
1602                    THREE_HUNDRED
1603                    FOUR_HUNDRED
1604                    five
1605                "
1606                .unindent(),
1607            },
1608            Example {
1609                name: "uncommitted hunk straddles start of unstaged hunk",
1610                head_text: "
1611                    one
1612                    two
1613                    three
1614                    four
1615                    five
1616                "
1617                .unindent(),
1618                index_text: "
1619                    one
1620                    TWO_HUNDRED
1621                    three
1622                    FOUR_HUNDRED
1623                    five
1624                "
1625                .unindent(),
1626                buffer_marked_text: "
1627                    ZERO
1628                    one
1629                    «TWO_HUNDRED
1630                    THREE_HUNDRED»
1631                    four
1632                    five
1633                    SIX
1634                "
1635                .unindent(),
1636                final_index_text: "
1637                    one
1638                    TWO_HUNDRED
1639                    THREE_HUNDRED
1640                    four
1641                    five
1642                "
1643                .unindent(),
1644            },
1645            Example {
1646                name: "uncommitted hunk strictly contains unstaged hunks",
1647                head_text: "
1648                    one
1649                    two
1650                    three
1651                    four
1652                    five
1653                    six
1654                    seven
1655                "
1656                .unindent(),
1657                index_text: "
1658                    one
1659                    TWO
1660                    THREE
1661                    FOUR
1662                    FIVE
1663                    SIX
1664                    seven
1665                "
1666                .unindent(),
1667                buffer_marked_text: "
1668                    one
1669                    TWO
1670                    «THREE_HUNDRED
1671                    FOUR
1672                    FIVE_HUNDRED»
1673                    SIX
1674                    seven
1675                "
1676                .unindent(),
1677                final_index_text: "
1678                    one
1679                    TWO
1680                    THREE_HUNDRED
1681                    FOUR
1682                    FIVE_HUNDRED
1683                    SIX
1684                    seven
1685                "
1686                .unindent(),
1687            },
1688            Example {
1689                name: "uncommitted deletion hunk",
1690                head_text: "
1691                    one
1692                    two
1693                    three
1694                    four
1695                    five
1696                "
1697                .unindent(),
1698                index_text: "
1699                    one
1700                    two
1701                    three
1702                    four
1703                    five
1704                "
1705                .unindent(),
1706                buffer_marked_text: "
1707                    one
1708                    ˇfive
1709                "
1710                .unindent(),
1711                final_index_text: "
1712                    one
1713                    five
1714                "
1715                .unindent(),
1716            },
1717            Example {
1718                name: "one unstaged hunk that contains two uncommitted hunks",
1719                head_text: "
1720                    one
1721                    two
1722
1723                    three
1724                    four
1725                "
1726                .unindent(),
1727                index_text: "
1728                    one
1729                    two
1730                    three
1731                    four
1732                "
1733                .unindent(),
1734                buffer_marked_text: "
1735                    «one
1736
1737                    three // modified
1738                    four»
1739                "
1740                .unindent(),
1741                final_index_text: "
1742                    one
1743
1744                    three // modified
1745                    four
1746                "
1747                .unindent(),
1748            },
1749            Example {
1750                name: "one uncommitted hunk that contains two unstaged hunks",
1751                head_text: "
1752                    one
1753                    two
1754                    three
1755                    four
1756                    five
1757                "
1758                .unindent(),
1759                index_text: "
1760                    ZERO
1761                    one
1762                    TWO
1763                    THREE
1764                    FOUR
1765                    five
1766                "
1767                .unindent(),
1768                buffer_marked_text: "
1769                    «one
1770                    TWO_HUNDRED
1771                    THREE
1772                    FOUR_HUNDRED
1773                    five»
1774                "
1775                .unindent(),
1776                final_index_text: "
1777                    ZERO
1778                    one
1779                    TWO_HUNDRED
1780                    THREE
1781                    FOUR_HUNDRED
1782                    five
1783                "
1784                .unindent(),
1785            },
1786        ];
1787
1788        for example in table {
1789            let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
1790            let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1791            let hunk_range =
1792                buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
1793
1794            let unstaged =
1795                BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
1796            let uncommitted =
1797                BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
1798
1799            let unstaged_diff = cx.new(|cx| {
1800                let mut diff = BufferDiff::new(&buffer, cx);
1801                diff.set_snapshot(unstaged, &buffer, cx);
1802                diff
1803            });
1804
1805            let uncommitted_diff = cx.new(|cx| {
1806                let mut diff = BufferDiff::new(&buffer, cx);
1807                diff.set_snapshot(uncommitted, &buffer, cx);
1808                diff.set_secondary_diff(unstaged_diff);
1809                diff
1810            });
1811
1812            uncommitted_diff.update(cx, |diff, cx| {
1813                let hunks = diff
1814                    .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
1815                    .collect::<Vec<_>>();
1816                for hunk in &hunks {
1817                    assert_ne!(
1818                        hunk.secondary_status,
1819                        DiffHunkSecondaryStatus::NoSecondaryHunk
1820                    )
1821                }
1822
1823                let new_index_text = diff
1824                    .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
1825                    .unwrap()
1826                    .to_string();
1827
1828                let hunks = diff
1829                    .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
1830                    .collect::<Vec<_>>();
1831                for hunk in &hunks {
1832                    assert_eq!(
1833                        hunk.secondary_status,
1834                        DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1835                    )
1836                }
1837
1838                pretty_assertions::assert_eq!(
1839                    new_index_text,
1840                    example.final_index_text,
1841                    "example: {}",
1842                    example.name
1843                );
1844            });
1845        }
1846    }
1847
1848    #[gpui::test]
1849    async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
1850        let head_text = "
1851            one
1852            two
1853            three
1854        "
1855        .unindent();
1856        let index_text = head_text.clone();
1857        let buffer_text = "
1858            one
1859            three
1860        "
1861        .unindent();
1862
1863        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
1864        let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
1865        let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
1866        let unstaged_diff = cx.new(|cx| {
1867            let mut diff = BufferDiff::new(&buffer, cx);
1868            diff.set_snapshot(unstaged, &buffer, cx);
1869            diff
1870        });
1871        let uncommitted_diff = cx.new(|cx| {
1872            let mut diff = BufferDiff::new(&buffer, cx);
1873            diff.set_snapshot(uncommitted, &buffer, cx);
1874            diff.set_secondary_diff(unstaged_diff.clone());
1875            diff
1876        });
1877
1878        uncommitted_diff.update(cx, |diff, cx| {
1879            let hunk = diff.hunks(&buffer, cx).next().unwrap();
1880
1881            let new_index_text = diff
1882                .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
1883                .unwrap()
1884                .to_string();
1885            assert_eq!(new_index_text, buffer_text);
1886
1887            let hunk = diff.hunks(&buffer, cx).next().unwrap();
1888            assert_eq!(
1889                hunk.secondary_status,
1890                DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
1891            );
1892
1893            let index_text = diff
1894                .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
1895                .unwrap()
1896                .to_string();
1897            assert_eq!(index_text, head_text);
1898
1899            let hunk = diff.hunks(&buffer, cx).next().unwrap();
1900            // optimistically unstaged (fine, could also be HasSecondaryHunk)
1901            assert_eq!(
1902                hunk.secondary_status,
1903                DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
1904            );
1905        });
1906    }
1907
1908    #[gpui::test]
1909    async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
1910        let base_text = "
1911            zero
1912            one
1913            two
1914            three
1915            four
1916            five
1917            six
1918            seven
1919            eight
1920            nine
1921        "
1922        .unindent();
1923
1924        let buffer_text_1 = "
1925            one
1926            three
1927            four
1928            five
1929            SIX
1930            seven
1931            eight
1932            NINE
1933        "
1934        .unindent();
1935
1936        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
1937
1938        let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
1939        let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1940        let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
1941        assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
1942
1943        // Edit does not affect the diff.
1944        buffer.edit_via_marked_text(
1945            &"
1946                one
1947                three
1948                four
1949                five
1950                «SIX.5»
1951                seven
1952                eight
1953                NINE
1954            "
1955            .unindent(),
1956        );
1957        let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1958        assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
1959
1960        // Edit turns a deletion hunk into a modification.
1961        buffer.edit_via_marked_text(
1962            &"
1963                one
1964                «THREE»
1965                four
1966                five
1967                SIX.5
1968                seven
1969                eight
1970                NINE
1971            "
1972            .unindent(),
1973        );
1974        let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1975        let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap();
1976        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
1977
1978        // Edit turns a modification hunk into a deletion.
1979        buffer.edit_via_marked_text(
1980            &"
1981                one
1982                THREE
1983                four
1984                five«»
1985                seven
1986                eight
1987                NINE
1988            "
1989            .unindent(),
1990        );
1991        let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
1992        let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap();
1993        assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
1994
1995        // Edit introduces a new insertion hunk.
1996        buffer.edit_via_marked_text(
1997            &"
1998                one
1999                THREE
2000                four«
2001                FOUR.5
2002                »five
2003                seven
2004                eight
2005                NINE
2006            "
2007            .unindent(),
2008        );
2009        let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
2010        let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap();
2011        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
2012
2013        // Edit removes a hunk.
2014        buffer.edit_via_marked_text(
2015            &"
2016                one
2017                THREE
2018                four
2019                FOUR.5
2020                five
2021                seven
2022                eight
2023                «nine»
2024            "
2025            .unindent(),
2026        );
2027        let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
2028        let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap();
2029        assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
2030    }
2031
2032    #[gpui::test(iterations = 100)]
2033    async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
2034        fn gen_line(rng: &mut StdRng) -> String {
2035            if rng.gen_bool(0.2) {
2036                "\n".to_owned()
2037            } else {
2038                let c = rng.gen_range('A'..='Z');
2039                format!("{c}{c}{c}\n")
2040            }
2041        }
2042
2043        fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
2044            let mut old_lines = {
2045                let mut old_lines = Vec::new();
2046                let old_lines_iter = head.lines();
2047                for line in old_lines_iter {
2048                    assert!(!line.ends_with("\n"));
2049                    old_lines.push(line.to_owned());
2050                }
2051                if old_lines.last().is_some_and(|line| line.is_empty()) {
2052                    old_lines.pop();
2053                }
2054                old_lines.into_iter()
2055            };
2056            let mut result = String::new();
2057            let unchanged_count = rng.gen_range(0..=old_lines.len());
2058            result +=
2059                &old_lines
2060                    .by_ref()
2061                    .take(unchanged_count)
2062                    .fold(String::new(), |mut s, line| {
2063                        writeln!(&mut s, "{line}").unwrap();
2064                        s
2065                    });
2066            while old_lines.len() > 0 {
2067                let deleted_count = rng.gen_range(0..=old_lines.len());
2068                let _advance = old_lines
2069                    .by_ref()
2070                    .take(deleted_count)
2071                    .map(|line| line.len() + 1)
2072                    .sum::<usize>();
2073                let minimum_added = if deleted_count == 0 { 1 } else { 0 };
2074                let added_count = rng.gen_range(minimum_added..=5);
2075                let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
2076                result += &addition;
2077
2078                if old_lines.len() > 0 {
2079                    let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
2080                    if blank_lines == old_lines.len() {
2081                        break;
2082                    };
2083                    let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
2084                    result += &old_lines.by_ref().take(unchanged_count).fold(
2085                        String::new(),
2086                        |mut s, line| {
2087                            writeln!(&mut s, "{line}").unwrap();
2088                            s
2089                        },
2090                    );
2091                }
2092            }
2093            result
2094        }
2095
2096        fn uncommitted_diff(
2097            working_copy: &language::BufferSnapshot,
2098            index_text: &Rope,
2099            head_text: String,
2100            cx: &mut TestAppContext,
2101        ) -> Entity<BufferDiff> {
2102            let inner =
2103                BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
2104            let secondary = BufferDiff {
2105                buffer_id: working_copy.remote_id(),
2106                inner: BufferDiffSnapshot::new_sync(
2107                    working_copy.text.clone(),
2108                    index_text.to_string(),
2109                    cx,
2110                )
2111                .inner,
2112                secondary_diff: None,
2113            };
2114            let secondary = cx.new(|_| secondary);
2115            cx.new(|_| BufferDiff {
2116                buffer_id: working_copy.remote_id(),
2117                inner,
2118                secondary_diff: Some(secondary),
2119            })
2120        }
2121
2122        let operations = std::env::var("OPERATIONS")
2123            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2124            .unwrap_or(10);
2125
2126        let rng = &mut rng;
2127        let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
2128            writeln!(&mut s, "{c}{c}{c}").unwrap();
2129            s
2130        });
2131        let working_copy = gen_working_copy(rng, &head_text);
2132        let working_copy = cx.new(|cx| {
2133            language::Buffer::local_normalized(
2134                Rope::from(working_copy.as_str()),
2135                text::LineEnding::default(),
2136                cx,
2137            )
2138        });
2139        let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
2140        let mut index_text = if rng.r#gen() {
2141            Rope::from(head_text.as_str())
2142        } else {
2143            working_copy.as_rope().clone()
2144        };
2145
2146        let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2147        let mut hunks = diff.update(cx, |diff, cx| {
2148            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
2149                .collect::<Vec<_>>()
2150        });
2151        if hunks.is_empty() {
2152            return;
2153        }
2154
2155        for _ in 0..operations {
2156            let i = rng.gen_range(0..hunks.len());
2157            let hunk = &mut hunks[i];
2158            let hunk_to_change = hunk.clone();
2159            let stage = match hunk.secondary_status {
2160                DiffHunkSecondaryStatus::HasSecondaryHunk => {
2161                    hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
2162                    true
2163                }
2164                DiffHunkSecondaryStatus::NoSecondaryHunk => {
2165                    hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
2166                    false
2167                }
2168                _ => unreachable!(),
2169            };
2170
2171            index_text = diff.update(cx, |diff, cx| {
2172                diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
2173                    .unwrap()
2174            });
2175
2176            diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
2177            let found_hunks = diff.update(cx, |diff, cx| {
2178                diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
2179                    .collect::<Vec<_>>()
2180            });
2181            assert_eq!(hunks.len(), found_hunks.len());
2182
2183            for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
2184                assert_eq!(
2185                    expected_hunk.buffer_range.to_point(&working_copy),
2186                    found_hunk.buffer_range.to_point(&working_copy)
2187                );
2188                assert_eq!(
2189                    expected_hunk.diff_base_byte_range,
2190                    found_hunk.diff_base_byte_range
2191                );
2192                assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
2193            }
2194            hunks = found_hunks;
2195        }
2196    }
2197}