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