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