buffer_diff.rs

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