buffer_diff.rs

   1use futures::{channel::oneshot, future::OptionFuture};
   2use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
   3use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter};
   4use language::{Language, LanguageRegistry};
   5use rope::Rope;
   6use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
   7use sum_tree::SumTree;
   8use text::ToOffset as _;
   9use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
  10use util::ResultExt;
  11
  12pub struct BufferDiff {
  13    pub buffer_id: BufferId,
  14    inner: BufferDiffInner,
  15    secondary_diff: Option<Entity<BufferDiff>>,
  16}
  17
  18#[derive(Clone, Debug)]
  19pub struct BufferDiffSnapshot {
  20    inner: BufferDiffInner,
  21    secondary_diff: Option<Box<BufferDiffSnapshot>>,
  22    pub is_single_insertion: bool,
  23}
  24
  25#[derive(Clone)]
  26struct BufferDiffInner {
  27    hunks: SumTree<InternalDiffHunk>,
  28    base_text: Option<language::BufferSnapshot>,
  29}
  30
  31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  32pub struct DiffHunkStatus {
  33    pub kind: DiffHunkStatusKind,
  34    pub secondary: DiffHunkSecondaryStatus,
  35}
  36
  37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  38pub enum DiffHunkStatusKind {
  39    Added,
  40    Modified,
  41    Deleted,
  42}
  43
  44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  45pub enum DiffHunkSecondaryStatus {
  46    HasSecondaryHunk,
  47    OverlapsWithSecondaryHunk,
  48    None,
  49}
  50
  51impl DiffHunkSecondaryStatus {
  52    pub fn is_secondary(&self) -> bool {
  53        match self {
  54            DiffHunkSecondaryStatus::HasSecondaryHunk => true,
  55            DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => true,
  56            DiffHunkSecondaryStatus::None => false,
  57        }
  58    }
  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, expressed in terms of rows.
  65    pub row_range: Range<u32>,
  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    pub secondary_diff_base_byte_range: Option<Range<usize>>,
  72}
  73
  74/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
  75#[derive(Debug, Clone, PartialEq, Eq)]
  76struct InternalDiffHunk {
  77    buffer_range: Range<Anchor>,
  78    diff_base_byte_range: Range<usize>,
  79}
  80
  81impl sum_tree::Item for InternalDiffHunk {
  82    type Summary = DiffHunkSummary;
  83
  84    fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
  85        DiffHunkSummary {
  86            buffer_range: self.buffer_range.clone(),
  87        }
  88    }
  89}
  90
  91#[derive(Debug, Default, Clone)]
  92pub struct DiffHunkSummary {
  93    buffer_range: Range<Anchor>,
  94}
  95
  96impl sum_tree::Summary for DiffHunkSummary {
  97    type Context = text::BufferSnapshot;
  98
  99    fn zero(_cx: &Self::Context) -> Self {
 100        Default::default()
 101    }
 102
 103    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
 104        self.buffer_range.start = self
 105            .buffer_range
 106            .start
 107            .min(&other.buffer_range.start, buffer);
 108        self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
 109    }
 110}
 111
 112impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor {
 113    fn cmp(
 114        &self,
 115        cursor_location: &DiffHunkSummary,
 116        buffer: &text::BufferSnapshot,
 117    ) -> cmp::Ordering {
 118        self.cmp(&cursor_location.buffer_range.end, buffer)
 119    }
 120}
 121
 122impl std::fmt::Debug for BufferDiffInner {
 123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 124        f.debug_struct("BufferDiffSnapshot")
 125            .field("hunks", &self.hunks)
 126            .finish()
 127    }
 128}
 129
 130impl BufferDiffSnapshot {
 131    pub fn is_empty(&self) -> bool {
 132        self.inner.hunks.is_empty()
 133    }
 134
 135    pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
 136        self.secondary_diff.as_deref()
 137    }
 138
 139    pub fn hunks_intersecting_range<'a>(
 140        &'a self,
 141        range: Range<Anchor>,
 142        buffer: &'a text::BufferSnapshot,
 143    ) -> impl 'a + Iterator<Item = DiffHunk> {
 144        let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner);
 145        self.inner
 146            .hunks_intersecting_range(range, buffer, unstaged_counterpart)
 147    }
 148
 149    pub fn hunks_intersecting_range_rev<'a>(
 150        &'a self,
 151        range: Range<Anchor>,
 152        buffer: &'a text::BufferSnapshot,
 153    ) -> impl 'a + Iterator<Item = DiffHunk> {
 154        self.inner.hunks_intersecting_range_rev(range, buffer)
 155    }
 156
 157    pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
 158        self.inner.base_text.as_ref()
 159    }
 160
 161    pub fn base_texts_eq(&self, other: &Self) -> bool {
 162        match (other.base_text(), self.base_text()) {
 163            (None, None) => true,
 164            (None, Some(_)) => false,
 165            (Some(_), None) => false,
 166            (Some(old), Some(new)) => {
 167                let (old_id, old_empty) = (old.remote_id(), old.is_empty());
 168                let (new_id, new_empty) = (new.remote_id(), new.is_empty());
 169                new_id == old_id || (new_empty && old_empty)
 170            }
 171        }
 172    }
 173
 174    fn buffer_range_to_unchanged_diff_base_range(
 175        &self,
 176        buffer_range: Range<Anchor>,
 177        buffer: &text::BufferSnapshot,
 178    ) -> Option<Range<usize>> {
 179        let mut hunks = self.inner.hunks.iter();
 180        let mut start = 0;
 181        let mut pos = buffer.anchor_before(0);
 182        while let Some(hunk) = hunks.next() {
 183            assert!(buffer_range.start.cmp(&pos, buffer).is_ge());
 184            assert!(hunk.buffer_range.start.cmp(&pos, buffer).is_ge());
 185            if hunk
 186                .buffer_range
 187                .start
 188                .cmp(&buffer_range.end, buffer)
 189                .is_ge()
 190            {
 191                // target buffer range is contained in the unchanged stretch leading up to this next hunk,
 192                // so do a final adjustment based on that
 193                break;
 194            }
 195
 196            // if the target buffer range intersects this hunk at all, no dice
 197            if buffer_range
 198                .start
 199                .cmp(&hunk.buffer_range.end, buffer)
 200                .is_lt()
 201            {
 202                return None;
 203            }
 204
 205            start += hunk.buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
 206            start += hunk.diff_base_byte_range.end - hunk.diff_base_byte_range.start;
 207            pos = hunk.buffer_range.end;
 208        }
 209        start += buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
 210        let end = start + buffer_range.end.to_offset(buffer) - buffer_range.start.to_offset(buffer);
 211        Some(start..end)
 212    }
 213
 214    pub fn secondary_edits_for_stage_or_unstage(
 215        &self,
 216        stage: bool,
 217        hunks: impl Iterator<Item = (Range<usize>, Option<Range<usize>>, Range<Anchor>)>,
 218        buffer: &text::BufferSnapshot,
 219    ) -> Vec<(Range<usize>, String)> {
 220        let Some(secondary_diff) = self.secondary_diff() else {
 221            log::debug!("no secondary diff");
 222            return Vec::new();
 223        };
 224        let index_base = secondary_diff.base_text().map_or_else(
 225            || Rope::from(""),
 226            |snapshot| snapshot.text.as_rope().clone(),
 227        );
 228        let head_base = self.base_text().map_or_else(
 229            || Rope::from(""),
 230            |snapshot| snapshot.text.as_rope().clone(),
 231        );
 232        log::debug!("original: {:?}", index_base.to_string());
 233        let mut edits = Vec::new();
 234        for (diff_base_byte_range, secondary_diff_base_byte_range, buffer_range) in hunks {
 235            let (index_byte_range, replacement_text) = if stage {
 236                log::debug!("staging");
 237                let mut replacement_text = String::new();
 238                let Some(index_byte_range) = secondary_diff_base_byte_range.clone() else {
 239                    log::debug!("not a stageable hunk");
 240                    continue;
 241                };
 242                log::debug!("using {:?}", index_byte_range);
 243                for chunk in buffer.text_for_range(buffer_range.clone()) {
 244                    replacement_text.push_str(chunk);
 245                }
 246                (index_byte_range, replacement_text)
 247            } else {
 248                log::debug!("unstaging");
 249                let mut replacement_text = String::new();
 250                let Some(index_byte_range) = secondary_diff
 251                    .buffer_range_to_unchanged_diff_base_range(buffer_range.clone(), &buffer)
 252                else {
 253                    log::debug!("not an unstageable hunk");
 254                    continue;
 255                };
 256                for chunk in head_base.chunks_in_range(diff_base_byte_range.clone()) {
 257                    replacement_text.push_str(chunk);
 258                }
 259                (index_byte_range, replacement_text)
 260            };
 261            edits.push((index_byte_range, replacement_text));
 262        }
 263        log::debug!("edits: {edits:?}");
 264        edits
 265    }
 266}
 267
 268impl BufferDiffInner {
 269    fn hunks_intersecting_range<'a>(
 270        &'a self,
 271        range: Range<Anchor>,
 272        buffer: &'a text::BufferSnapshot,
 273        secondary: Option<&'a Self>,
 274    ) -> impl 'a + Iterator<Item = DiffHunk> {
 275        let range = range.to_offset(buffer);
 276
 277        let mut cursor = self
 278            .hunks
 279            .filter::<_, DiffHunkSummary>(buffer, move |summary| {
 280                let summary_range = summary.buffer_range.to_offset(buffer);
 281                let before_start = summary_range.end < range.start;
 282                let after_end = summary_range.start > range.end;
 283                !before_start && !after_end
 284            });
 285
 286        let anchor_iter = iter::from_fn(move || {
 287            cursor.next(buffer);
 288            cursor.item()
 289        })
 290        .flat_map(move |hunk| {
 291            [
 292                (
 293                    &hunk.buffer_range.start,
 294                    (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
 295                ),
 296                (
 297                    &hunk.buffer_range.end,
 298                    (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
 299                ),
 300            ]
 301        });
 302
 303        let mut secondary_cursor = secondary.as_ref().map(|diff| {
 304            let mut cursor = diff.hunks.cursor::<DiffHunkSummary>(buffer);
 305            cursor.next(buffer);
 306            cursor
 307        });
 308
 309        let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
 310        iter::from_fn(move || loop {
 311            let (start_point, (start_anchor, start_base)) = summaries.next()?;
 312            let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
 313
 314            if !start_anchor.is_valid(buffer) {
 315                continue;
 316            }
 317
 318            if end_point.column > 0 {
 319                end_point.row += 1;
 320                end_point.column = 0;
 321                end_anchor = buffer.anchor_before(end_point);
 322            }
 323
 324            let mut secondary_status = DiffHunkSecondaryStatus::None;
 325            let mut secondary_diff_base_byte_range = None;
 326            if let Some(secondary_cursor) = secondary_cursor.as_mut() {
 327                if start_anchor
 328                    .cmp(&secondary_cursor.start().buffer_range.start, buffer)
 329                    .is_gt()
 330                {
 331                    secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer);
 332                }
 333
 334                if let Some(secondary_hunk) = secondary_cursor.item() {
 335                    let mut secondary_range = secondary_hunk.buffer_range.to_point(buffer);
 336                    if secondary_range.end.column > 0 {
 337                        secondary_range.end.row += 1;
 338                        secondary_range.end.column = 0;
 339                    }
 340                    if secondary_range == (start_point..end_point) {
 341                        secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
 342                        secondary_diff_base_byte_range =
 343                            Some(secondary_hunk.diff_base_byte_range.clone());
 344                    } else if secondary_range.start <= end_point {
 345                        secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
 346                    }
 347                }
 348            }
 349
 350            return Some(DiffHunk {
 351                row_range: start_point.row..end_point.row,
 352                diff_base_byte_range: start_base..end_base,
 353                buffer_range: start_anchor..end_anchor,
 354                secondary_status,
 355                secondary_diff_base_byte_range,
 356            });
 357        })
 358    }
 359
 360    fn hunks_intersecting_range_rev<'a>(
 361        &'a self,
 362        range: Range<Anchor>,
 363        buffer: &'a text::BufferSnapshot,
 364    ) -> impl 'a + Iterator<Item = DiffHunk> {
 365        let mut cursor = self
 366            .hunks
 367            .filter::<_, DiffHunkSummary>(buffer, move |summary| {
 368                let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
 369                let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
 370                !before_start && !after_end
 371            });
 372
 373        iter::from_fn(move || {
 374            cursor.prev(buffer);
 375
 376            let hunk = cursor.item()?;
 377            let range = hunk.buffer_range.to_point(buffer);
 378            let end_row = if range.end.column > 0 {
 379                range.end.row + 1
 380            } else {
 381                range.end.row
 382            };
 383
 384            Some(DiffHunk {
 385                row_range: range.start.row..end_row,
 386                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
 387                buffer_range: hunk.buffer_range.clone(),
 388                // The secondary status is not used by callers of this method.
 389                secondary_status: DiffHunkSecondaryStatus::None,
 390                secondary_diff_base_byte_range: None,
 391            })
 392        })
 393    }
 394
 395    fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
 396        let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
 397        let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
 398        old_cursor.next(new_snapshot);
 399        new_cursor.next(new_snapshot);
 400        let mut start = None;
 401        let mut end = None;
 402
 403        loop {
 404            match (new_cursor.item(), old_cursor.item()) {
 405                (Some(new_hunk), Some(old_hunk)) => {
 406                    match new_hunk
 407                        .buffer_range
 408                        .start
 409                        .cmp(&old_hunk.buffer_range.start, new_snapshot)
 410                    {
 411                        cmp::Ordering::Less => {
 412                            start.get_or_insert(new_hunk.buffer_range.start);
 413                            end.replace(new_hunk.buffer_range.end);
 414                            new_cursor.next(new_snapshot);
 415                        }
 416                        cmp::Ordering::Equal => {
 417                            if new_hunk != old_hunk {
 418                                start.get_or_insert(new_hunk.buffer_range.start);
 419                                if old_hunk
 420                                    .buffer_range
 421                                    .end
 422                                    .cmp(&new_hunk.buffer_range.end, new_snapshot)
 423                                    .is_ge()
 424                                {
 425                                    end.replace(old_hunk.buffer_range.end);
 426                                } else {
 427                                    end.replace(new_hunk.buffer_range.end);
 428                                }
 429                            }
 430
 431                            new_cursor.next(new_snapshot);
 432                            old_cursor.next(new_snapshot);
 433                        }
 434                        cmp::Ordering::Greater => {
 435                            start.get_or_insert(old_hunk.buffer_range.start);
 436                            end.replace(old_hunk.buffer_range.end);
 437                            old_cursor.next(new_snapshot);
 438                        }
 439                    }
 440                }
 441                (Some(new_hunk), None) => {
 442                    start.get_or_insert(new_hunk.buffer_range.start);
 443                    end.replace(new_hunk.buffer_range.end);
 444                    new_cursor.next(new_snapshot);
 445                }
 446                (None, Some(old_hunk)) => {
 447                    start.get_or_insert(old_hunk.buffer_range.start);
 448                    end.replace(old_hunk.buffer_range.end);
 449                    old_cursor.next(new_snapshot);
 450                }
 451                (None, None) => break,
 452            }
 453        }
 454
 455        start.zip(end).map(|(start, end)| start..end)
 456    }
 457}
 458
 459fn compute_hunks(
 460    diff_base: Option<(Arc<String>, Rope)>,
 461    buffer: text::BufferSnapshot,
 462) -> SumTree<InternalDiffHunk> {
 463    let mut tree = SumTree::new(&buffer);
 464
 465    if let Some((diff_base, diff_base_rope)) = diff_base {
 466        let buffer_text = buffer.as_rope().to_string();
 467
 468        let mut options = GitOptions::default();
 469        options.context_lines(0);
 470        let patch = GitPatch::from_buffers(
 471            diff_base.as_bytes(),
 472            None,
 473            buffer_text.as_bytes(),
 474            None,
 475            Some(&mut options),
 476        )
 477        .log_err();
 478
 479        // A common case in Zed is that the empty buffer is represented as just a newline,
 480        // but if we just compute a naive diff you get a "preserved" line in the middle,
 481        // which is a bit odd.
 482        if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
 483            tree.push(
 484                InternalDiffHunk {
 485                    buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
 486                    diff_base_byte_range: 0..diff_base.len() - 1,
 487                },
 488                &buffer,
 489            );
 490            return tree;
 491        }
 492
 493        if let Some(patch) = patch {
 494            let mut divergence = 0;
 495            for hunk_index in 0..patch.num_hunks() {
 496                let hunk = process_patch_hunk(
 497                    &patch,
 498                    hunk_index,
 499                    &diff_base_rope,
 500                    &buffer,
 501                    &mut divergence,
 502                );
 503                tree.push(hunk, &buffer);
 504            }
 505        }
 506    }
 507
 508    tree
 509}
 510
 511fn process_patch_hunk(
 512    patch: &GitPatch<'_>,
 513    hunk_index: usize,
 514    diff_base: &Rope,
 515    buffer: &text::BufferSnapshot,
 516    buffer_row_divergence: &mut i64,
 517) -> InternalDiffHunk {
 518    let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
 519    assert!(line_item_count > 0);
 520
 521    let mut first_deletion_buffer_row: Option<u32> = None;
 522    let mut buffer_row_range: Option<Range<u32>> = None;
 523    let mut diff_base_byte_range: Option<Range<usize>> = None;
 524    let mut first_addition_old_row: Option<u32> = None;
 525
 526    for line_index in 0..line_item_count {
 527        let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
 528        let kind = line.origin_value();
 529        let content_offset = line.content_offset() as isize;
 530        let content_len = line.content().len() as isize;
 531        match kind {
 532            GitDiffLineType::Addition => {
 533                if first_addition_old_row.is_none() {
 534                    first_addition_old_row = Some(
 535                        (line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
 536                    );
 537                }
 538                *buffer_row_divergence += 1;
 539                let row = line.new_lineno().unwrap().saturating_sub(1);
 540
 541                match &mut buffer_row_range {
 542                    Some(Range { end, .. }) => *end = row + 1,
 543                    None => buffer_row_range = Some(row..row + 1),
 544                }
 545            }
 546            GitDiffLineType::Deletion => {
 547                let end = content_offset + content_len;
 548
 549                match &mut diff_base_byte_range {
 550                    Some(head_byte_range) => head_byte_range.end = end as usize,
 551                    None => diff_base_byte_range = Some(content_offset as usize..end as usize),
 552                }
 553
 554                if first_deletion_buffer_row.is_none() {
 555                    let old_row = line.old_lineno().unwrap().saturating_sub(1);
 556                    let row = old_row as i64 + *buffer_row_divergence;
 557                    first_deletion_buffer_row = Some(row as u32);
 558                }
 559
 560                *buffer_row_divergence -= 1;
 561            }
 562            _ => {}
 563        }
 564    }
 565
 566    let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
 567        // Pure deletion hunk without addition.
 568        let row = first_deletion_buffer_row.unwrap();
 569        row..row
 570    });
 571    let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
 572        // Pure addition hunk without deletion.
 573        let row = first_addition_old_row.unwrap();
 574        let offset = diff_base.point_to_offset(Point::new(row, 0));
 575        offset..offset
 576    });
 577
 578    let start = Point::new(buffer_row_range.start, 0);
 579    let end = Point::new(buffer_row_range.end, 0);
 580    let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
 581    InternalDiffHunk {
 582        buffer_range,
 583        diff_base_byte_range,
 584    }
 585}
 586
 587impl std::fmt::Debug for BufferDiff {
 588    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 589        f.debug_struct("BufferChangeSet")
 590            .field("buffer_id", &self.buffer_id)
 591            .field("snapshot", &self.inner)
 592            .finish()
 593    }
 594}
 595
 596pub enum BufferDiffEvent {
 597    DiffChanged {
 598        changed_range: Option<Range<text::Anchor>>,
 599    },
 600    LanguageChanged,
 601}
 602
 603impl EventEmitter<BufferDiffEvent> for BufferDiff {}
 604
 605impl BufferDiff {
 606    #[cfg(test)]
 607    fn build_sync(
 608        buffer: text::BufferSnapshot,
 609        diff_base: String,
 610        cx: &mut gpui::TestAppContext,
 611    ) -> BufferDiffInner {
 612        let snapshot =
 613            cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
 614        cx.executor().block(snapshot)
 615    }
 616
 617    fn build(
 618        buffer: text::BufferSnapshot,
 619        diff_base: Option<Arc<String>>,
 620        language: Option<Arc<Language>>,
 621        language_registry: Option<Arc<LanguageRegistry>>,
 622        cx: &mut App,
 623    ) -> impl Future<Output = BufferDiffInner> {
 624        let diff_base =
 625            diff_base.map(|diff_base| (diff_base.clone(), Rope::from(diff_base.as_str())));
 626        let base_text_snapshot = diff_base.as_ref().map(|(_, diff_base)| {
 627            language::Buffer::build_snapshot(
 628                diff_base.clone(),
 629                language.clone(),
 630                language_registry.clone(),
 631                cx,
 632            )
 633        });
 634        let base_text_snapshot = cx.background_spawn(OptionFuture::from(base_text_snapshot));
 635
 636        let hunks = cx.background_spawn({
 637            let buffer = buffer.clone();
 638            async move { compute_hunks(diff_base, buffer) }
 639        });
 640
 641        async move {
 642            let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
 643            BufferDiffInner { base_text, hunks }
 644        }
 645    }
 646
 647    fn build_with_base_buffer(
 648        buffer: text::BufferSnapshot,
 649        diff_base: Option<Arc<String>>,
 650        diff_base_buffer: Option<language::BufferSnapshot>,
 651        cx: &App,
 652    ) -> impl Future<Output = BufferDiffInner> {
 653        let diff_base = diff_base.clone().zip(
 654            diff_base_buffer
 655                .clone()
 656                .map(|buffer| buffer.as_rope().clone()),
 657        );
 658        cx.background_spawn(async move {
 659            BufferDiffInner {
 660                hunks: compute_hunks(diff_base, buffer),
 661                base_text: diff_base_buffer,
 662            }
 663        })
 664    }
 665
 666    fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner {
 667        BufferDiffInner {
 668            hunks: SumTree::new(buffer),
 669            base_text: None,
 670        }
 671    }
 672
 673    pub fn build_with_single_insertion(
 674        insertion_present_in_secondary_diff: bool,
 675        buffer: language::BufferSnapshot,
 676        cx: &mut App,
 677    ) -> BufferDiffSnapshot {
 678        let base_text = language::Buffer::build_empty_snapshot(cx);
 679        let hunks = SumTree::from_item(
 680            InternalDiffHunk {
 681                buffer_range: Anchor::MIN..Anchor::MAX,
 682                diff_base_byte_range: 0..0,
 683            },
 684            &base_text,
 685        );
 686        BufferDiffSnapshot {
 687            inner: BufferDiffInner {
 688                hunks: hunks.clone(),
 689                base_text: Some(base_text.clone()),
 690            },
 691            secondary_diff: Some(Box::new(BufferDiffSnapshot {
 692                inner: BufferDiffInner {
 693                    hunks: if insertion_present_in_secondary_diff {
 694                        hunks
 695                    } else {
 696                        SumTree::new(&buffer.text)
 697                    },
 698                    base_text: Some(if insertion_present_in_secondary_diff {
 699                        base_text
 700                    } else {
 701                        buffer
 702                    }),
 703                },
 704                secondary_diff: None,
 705                is_single_insertion: true,
 706            })),
 707            is_single_insertion: true,
 708        }
 709    }
 710
 711    pub fn set_secondary_diff(&mut self, diff: Entity<BufferDiff>) {
 712        self.secondary_diff = Some(diff);
 713    }
 714
 715    pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
 716        Some(self.secondary_diff.as_ref()?.clone())
 717    }
 718
 719    pub fn range_to_hunk_range(
 720        &self,
 721        range: Range<Anchor>,
 722        buffer: &text::BufferSnapshot,
 723        cx: &App,
 724    ) -> Option<Range<Anchor>> {
 725        let start = self
 726            .hunks_intersecting_range(range.clone(), &buffer, cx)
 727            .next()?
 728            .buffer_range
 729            .start;
 730        let end = self
 731            .hunks_intersecting_range_rev(range.clone(), &buffer)
 732            .next()?
 733            .buffer_range
 734            .end;
 735        Some(start..end)
 736    }
 737
 738    #[allow(clippy::too_many_arguments)]
 739    pub async fn update_diff(
 740        this: Entity<BufferDiff>,
 741        buffer: text::BufferSnapshot,
 742        base_text: Option<Arc<String>>,
 743        base_text_changed: bool,
 744        language_changed: bool,
 745        language: Option<Arc<Language>>,
 746        language_registry: Option<Arc<LanguageRegistry>>,
 747        cx: &mut AsyncApp,
 748    ) -> anyhow::Result<Option<Range<Anchor>>> {
 749        let snapshot = if base_text_changed || language_changed {
 750            cx.update(|cx| {
 751                Self::build(
 752                    buffer.clone(),
 753                    base_text,
 754                    language.clone(),
 755                    language_registry.clone(),
 756                    cx,
 757                )
 758            })?
 759            .await
 760        } else {
 761            this.read_with(cx, |this, cx| {
 762                Self::build_with_base_buffer(
 763                    buffer.clone(),
 764                    base_text,
 765                    this.base_text().cloned(),
 766                    cx,
 767                )
 768            })?
 769            .await
 770        };
 771
 772        this.update(cx, |this, _| this.set_state(snapshot, &buffer))
 773    }
 774
 775    pub fn update_diff_from(
 776        &mut self,
 777        buffer: &text::BufferSnapshot,
 778        other: &Entity<Self>,
 779        cx: &mut Context<Self>,
 780    ) -> Option<Range<Anchor>> {
 781        let other = other.read(cx).inner.clone();
 782        self.set_state(other, buffer)
 783    }
 784
 785    fn set_state(
 786        &mut self,
 787        inner: BufferDiffInner,
 788        buffer: &text::BufferSnapshot,
 789    ) -> Option<Range<Anchor>> {
 790        let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) {
 791            (None, None) => None,
 792            (Some(old), Some(new)) if old.remote_id() == new.remote_id() => {
 793                inner.compare(&self.inner, buffer)
 794            }
 795            _ => Some(text::Anchor::MIN..text::Anchor::MAX),
 796        };
 797        self.inner = inner;
 798        changed_range
 799    }
 800
 801    pub fn base_text(&self) -> Option<&language::BufferSnapshot> {
 802        self.inner.base_text.as_ref()
 803    }
 804
 805    pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot {
 806        BufferDiffSnapshot {
 807            inner: self.inner.clone(),
 808            secondary_diff: self
 809                .secondary_diff
 810                .as_ref()
 811                .map(|diff| Box::new(diff.read(cx).snapshot(cx))),
 812            is_single_insertion: false,
 813        }
 814    }
 815
 816    pub fn hunks_intersecting_range<'a>(
 817        &'a self,
 818        range: Range<text::Anchor>,
 819        buffer_snapshot: &'a text::BufferSnapshot,
 820        cx: &'a App,
 821    ) -> impl 'a + Iterator<Item = DiffHunk> {
 822        let unstaged_counterpart = self
 823            .secondary_diff
 824            .as_ref()
 825            .map(|diff| &diff.read(cx).inner);
 826        self.inner
 827            .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart)
 828    }
 829
 830    pub fn hunks_intersecting_range_rev<'a>(
 831        &'a self,
 832        range: Range<text::Anchor>,
 833        buffer_snapshot: &'a text::BufferSnapshot,
 834    ) -> impl 'a + Iterator<Item = DiffHunk> {
 835        self.inner
 836            .hunks_intersecting_range_rev(range, buffer_snapshot)
 837    }
 838
 839    pub fn hunks_in_row_range<'a>(
 840        &'a self,
 841        range: Range<u32>,
 842        buffer: &'a text::BufferSnapshot,
 843        cx: &'a App,
 844    ) -> impl 'a + Iterator<Item = DiffHunk> {
 845        let start = buffer.anchor_before(Point::new(range.start, 0));
 846        let end = buffer.anchor_after(Point::new(range.end, 0));
 847        self.hunks_intersecting_range(start..end, buffer, cx)
 848    }
 849
 850    /// Used in cases where the change set isn't derived from git.
 851    pub fn set_base_text(
 852        &mut self,
 853        base_buffer: Entity<language::Buffer>,
 854        buffer: text::BufferSnapshot,
 855        cx: &mut Context<Self>,
 856    ) -> oneshot::Receiver<()> {
 857        let (tx, rx) = oneshot::channel();
 858        let this = cx.weak_entity();
 859        let base_buffer = base_buffer.read(cx);
 860        let language_registry = base_buffer.language_registry();
 861        let base_buffer = base_buffer.snapshot();
 862        let base_text = Arc::new(base_buffer.text());
 863
 864        let snapshot = BufferDiff::build(
 865            buffer.clone(),
 866            Some(base_text),
 867            base_buffer.language().cloned(),
 868            language_registry,
 869            cx,
 870        );
 871        let complete_on_drop = util::defer(|| {
 872            tx.send(()).ok();
 873        });
 874        cx.spawn(|_, mut cx| async move {
 875            let snapshot = snapshot.await;
 876            let Some(this) = this.upgrade() else {
 877                return;
 878            };
 879            this.update(&mut cx, |this, _| {
 880                this.set_state(snapshot, &buffer);
 881            })
 882            .log_err();
 883            drop(complete_on_drop)
 884        })
 885        .detach();
 886        rx
 887    }
 888
 889    #[cfg(any(test, feature = "test-support"))]
 890    pub fn base_text_string(&self) -> Option<String> {
 891        self.inner.base_text.as_ref().map(|buffer| buffer.text())
 892    }
 893
 894    pub fn new(buffer: &text::BufferSnapshot) -> Self {
 895        BufferDiff {
 896            buffer_id: buffer.remote_id(),
 897            inner: BufferDiff::build_empty(buffer),
 898            secondary_diff: None,
 899        }
 900    }
 901
 902    #[cfg(any(test, feature = "test-support"))]
 903    pub fn new_with_base_text(
 904        base_text: &str,
 905        buffer: &Entity<language::Buffer>,
 906        cx: &mut App,
 907    ) -> Self {
 908        let mut base_text = base_text.to_owned();
 909        text::LineEnding::normalize(&mut base_text);
 910        let snapshot = BufferDiff::build(
 911            buffer.read(cx).text_snapshot(),
 912            Some(base_text.into()),
 913            None,
 914            None,
 915            cx,
 916        );
 917        let snapshot = cx.background_executor().block(snapshot);
 918        BufferDiff {
 919            buffer_id: buffer.read(cx).remote_id(),
 920            inner: snapshot,
 921            secondary_diff: None,
 922        }
 923    }
 924
 925    #[cfg(any(test, feature = "test-support"))]
 926    pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
 927        let base_text = self
 928            .inner
 929            .base_text
 930            .as_ref()
 931            .map(|base_text| base_text.text());
 932        let snapshot = BufferDiff::build_with_base_buffer(
 933            buffer.clone(),
 934            base_text.clone().map(Arc::new),
 935            self.inner.base_text.clone(),
 936            cx,
 937        );
 938        let snapshot = cx.background_executor().block(snapshot);
 939        let changed_range = self.set_state(snapshot, &buffer);
 940        cx.emit(BufferDiffEvent::DiffChanged { changed_range });
 941    }
 942}
 943
 944impl DiffHunk {
 945    pub fn status(&self) -> DiffHunkStatus {
 946        let kind = if self.buffer_range.start == self.buffer_range.end {
 947            DiffHunkStatusKind::Deleted
 948        } else if self.diff_base_byte_range.is_empty() {
 949            DiffHunkStatusKind::Added
 950        } else {
 951            DiffHunkStatusKind::Modified
 952        };
 953        DiffHunkStatus {
 954            kind,
 955            secondary: self.secondary_status,
 956        }
 957    }
 958}
 959
 960impl DiffHunkStatus {
 961    pub fn is_deleted(&self) -> bool {
 962        self.kind == DiffHunkStatusKind::Deleted
 963    }
 964
 965    pub fn is_added(&self) -> bool {
 966        self.kind == DiffHunkStatusKind::Added
 967    }
 968
 969    pub fn is_modified(&self) -> bool {
 970        self.kind == DiffHunkStatusKind::Modified
 971    }
 972
 973    pub fn added(secondary: DiffHunkSecondaryStatus) -> Self {
 974        Self {
 975            kind: DiffHunkStatusKind::Added,
 976            secondary,
 977        }
 978    }
 979
 980    pub fn modified(secondary: DiffHunkSecondaryStatus) -> Self {
 981        Self {
 982            kind: DiffHunkStatusKind::Modified,
 983            secondary,
 984        }
 985    }
 986
 987    pub fn deleted(secondary: DiffHunkSecondaryStatus) -> Self {
 988        Self {
 989            kind: DiffHunkStatusKind::Deleted,
 990            secondary,
 991        }
 992    }
 993
 994    #[cfg(any(test, feature = "test-support"))]
 995    pub fn deleted_none() -> Self {
 996        Self {
 997            kind: DiffHunkStatusKind::Deleted,
 998            secondary: DiffHunkSecondaryStatus::None,
 999        }
1000    }
1001
1002    #[cfg(any(test, feature = "test-support"))]
1003    pub fn added_none() -> Self {
1004        Self {
1005            kind: DiffHunkStatusKind::Added,
1006            secondary: DiffHunkSecondaryStatus::None,
1007        }
1008    }
1009
1010    #[cfg(any(test, feature = "test-support"))]
1011    pub fn modified_none() -> Self {
1012        Self {
1013            kind: DiffHunkStatusKind::Modified,
1014            secondary: DiffHunkSecondaryStatus::None,
1015        }
1016    }
1017}
1018
1019/// Range (crossing new lines), old, new
1020#[cfg(any(test, feature = "test-support"))]
1021#[track_caller]
1022pub fn assert_hunks<Iter>(
1023    diff_hunks: Iter,
1024    buffer: &text::BufferSnapshot,
1025    diff_base: &str,
1026    expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
1027) where
1028    Iter: Iterator<Item = DiffHunk>,
1029{
1030    let actual_hunks = diff_hunks
1031        .map(|hunk| {
1032            (
1033                hunk.row_range.clone(),
1034                &diff_base[hunk.diff_base_byte_range.clone()],
1035                buffer
1036                    .text_for_range(
1037                        Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
1038                    )
1039                    .collect::<String>(),
1040                hunk.status(),
1041            )
1042        })
1043        .collect::<Vec<_>>();
1044
1045    let expected_hunks: Vec<_> = expected_hunks
1046        .iter()
1047        .map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status))
1048        .collect();
1049
1050    assert_eq!(actual_hunks, expected_hunks);
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use std::fmt::Write as _;
1056
1057    use super::*;
1058    use gpui::TestAppContext;
1059    use rand::{rngs::StdRng, Rng as _};
1060    use text::{Buffer, BufferId, Rope};
1061    use unindent::Unindent as _;
1062
1063    #[ctor::ctor]
1064    fn init_logger() {
1065        if std::env::var("RUST_LOG").is_ok() {
1066            env_logger::init();
1067        }
1068    }
1069
1070    #[gpui::test]
1071    async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
1072        let diff_base = "
1073            one
1074            two
1075            three
1076        "
1077        .unindent();
1078
1079        let buffer_text = "
1080            one
1081            HELLO
1082            three
1083        "
1084        .unindent();
1085
1086        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1087        let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
1088        assert_hunks(
1089            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1090            &buffer,
1091            &diff_base,
1092            &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
1093        );
1094
1095        buffer.edit([(0..0, "point five\n")]);
1096        diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
1097        assert_hunks(
1098            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1099            &buffer,
1100            &diff_base,
1101            &[
1102                (0..1, "", "point five\n", DiffHunkStatus::added_none()),
1103                (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
1104            ],
1105        );
1106
1107        diff = BufferDiff::build_empty(&buffer);
1108        assert_hunks(
1109            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
1110            &buffer,
1111            &diff_base,
1112            &[],
1113        );
1114    }
1115
1116    #[gpui::test]
1117    async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
1118        let head_text = "
1119            zero
1120            one
1121            two
1122            three
1123            four
1124            five
1125            six
1126            seven
1127            eight
1128            nine
1129        "
1130        .unindent();
1131
1132        let index_text = "
1133            zero
1134            one
1135            TWO
1136            three
1137            FOUR
1138            five
1139            six
1140            seven
1141            eight
1142            NINE
1143        "
1144        .unindent();
1145
1146        let buffer_text = "
1147            zero
1148            one
1149            TWO
1150            three
1151            FOUR
1152            FIVE
1153            six
1154            SEVEN
1155            eight
1156            nine
1157        "
1158        .unindent();
1159
1160        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1161        let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx);
1162
1163        let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
1164
1165        let expected_hunks = vec![
1166            (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
1167            (
1168                4..6,
1169                "four\nfive\n",
1170                "FOUR\nFIVE\n",
1171                DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
1172            ),
1173            (
1174                7..8,
1175                "seven\n",
1176                "SEVEN\n",
1177                DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
1178            ),
1179        ];
1180
1181        assert_hunks(
1182            uncommitted_diff.hunks_intersecting_range(
1183                Anchor::MIN..Anchor::MAX,
1184                &buffer,
1185                Some(&unstaged_diff),
1186            ),
1187            &buffer,
1188            &head_text,
1189            &expected_hunks,
1190        );
1191    }
1192
1193    #[gpui::test]
1194    async fn test_buffer_diff_range(cx: &mut TestAppContext) {
1195        let diff_base = Arc::new(
1196            "
1197            one
1198            two
1199            three
1200            four
1201            five
1202            six
1203            seven
1204            eight
1205            nine
1206            ten
1207        "
1208            .unindent(),
1209        );
1210
1211        let buffer_text = "
1212            A
1213            one
1214            B
1215            two
1216            C
1217            three
1218            HELLO
1219            four
1220            five
1221            SIXTEEN
1222            seven
1223            eight
1224            WORLD
1225            nine
1226
1227            ten
1228
1229        "
1230        .unindent();
1231
1232        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
1233        let diff = cx
1234            .update(|cx| {
1235                BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx)
1236            })
1237            .await;
1238        assert_eq!(
1239            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None)
1240                .count(),
1241            8
1242        );
1243
1244        assert_hunks(
1245            diff.hunks_intersecting_range(
1246                buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
1247                &buffer,
1248                None,
1249            ),
1250            &buffer,
1251            &diff_base,
1252            &[
1253                (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
1254                (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
1255                (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
1256            ],
1257        );
1258    }
1259
1260    #[gpui::test]
1261    async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
1262        let base_text = "
1263            zero
1264            one
1265            two
1266            three
1267            four
1268            five
1269            six
1270            seven
1271            eight
1272            nine
1273        "
1274        .unindent();
1275
1276        let buffer_text_1 = "
1277            one
1278            three
1279            four
1280            five
1281            SIX
1282            seven
1283            eight
1284            NINE
1285        "
1286        .unindent();
1287
1288        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
1289
1290        let empty_diff = BufferDiff::build_empty(&buffer);
1291        let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1292        let range = diff_1.compare(&empty_diff, &buffer).unwrap();
1293        assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
1294
1295        // Edit does not affect the diff.
1296        buffer.edit_via_marked_text(
1297            &"
1298                one
1299                three
1300                four
1301                five
1302                «SIX.5»
1303                seven
1304                eight
1305                NINE
1306            "
1307            .unindent(),
1308        );
1309        let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1310        assert_eq!(None, diff_2.compare(&diff_1, &buffer));
1311
1312        // Edit turns a deletion hunk into a modification.
1313        buffer.edit_via_marked_text(
1314            &"
1315                one
1316                «THREE»
1317                four
1318                five
1319                SIX.5
1320                seven
1321                eight
1322                NINE
1323            "
1324            .unindent(),
1325        );
1326        let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1327        let range = diff_3.compare(&diff_2, &buffer).unwrap();
1328        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
1329
1330        // Edit turns a modification hunk into a deletion.
1331        buffer.edit_via_marked_text(
1332            &"
1333                one
1334                THREE
1335                four
1336                five«»
1337                seven
1338                eight
1339                NINE
1340            "
1341            .unindent(),
1342        );
1343        let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
1344        let range = diff_4.compare(&diff_3, &buffer).unwrap();
1345        assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
1346
1347        // Edit introduces a new insertion hunk.
1348        buffer.edit_via_marked_text(
1349            &"
1350                one
1351                THREE
1352                four«
1353                FOUR.5
1354                »five
1355                seven
1356                eight
1357                NINE
1358            "
1359            .unindent(),
1360        );
1361        let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx);
1362        let range = diff_5.compare(&diff_4, &buffer).unwrap();
1363        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
1364
1365        // Edit removes a hunk.
1366        buffer.edit_via_marked_text(
1367            &"
1368                one
1369                THREE
1370                four
1371                FOUR.5
1372                five
1373                seven
1374                eight
1375                «nine»
1376            "
1377            .unindent(),
1378        );
1379        let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx);
1380        let range = diff_6.compare(&diff_5, &buffer).unwrap();
1381        assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
1382    }
1383
1384    #[gpui::test(iterations = 100)]
1385    async fn test_secondary_edits_for_stage_unstage(cx: &mut TestAppContext, mut rng: StdRng) {
1386        fn gen_line(rng: &mut StdRng) -> String {
1387            if rng.gen_bool(0.2) {
1388                "\n".to_owned()
1389            } else {
1390                let c = rng.gen_range('A'..='Z');
1391                format!("{c}{c}{c}\n")
1392            }
1393        }
1394
1395        fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
1396            let mut old_lines = {
1397                let mut old_lines = Vec::new();
1398                let mut old_lines_iter = head.lines();
1399                while let Some(line) = old_lines_iter.next() {
1400                    assert!(!line.ends_with("\n"));
1401                    old_lines.push(line.to_owned());
1402                }
1403                if old_lines.last().is_some_and(|line| line.is_empty()) {
1404                    old_lines.pop();
1405                }
1406                old_lines.into_iter()
1407            };
1408            let mut result = String::new();
1409            let unchanged_count = rng.gen_range(0..=old_lines.len());
1410            result +=
1411                &old_lines
1412                    .by_ref()
1413                    .take(unchanged_count)
1414                    .fold(String::new(), |mut s, line| {
1415                        writeln!(&mut s, "{line}").unwrap();
1416                        s
1417                    });
1418            while old_lines.len() > 0 {
1419                let deleted_count = rng.gen_range(0..=old_lines.len());
1420                let _advance = old_lines
1421                    .by_ref()
1422                    .take(deleted_count)
1423                    .map(|line| line.len() + 1)
1424                    .sum::<usize>();
1425                let minimum_added = if deleted_count == 0 { 1 } else { 0 };
1426                let added_count = rng.gen_range(minimum_added..=5);
1427                let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
1428                result += &addition;
1429
1430                if old_lines.len() > 0 {
1431                    let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
1432                    if blank_lines == old_lines.len() {
1433                        break;
1434                    };
1435                    let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
1436                    result += &old_lines.by_ref().take(unchanged_count).fold(
1437                        String::new(),
1438                        |mut s, line| {
1439                            writeln!(&mut s, "{line}").unwrap();
1440                            s
1441                        },
1442                    );
1443                }
1444            }
1445            result
1446        }
1447
1448        fn uncommitted_diff(
1449            working_copy: &language::BufferSnapshot,
1450            index_text: &Entity<language::Buffer>,
1451            head_text: String,
1452            cx: &mut TestAppContext,
1453        ) -> BufferDiff {
1454            let inner = BufferDiff::build_sync(working_copy.text.clone(), head_text, cx);
1455            let secondary = BufferDiff {
1456                buffer_id: working_copy.remote_id(),
1457                inner: BufferDiff::build_sync(
1458                    working_copy.text.clone(),
1459                    index_text.read_with(cx, |index_text, _| index_text.text()),
1460                    cx,
1461                ),
1462                secondary_diff: None,
1463            };
1464            let secondary = cx.new(|_| secondary);
1465            BufferDiff {
1466                buffer_id: working_copy.remote_id(),
1467                inner,
1468                secondary_diff: Some(secondary),
1469            }
1470        }
1471
1472        let operations = std::env::var("OPERATIONS")
1473            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1474            .unwrap_or(10);
1475
1476        let rng = &mut rng;
1477        let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
1478            writeln!(&mut s, "{c}{c}{c}").unwrap();
1479            s
1480        });
1481        let working_copy = gen_working_copy(rng, &head_text);
1482        let working_copy = cx.new(|cx| {
1483            language::Buffer::local_normalized(
1484                Rope::from(working_copy.as_str()),
1485                text::LineEnding::default(),
1486                cx,
1487            )
1488        });
1489        let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
1490        let index_text = cx.new(|cx| {
1491            language::Buffer::local_normalized(
1492                if rng.gen() {
1493                    Rope::from(head_text.as_str())
1494                } else {
1495                    working_copy.as_rope().clone()
1496                },
1497                text::LineEnding::default(),
1498                cx,
1499            )
1500        });
1501
1502        let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1503        let mut hunks = cx.update(|cx| {
1504            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1505                .collect::<Vec<_>>()
1506        });
1507        if hunks.len() == 0 {
1508            return;
1509        }
1510
1511        for _ in 0..operations {
1512            let i = rng.gen_range(0..hunks.len());
1513            let hunk = &mut hunks[i];
1514            let hunk_fields = (
1515                hunk.diff_base_byte_range.clone(),
1516                hunk.secondary_diff_base_byte_range.clone(),
1517                hunk.buffer_range.clone(),
1518            );
1519            let stage = match (
1520                hunk.secondary_status,
1521                hunk.secondary_diff_base_byte_range.clone(),
1522            ) {
1523                (DiffHunkSecondaryStatus::HasSecondaryHunk, Some(_)) => {
1524                    hunk.secondary_status = DiffHunkSecondaryStatus::None;
1525                    hunk.secondary_diff_base_byte_range = None;
1526                    true
1527                }
1528                (DiffHunkSecondaryStatus::None, None) => {
1529                    hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
1530                    // We don't look at this, just notice whether it's Some or not.
1531                    hunk.secondary_diff_base_byte_range = Some(17..17);
1532                    false
1533                }
1534                _ => unreachable!(),
1535            };
1536
1537            let snapshot = cx.update(|cx| diff.snapshot(cx));
1538            let edits = snapshot.secondary_edits_for_stage_or_unstage(
1539                stage,
1540                [hunk_fields].into_iter(),
1541                &working_copy,
1542            );
1543            index_text.update(cx, |index_text, cx| {
1544                index_text.edit(edits, None, cx);
1545            });
1546
1547            diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
1548            let found_hunks = cx.update(|cx| {
1549                diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
1550                    .collect::<Vec<_>>()
1551            });
1552            assert_eq!(hunks.len(), found_hunks.len());
1553            for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
1554                assert_eq!(
1555                    expected_hunk.buffer_range.to_point(&working_copy),
1556                    found_hunk.buffer_range.to_point(&working_copy)
1557                );
1558                assert_eq!(
1559                    expected_hunk.diff_base_byte_range,
1560                    found_hunk.diff_base_byte_range
1561                );
1562                assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
1563                assert_eq!(
1564                    expected_hunk.secondary_diff_base_byte_range.is_some(),
1565                    found_hunk.secondary_diff_base_byte_range.is_some()
1566                )
1567            }
1568            hunks = found_hunks;
1569        }
1570    }
1571}