use futures::channel::oneshot;use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};use language::{ BufferRow, 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, LazyLock},};use sum_tree::SumTree;use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _};use util::ResultExt; pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner, // diff of the index vs head secondary_diff: Option<Entity<BufferDiff>>,} #[derive(Clone, Debug)]pub struct BufferDiffSnapshot { inner: BufferDiffInner, secondary_diff: Option<Box<BufferDiffSnapshot>>,} #[derive(Clone)]struct BufferDiffInner { hunks: SumTree<InternalDiffHunk>, // Used for making staging mo pending_hunks: SumTree<PendingHunk>, base_text: language::BufferSnapshot, 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 { 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 { pub fn buffer_diff_id(&self) -> BufferId { self.inner.base_text.remote_id() } fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot { BufferDiffSnapshot { inner: BufferDiffInner { base_text: language::Buffer::build_empty_snapshot(cx), hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text_exists: false, }, secondary_diff: None, } } fn unchanged( buffer: &text::BufferSnapshot, base_text: language::BufferSnapshot, ) -> BufferDiffSnapshot { debug_assert_eq!(buffer.text(), base_text.text()); BufferDiffSnapshot { inner: BufferDiffInner { base_text, hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text_exists: false, }, secondary_diff: None, } } fn new_with_base_text( buffer: text::BufferSnapshot, base_text: Option<Arc<String>>, language: Option<Arc<Language>>, language_registry: Option<Arc<LanguageRegistry>>, cx: &mut App, ) -> impl Future<Output = Self> + use<> { let base_text_pair; let base_text_exists; let base_text_snapshot; let diff_options = build_diff_options( None, language.as_ref().map(|l| l.name()), language.as_ref().map(|l| l.default_scope()), cx, ); if let Some(text) = &base_text { let base_text_rope = Rope::from(text.as_str()); base_text_pair = Some((text.clone(), base_text_rope.clone())); let snapshot = language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); base_text_snapshot = cx.background_spawn(snapshot); base_text_exists = true; } else { base_text_pair = None; base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx)); base_text_exists = false; }; let hunks = cx .background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, { let buffer = buffer.clone(); async move { compute_hunks(base_text_pair, buffer, diff_options) } }); async move { let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); Self { inner: BufferDiffInner { base_text, hunks, base_text_exists, pending_hunks: SumTree::new(&buffer), }, secondary_diff: None, } } } pub fn new_with_base_buffer( buffer: text::BufferSnapshot, base_text: Option<Arc<String>>, base_text_snapshot: language::BufferSnapshot, cx: &App, ) -> impl Future<Output = Self> + use<> { let diff_options = build_diff_options( base_text_snapshot.file(), base_text_snapshot.language().map(|l| l.name()), base_text_snapshot.language().map(|l| l.default_scope()), cx, ); let base_text_exists = base_text.is_some(); let base_text_pair = base_text.map(|text| { debug_assert_eq!(&*text, &base_text_snapshot.text()); (text, base_text_snapshot.as_rope().clone()) }); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { inner: BufferDiffInner { base_text: base_text_snapshot, pending_hunks: SumTree::new(&buffer), hunks: compute_hunks(base_text_pair, buffer, diff_options), base_text_exists, }, secondary_diff: None, } }) } #[cfg(test)] fn new_sync( buffer: text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { cx.executor().block(cx.update(|cx| { Self::new_with_base_text(buffer, Some(Arc::new(diff_base)), None, None, cx) })) } pub fn is_empty(&self) -> bool { self.inner.hunks.is_empty() } 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> { self.inner.hunks_intersecting_range_rev(range, buffer) } pub fn base_text(&self) -> &language::BufferSnapshot { &self.inner.base_text } pub fn base_texts_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_empty) = (left.remote_id(), left.is_empty()); let (new_id, new_empty) = (right.remote_id(), right.is_empty()); new_id == old_id || (new_empty && old_empty) } pub fn row_to_base_text_row(&self, row: BufferRow, buffer: &text::BufferSnapshot) -> u32 { // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start // Find the last hunk that starts before this position. let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer); let position = buffer.anchor_before(Point::new(row, 0)); cursor.seek(&position, Bias::Left); if cursor .item() .is_none_or(|hunk| hunk.buffer_range.start.cmp(&position, buffer).is_gt()) { cursor.prev(); } let unclipped_point = if let Some(hunk) = cursor.item() && hunk.buffer_range.start.cmp(&position, buffer).is_le() { let mut unclipped_point = cursor .end() .diff_base_byte_range .end .to_point(self.base_text()); if position.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { unclipped_point += Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); } // Move the cursor so that at the next step we can clip with the start of the next hunk. cursor.next(); unclipped_point } else { // Position is before the added region for the first hunk. debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { position.cmp(&first_hunk.buffer_range.start, buffer).is_le() })); Point::new(row, 0) }; 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 { /// 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, ) -> Option<Rope> { let head_text = self .base_text_exists .then(|| self.base_text.as_rope().clone()); let index_text = unstaged_diff .base_text_exists .then(|| unstaged_diff.base_text.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(); Lines 1–500 of 2479. Load more
use futures::channel::oneshot; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel}; use language::{ BufferRow, 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, LazyLock}, }; use sum_tree::SumTree; use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _}; use util::ResultExt; pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new); pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner, // diff of the index vs head secondary_diff: Option<Entity<BufferDiff>>, } #[derive(Clone, Debug)] pub struct BufferDiffSnapshot { inner: BufferDiffInner, secondary_diff: Option<Box<BufferDiffSnapshot>>, } #[derive(Clone)] struct BufferDiffInner { hunks: SumTree<InternalDiffHunk>, // Used for making staging mo pending_hunks: SumTree<PendingHunk>, base_text: language::BufferSnapshot, 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 { 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 { pub fn buffer_diff_id(&self) -> BufferId { self.inner.base_text.remote_id() } fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot { BufferDiffSnapshot { inner: BufferDiffInner { base_text: language::Buffer::build_empty_snapshot(cx), hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text_exists: false, }, secondary_diff: None, } } fn unchanged( buffer: &text::BufferSnapshot, base_text: language::BufferSnapshot, ) -> BufferDiffSnapshot { debug_assert_eq!(buffer.text(), base_text.text()); BufferDiffSnapshot { inner: BufferDiffInner { base_text, hunks: SumTree::new(buffer), pending_hunks: SumTree::new(buffer), base_text_exists: false, }, secondary_diff: None, } } fn new_with_base_text( buffer: text::BufferSnapshot, base_text: Option<Arc<String>>, language: Option<Arc<Language>>, language_registry: Option<Arc<LanguageRegistry>>, cx: &mut App, ) -> impl Future<Output = Self> + use<> { let base_text_pair; let base_text_exists; let base_text_snapshot; let diff_options = build_diff_options( None, language.as_ref().map(|l| l.name()), language.as_ref().map(|l| l.default_scope()), cx, ); if let Some(text) = &base_text { let base_text_rope = Rope::from(text.as_str()); base_text_pair = Some((text.clone(), base_text_rope.clone())); let snapshot = language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); base_text_snapshot = cx.background_spawn(snapshot); base_text_exists = true; } else { base_text_pair = None; base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx)); base_text_exists = false; }; let hunks = cx .background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, { let buffer = buffer.clone(); async move { compute_hunks(base_text_pair, buffer, diff_options) } }); async move { let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); Self { inner: BufferDiffInner { base_text, hunks, base_text_exists, pending_hunks: SumTree::new(&buffer), }, secondary_diff: None, } } } pub fn new_with_base_buffer( buffer: text::BufferSnapshot, base_text: Option<Arc<String>>, base_text_snapshot: language::BufferSnapshot, cx: &App, ) -> impl Future<Output = Self> + use<> { let diff_options = build_diff_options( base_text_snapshot.file(), base_text_snapshot.language().map(|l| l.name()), base_text_snapshot.language().map(|l| l.default_scope()), cx, ); let base_text_exists = base_text.is_some(); let base_text_pair = base_text.map(|text| { debug_assert_eq!(&*text, &base_text_snapshot.text()); (text, base_text_snapshot.as_rope().clone()) }); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { inner: BufferDiffInner { base_text: base_text_snapshot, pending_hunks: SumTree::new(&buffer), hunks: compute_hunks(base_text_pair, buffer, diff_options), base_text_exists, }, secondary_diff: None, } }) } #[cfg(test)] fn new_sync( buffer: text::BufferSnapshot, diff_base: String, cx: &mut gpui::TestAppContext, ) -> BufferDiffSnapshot { cx.executor().block(cx.update(|cx| { Self::new_with_base_text(buffer, Some(Arc::new(diff_base)), None, None, cx) })) } pub fn is_empty(&self) -> bool { self.inner.hunks.is_empty() } 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> { self.inner.hunks_intersecting_range_rev(range, buffer) } pub fn base_text(&self) -> &language::BufferSnapshot { &self.inner.base_text } pub fn base_texts_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_empty) = (left.remote_id(), left.is_empty()); let (new_id, new_empty) = (right.remote_id(), right.is_empty()); new_id == old_id || (new_empty && old_empty) } pub fn row_to_base_text_row(&self, row: BufferRow, buffer: &text::BufferSnapshot) -> u32 { // TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start // Find the last hunk that starts before this position. let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer); let position = buffer.anchor_before(Point::new(row, 0)); cursor.seek(&position, Bias::Left); if cursor .item() .is_none_or(|hunk| hunk.buffer_range.start.cmp(&position, buffer).is_gt()) { cursor.prev(); } let unclipped_point = if let Some(hunk) = cursor.item() && hunk.buffer_range.start.cmp(&position, buffer).is_le() { let mut unclipped_point = cursor .end() .diff_base_byte_range .end .to_point(self.base_text()); if position.cmp(&cursor.end().buffer_range.end, buffer).is_ge() { unclipped_point += Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer); } // Move the cursor so that at the next step we can clip with the start of the next hunk. cursor.next(); unclipped_point } else { // Position is before the added region for the first hunk. debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| { position.cmp(&first_hunk.buffer_range.start, buffer).is_le() })); Point::new(row, 0) }; 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 { /// 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, ) -> Option<Rope> { let head_text = self .base_text_exists .then(|| self.base_text.as_rope().clone()); let index_text = unstaged_diff .base_text_exists .then(|| unstaged_diff.base_text.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();