use futures::channel::oneshot;use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};use language::{ BufferRow, Capability, DiffOptions, File, Language, LanguageName, LanguageRegistry, language_settings::language_settings, word_diff_ranges,};use rope::Rope;use std::{cmp::Ordering, future::Future, iter, ops::Range, sync::Arc};use sum_tree::SumTree;use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _};use util::ResultExt; pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner<Entity<language::Buffer>>, // diff of the index vs head secondary_diff: Option<Entity<BufferDiff>>,} #[derive(Clone)]pub struct BufferDiffSnapshot { inner: BufferDiffInner<language::BufferSnapshot>, secondary_diff: Option<Box<BufferDiffSnapshot>>,} impl std::fmt::Debug for BufferDiffSnapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("inner", &self.inner) .field("secondary_diff", &self.secondary_diff) .finish() }} #[derive(Clone)]pub struct BufferDiffUpdate { base_text_changed: bool, inner: BufferDiffInner<Arc<str>>,} #[derive(Clone)]struct BufferDiffInner<BaseText> { hunks: SumTree<InternalDiffHunk>, pending_hunks: SumTree<PendingHunk>, base_text: BaseText, base_text_exists: bool,} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub struct DiffHunkStatus { pub kind: DiffHunkStatusKind, pub secondary: DiffHunkSecondaryStatus,} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum DiffHunkStatusKind { Added, Modified, Deleted,} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]/// Diff of Working Copy vs Index/// aka 'is this hunk staged or not'pub enum DiffHunkSecondaryStatus { /// Unstaged HasSecondaryHunk, /// Partially staged OverlapsWithSecondaryHunk, /// Staged NoSecondaryHunk, /// We are unstaging SecondaryHunkAdditionPending, /// We are stagind SecondaryHunkRemovalPending,} /// A diff hunk resolved to rows in the buffer.#[derive(Debug, Clone, PartialEq, Eq)]pub struct DiffHunk { /// The buffer range as points. pub range: Range<Point>, /// The range in the buffer to which this hunk corresponds. pub buffer_range: Range<Anchor>, /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range<usize>, pub secondary_status: DiffHunkSecondaryStatus, // Anchors representing the word diff locations in the active buffer pub buffer_word_diffs: Vec<Range<Anchor>>, // Offsets relative to the start of the deleted diff that represent word diff locations pub base_word_diffs: Vec<Range<usize>>,} /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.#[derive(Debug, Clone, PartialEq, Eq)]struct InternalDiffHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, base_word_diffs: Vec<Range<usize>>, buffer_word_diffs: Vec<Range<Anchor>>,} #[derive(Debug, Clone, PartialEq, Eq)]struct PendingHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, buffer_version: clock::Global, new_status: DiffHunkSecondaryStatus,} #[derive(Debug, Clone)]pub struct DiffHunkSummary { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>,} impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), } }} impl sum_tree::Item for PendingHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), } }} impl sum_tree::Summary for DiffHunkSummary { type Context<'a> = &'a text::BufferSnapshot; fn zero(_cx: Self::Context<'_>) -> Self { DiffHunkSummary { buffer_range: Anchor::MIN..Anchor::MIN, diff_base_byte_range: 0..0, } } fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) { self.buffer_range.start = *self .buffer_range .start .min(&other.buffer_range.start, buffer); self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer); self.diff_base_byte_range.start = self .diff_base_byte_range .start .min(other.diff_base_byte_range.start); self.diff_base_byte_range.end = self .diff_base_byte_range .end .max(other.diff_base_byte_range.end); }} impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor { fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering { if self .cmp(&cursor_location.buffer_range.start, buffer) .is_lt() { Ordering::Less } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() { Ordering::Greater } else { Ordering::Equal } }} impl std::fmt::Debug for BufferDiffInner<language::BufferSnapshot> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) .field("remote_id", &self.base_text.remote_id()) .finish() }} impl BufferDiffSnapshot { #[cfg(test)] fn new_sync( buffer: text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx)) } pub fn is_empty(&self) -> bool { self.inner.hunks.is_empty() } pub fn base_text_string(&self) -> Option<String> { self.inner .base_text_exists .then(|| self.inner.base_text.text()) } pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> { self.secondary_diff.as_deref() } pub fn hunks_intersecting_range<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); self.inner .hunks_intersecting_range(range, buffer, unstaged_counterpart) } pub fn hunks_intersecting_range_rev<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); !before_start && !after_end }; self.inner.hunks_intersecting_range_rev_impl(filter, buffer) } pub fn hunks_intersecting_base_text_range<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end < range.start; let after_end = summary.diff_base_byte_range.start > range.end; !before_start && !after_end }; self.inner .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) } pub fn hunks_intersecting_base_text_range_rev<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt(); let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt(); !before_start && !after_end }; self.inner .hunks_intersecting_range_rev_impl(filter, main_buffer) } pub fn hunks<'a>( &'a self, buffer_snapshot: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { self.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()), buffer_snapshot, ) } pub fn hunks_in_row_range<'a>( &'a self, range: Range<u32>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let start = buffer.anchor_before(Point::new(range.start, 0)); let end = buffer.anchor_after(Point::new(range.end, 0)); self.hunks_intersecting_range(start..end, buffer) } pub fn range_to_hunk_range( &self, range: Range<Anchor>, buffer: &text::BufferSnapshot, ) -> (Option<Range<Anchor>>, Option<Range<usize>>) { let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next(); let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next(); let range = first_hunk .as_ref() .zip(last_hunk.as_ref()) .map(|(first, last)| first.buffer_range.start..last.buffer_range.end); let base_text_range = first_hunk .zip(last_hunk) .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end); (range, base_text_range) } pub fn base_text(&self) -> &language::BufferSnapshot { &self.inner.base_text } /// If this function returns `true`, the base texts are equal. If this /// function returns `false`, they might be equal, but might not. This /// result is used to avoid recalculating diffs in situations where we know /// nothing has changed. pub fn base_texts_definitely_eq(&self, other: &Self) -> bool { if self.inner.base_text_exists != other.inner.base_text_exists { return false; } let left = &self.inner.base_text; let right = &other.inner.base_text; let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty()); let (new_id, new_version, new_empty) = (right.remote_id(), right.version(), right.is_empty()); (new_id == old_id && new_version == old_version) || (new_empty && old_empty) } pub fn row_to_base_text_row( &self, row: BufferRow, bias: Bias, buffer: &text::BufferSnapshot, ) -> u32 { // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start let target = buffer.anchor_before(Point::new(row, 0)); // Find the last hunk that starts before the target. let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer); cursor.seek(&target, Bias::Left); if cursor .item() .is_none_or(|hunk| hunk.buffer_range.start.cmp(&target, buffer).is_gt()) { cursor.prev(); } let unclipped_point = if let Some(hunk) = cursor.item() && hunk.buffer_range.start.cmp(&target, buffer).is_le() { // Found a hunk that starts before the target. let hunk_base_text_end = cursor.end().diff_base_byte_range.end; let unclipped_point = if target.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { // Target falls strictly between two hunks. let mut unclipped_point = hunk_base_text_end.to_point(self.base_text()); unclipped_point += Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); unclipped_point } else if bias == Bias::Right { hunk_base_text_end.to_point(self.base_text()) } else { hunk.diff_base_byte_range.start.to_point(self.base_text()) }; // Move the cursor so that at the next step we can clip with the start of the next hunk. cursor.next(); unclipped_point } else { // Target is before the added region for the first hunk. debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { target.cmp(&first_hunk.buffer_range.start, buffer).is_le() })); Point::new(row, 0) }; // If the target falls in the region between two hunks, we added an overshoot above. // There may be changes in the main buffer that are not reflected in the hunks, // so we need to ensure this overshoot keeps us in the corresponding base text region. let max_point = if let Some(next_hunk) = cursor.item() { next_hunk .diff_base_byte_range .start .to_point(self.base_text()) } else { self.base_text().max_point() }; unclipped_point.min(max_point).row }} impl BufferDiffInner<Entity<language::Buffer>> { /// Returns the new index text and new pending hunks. fn stage_or_unstage_hunks_impl( &mut self, unstaged_diff: &Self, stage: bool, hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, cx: &mut Context<BufferDiff>, ) -> Option<Rope> { let head_text = self .base_text_exists .then(|| self.base_text.read(cx).as_rope().clone()); let index_text = unstaged_diff .base_text_exists .then(|| unstaged_diff.base_text.read(cx).as_rope().clone()); // If the file doesn't exist in either HEAD or the index, then the // entire file must be either created or deleted in the index. let (index_text, head_text) = match (index_text, head_text) { (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text), (index_text, head_text) => { let (new_index_text, new_status) = if stage { log::debug!("stage all"); ( file_exists.then(|| buffer.as_rope().clone()), DiffHunkSecondaryStatus::SecondaryHunkRemovalPending, ) } else { log::debug!("unstage all"); ( head_text, DiffHunkSecondaryStatus::SecondaryHunkAdditionPending, ) }; let hunk = PendingHunk { buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()), diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()), buffer_version: buffer.version().clone(), new_status, }; self.pending_hunks = SumTree::from_item(hunk, buffer); return new_index_text; } }; let mut pending_hunks = SumTree::new(buffer); let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer); // first, merge new hunks into pending_hunks for DiffHunk { buffer_range, diff_base_byte_range, secondary_status, .. } in hunks.iter().cloned() { let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left); pending_hunks.append(preceding_pending_hunks, buffer); // Skip all overlapping or adjacent old pending hunks while old_pending_hunks.item().is_some_and(|old_hunk| { old_hunk .buffer_range .start .cmp(&buffer_range.end, buffer) .is_le() }) { old_pending_hunks.next(); } if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk) { continue; } pending_hunks.push( PendingHunk { buffer_range, diff_base_byte_range, buffer_version: buffer.version().clone(), new_status: if stage { DiffHunkSecondaryStatus::SecondaryHunkRemovalPending } else { DiffHunkSecondaryStatus::SecondaryHunkAdditionPending }, }, buffer, ); } // append the remainder pending_hunks.append(old_pending_hunks.suffix(), buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer); unstaged_hunk_cursor.next(); // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits let mut prev_unstaged_hunk_buffer_end = 0; let mut prev_unstaged_hunk_base_text_end = 0; let mut edits = Vec::<(Range<usize>, String)>::new(); let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable(); while let Some(PendingHunk { buffer_range, diff_base_byte_range, new_status, .. }) = pending_hunks_iter.next() { // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);Lines 1–500 of 2673. Load more
use futures::channel::oneshot; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; use language::{ BufferRow, Capability, DiffOptions, File, Language, LanguageName, LanguageRegistry, language_settings::language_settings, word_diff_ranges, }; use rope::Rope; use std::{cmp::Ordering, future::Future, iter, ops::Range, sync::Arc}; use sum_tree::SumTree; use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _}; use util::ResultExt; pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner<Entity<language::Buffer>>, // diff of the index vs head secondary_diff: Option<Entity<BufferDiff>>, } #[derive(Clone)] pub struct BufferDiffSnapshot { inner: BufferDiffInner<language::BufferSnapshot>, secondary_diff: Option<Box<BufferDiffSnapshot>>, } impl std::fmt::Debug for BufferDiffSnapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("inner", &self.inner) .field("secondary_diff", &self.secondary_diff) .finish() } } #[derive(Clone)] pub struct BufferDiffUpdate { base_text_changed: bool, inner: BufferDiffInner<Arc<str>>, } #[derive(Clone)] struct BufferDiffInner<BaseText> { hunks: SumTree<InternalDiffHunk>, pending_hunks: SumTree<PendingHunk>, base_text: BaseText, base_text_exists: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DiffHunkStatus { pub kind: DiffHunkStatusKind, pub secondary: DiffHunkSecondaryStatus, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DiffHunkStatusKind { Added, Modified, Deleted, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// Diff of Working Copy vs Index /// aka 'is this hunk staged or not' pub enum DiffHunkSecondaryStatus { /// Unstaged HasSecondaryHunk, /// Partially staged OverlapsWithSecondaryHunk, /// Staged NoSecondaryHunk, /// We are unstaging SecondaryHunkAdditionPending, /// We are stagind SecondaryHunkRemovalPending, } /// A diff hunk resolved to rows in the buffer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { /// The buffer range as points. pub range: Range<Point>, /// The range in the buffer to which this hunk corresponds. pub buffer_range: Range<Anchor>, /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range<usize>, pub secondary_status: DiffHunkSecondaryStatus, // Anchors representing the word diff locations in the active buffer pub buffer_word_diffs: Vec<Range<Anchor>>, // Offsets relative to the start of the deleted diff that represent word diff locations pub base_word_diffs: Vec<Range<usize>>, } /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. #[derive(Debug, Clone, PartialEq, Eq)] struct InternalDiffHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, base_word_diffs: Vec<Range<usize>>, buffer_word_diffs: Vec<Range<Anchor>>, } #[derive(Debug, Clone, PartialEq, Eq)] struct PendingHunk { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, buffer_version: clock::Global, new_status: DiffHunkSecondaryStatus, } #[derive(Debug, Clone)] pub struct DiffHunkSummary { buffer_range: Range<Anchor>, diff_base_byte_range: Range<usize>, } impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), } } } impl sum_tree::Item for PendingHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), diff_base_byte_range: self.diff_base_byte_range.clone(), } } } impl sum_tree::Summary for DiffHunkSummary { type Context<'a> = &'a text::BufferSnapshot; fn zero(_cx: Self::Context<'_>) -> Self { DiffHunkSummary { buffer_range: Anchor::MIN..Anchor::MIN, diff_base_byte_range: 0..0, } } fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) { self.buffer_range.start = *self .buffer_range .start .min(&other.buffer_range.start, buffer); self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer); self.diff_base_byte_range.start = self .diff_base_byte_range .start .min(other.diff_base_byte_range.start); self.diff_base_byte_range.end = self .diff_base_byte_range .end .max(other.diff_base_byte_range.end); } } impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor { fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering { if self .cmp(&cursor_location.buffer_range.start, buffer) .is_lt() { Ordering::Less } else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() { Ordering::Greater } else { Ordering::Equal } } } impl std::fmt::Debug for BufferDiffInner<language::BufferSnapshot> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) .field("remote_id", &self.base_text.remote_id()) .finish() } } impl BufferDiffSnapshot { #[cfg(test)] fn new_sync( buffer: text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { let buffer_diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); buffer_diff.update(cx, |buffer_diff, cx| buffer_diff.snapshot(cx)) } pub fn is_empty(&self) -> bool { self.inner.hunks.is_empty() } pub fn base_text_string(&self) -> Option<String> { self.inner .base_text_exists .then(|| self.inner.base_text.text()) } pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> { self.secondary_diff.as_deref() } pub fn hunks_intersecting_range<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); self.inner .hunks_intersecting_range(range, buffer, unstaged_counterpart) } pub fn hunks_intersecting_range_rev<'a>( &'a self, range: Range<Anchor>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); !before_start && !after_end }; self.inner.hunks_intersecting_range_rev_impl(filter, buffer) } pub fn hunks_intersecting_base_text_range<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end < range.start; let after_end = summary.diff_base_byte_range.start > range.end; !before_start && !after_end }; self.inner .hunks_intersecting_range_impl(filter, main_buffer, unstaged_counterpart) } pub fn hunks_intersecting_base_text_range_rev<'a>( &'a self, range: Range<usize>, main_buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let filter = move |summary: &DiffHunkSummary| { let before_start = summary.diff_base_byte_range.end.cmp(&range.start).is_lt(); let after_end = summary.diff_base_byte_range.start.cmp(&range.end).is_gt(); !before_start && !after_end }; self.inner .hunks_intersecting_range_rev_impl(filter, main_buffer) } pub fn hunks<'a>( &'a self, buffer_snapshot: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { self.hunks_intersecting_range( Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()), buffer_snapshot, ) } pub fn hunks_in_row_range<'a>( &'a self, range: Range<u32>, buffer: &'a text::BufferSnapshot, ) -> impl 'a + Iterator<Item = DiffHunk> { let start = buffer.anchor_before(Point::new(range.start, 0)); let end = buffer.anchor_after(Point::new(range.end, 0)); self.hunks_intersecting_range(start..end, buffer) } pub fn range_to_hunk_range( &self, range: Range<Anchor>, buffer: &text::BufferSnapshot, ) -> (Option<Range<Anchor>>, Option<Range<usize>>) { let first_hunk = self.hunks_intersecting_range(range.clone(), buffer).next(); let last_hunk = self.hunks_intersecting_range_rev(range, buffer).next(); let range = first_hunk .as_ref() .zip(last_hunk.as_ref()) .map(|(first, last)| first.buffer_range.start..last.buffer_range.end); let base_text_range = first_hunk .zip(last_hunk) .map(|(first, last)| first.diff_base_byte_range.start..last.diff_base_byte_range.end); (range, base_text_range) } pub fn base_text(&self) -> &language::BufferSnapshot { &self.inner.base_text } /// If this function returns `true`, the base texts are equal. If this /// function returns `false`, they might be equal, but might not. This /// result is used to avoid recalculating diffs in situations where we know /// nothing has changed. pub fn base_texts_definitely_eq(&self, other: &Self) -> bool { if self.inner.base_text_exists != other.inner.base_text_exists { return false; } let left = &self.inner.base_text; let right = &other.inner.base_text; let (old_id, old_version, old_empty) = (left.remote_id(), left.version(), left.is_empty()); let (new_id, new_version, new_empty) = (right.remote_id(), right.version(), right.is_empty()); (new_id == old_id && new_version == old_version) || (new_empty && old_empty) } pub fn row_to_base_text_row( &self, row: BufferRow, bias: Bias, buffer: &text::BufferSnapshot, ) -> u32 { // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start let target = buffer.anchor_before(Point::new(row, 0)); // Find the last hunk that starts before the target. let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer); cursor.seek(&target, Bias::Left); if cursor .item() .is_none_or(|hunk| hunk.buffer_range.start.cmp(&target, buffer).is_gt()) { cursor.prev(); } let unclipped_point = if let Some(hunk) = cursor.item() && hunk.buffer_range.start.cmp(&target, buffer).is_le() { // Found a hunk that starts before the target. let hunk_base_text_end = cursor.end().diff_base_byte_range.end; let unclipped_point = if target.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { // Target falls strictly between two hunks. let mut unclipped_point = hunk_base_text_end.to_point(self.base_text()); unclipped_point += Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); unclipped_point } else if bias == Bias::Right { hunk_base_text_end.to_point(self.base_text()) } else { hunk.diff_base_byte_range.start.to_point(self.base_text()) }; // Move the cursor so that at the next step we can clip with the start of the next hunk. cursor.next(); unclipped_point } else { // Target is before the added region for the first hunk. debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { target.cmp(&first_hunk.buffer_range.start, buffer).is_le() })); Point::new(row, 0) }; // If the target falls in the region between two hunks, we added an overshoot above. // There may be changes in the main buffer that are not reflected in the hunks, // so we need to ensure this overshoot keeps us in the corresponding base text region. let max_point = if let Some(next_hunk) = cursor.item() { next_hunk .diff_base_byte_range .start .to_point(self.base_text()) } else { self.base_text().max_point() }; unclipped_point.min(max_point).row } } impl BufferDiffInner<Entity<language::Buffer>> { /// Returns the new index text and new pending hunks. fn stage_or_unstage_hunks_impl( &mut self, unstaged_diff: &Self, stage: bool, hunks: &[DiffHunk], buffer: &text::BufferSnapshot, file_exists: bool, cx: &mut Context<BufferDiff>, ) -> Option<Rope> { let head_text = self .base_text_exists .then(|| self.base_text.read(cx).as_rope().clone()); let index_text = unstaged_diff .base_text_exists .then(|| unstaged_diff.base_text.read(cx).as_rope().clone()); // If the file doesn't exist in either HEAD or the index, then the // entire file must be either created or deleted in the index. let (index_text, head_text) = match (index_text, head_text) { (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text), (index_text, head_text) => { let (new_index_text, new_status) = if stage { log::debug!("stage all"); ( file_exists.then(|| buffer.as_rope().clone()), DiffHunkSecondaryStatus::SecondaryHunkRemovalPending, ) } else { log::debug!("unstage all"); ( head_text, DiffHunkSecondaryStatus::SecondaryHunkAdditionPending, ) }; let hunk = PendingHunk { buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()), diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()), buffer_version: buffer.version().clone(), new_status, }; self.pending_hunks = SumTree::from_item(hunk, buffer); return new_index_text; } }; let mut pending_hunks = SumTree::new(buffer); let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer); // first, merge new hunks into pending_hunks for DiffHunk { buffer_range, diff_base_byte_range, secondary_status, .. } in hunks.iter().cloned() { let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left); pending_hunks.append(preceding_pending_hunks, buffer); // Skip all overlapping or adjacent old pending hunks while old_pending_hunks.item().is_some_and(|old_hunk| { old_hunk .buffer_range .start .cmp(&buffer_range.end, buffer) .is_le() }) { old_pending_hunks.next(); } if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) || (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk) { continue; } pending_hunks.push( PendingHunk { buffer_range, diff_base_byte_range, buffer_version: buffer.version().clone(), new_status: if stage { DiffHunkSecondaryStatus::SecondaryHunkRemovalPending } else { DiffHunkSecondaryStatus::SecondaryHunkAdditionPending }, }, buffer, ); } // append the remainder pending_hunks.append(old_pending_hunks.suffix(), buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer); unstaged_hunk_cursor.next(); // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits let mut prev_unstaged_hunk_buffer_end = 0; let mut prev_unstaged_hunk_base_text_end = 0; let mut edits = Vec::<(Range<usize>, String)>::new(); let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable(); while let Some(PendingHunk { buffer_range, diff_base_byte_range, new_status, .. }) = pending_hunks_iter.next() { // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left);