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