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