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::{Language, LanguageRegistry};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 _};use util::ResultExt; pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new); pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner, 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>, 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)]pub enum DiffHunkSecondaryStatus { HasSecondaryHunk, OverlapsWithSecondaryHunk, NoSecondaryHunk, SecondaryHunkAdditionPending, 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,} /// 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>,} #[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, Default, Clone)]pub struct DiffHunkSummary { buffer_range: Range<Anchor>,} impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_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(), } }} impl sum_tree::Summary for DiffHunkSummary { type Context = text::BufferSnapshot; fn zero(_cx: &Self::Context) -> Self { Default::default() } 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); }} 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) .finish() }} impl BufferDiffSnapshot { 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 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; 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.clone(), language_registry.clone(), 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) } }); 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 base_text_exists = base_text.is_some(); let base_text_pair = base_text.map(|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), 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) }} 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..Anchor::MAX, 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, buffer); 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(buffer); } 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), buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer); unstaged_hunk_cursor.next(buffer); // 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, buffer); if let Some(unstaged_hunk) = skipped_unstaged.last() { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer); } // Find where this hunk is in the index if it doesn't overlap let mut buffer_offset_range = buffer_range.to_offset(buffer); let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end; let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot; loop { // Merge this hunk with any overlapping unstaged hunks. if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() { let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer); if unstaged_hunk_offset_range.start <= buffer_offset_range.end { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end; index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start); buffer_offset_range.start = buffer_offset_range .start .min(unstaged_hunk_offset_range.start); buffer_offset_range.end = buffer_offset_range.end.max(unstaged_hunk_offset_range.end); unstaged_hunk_cursor.next(buffer); continue; } } // If any unstaged hunks were merged, then subsequent pending hunks may // now overlap this hunk. Merge them. if let Some(next_pending_hunk) = pending_hunks_iter.peek() { let next_pending_hunk_offset_range = next_pending_hunk.buffer_range.to_offset(buffer); if next_pending_hunk_offset_range.start <= buffer_offset_range.end { buffer_offset_range.end = next_pending_hunk_offset_range.end; pending_hunks_iter.next(); continue; } } break; } let end_overshoot = buffer_offset_range .end .saturating_sub(prev_unstaged_hunk_buffer_end); let index_end = prev_unstaged_hunk_base_text_end + end_overshoot; let index_byte_range = index_start..index_end; let replacement_text = match new_status { DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { log::debug!("staging hunk {:?}", buffer_offset_range); buffer .text_for_range(buffer_offset_range) .collect::<String>() } DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { log::debug!("unstaging hunk {:?}", buffer_offset_range); head_text .chunks_in_range(diff_base_byte_range.clone()) .collect::<String>() } _ => { debug_assert!(false); continue; } }; edits.push((index_byte_range, replacement_text)); } drop(pending_hunks_iter); drop(old_pending_hunks); self.pending_hunks = pending_hunks; #[cfg(debug_assertions)] // invariants: non-overlapping and sorted { for window in edits.windows(2) { let (range_a, range_b) = (&window[0].0, &window[1].0); debug_assert!(range_a.end < range_b.start); } } let mut new_index_text = Rope::new(); let mut index_cursor = index_text.cursor(0); for (old_range, replacement_text) in edits { new_index_text.append(index_cursor.slice(old_range.start)); index_cursor.seek_forward(old_range.end); new_index_text.push(&replacement_text); } new_index_text.append(index_cursor.suffix()); Some(new_index_text) } fn hunks_intersecting_range<'a>( &'a self,Lines 1–500 of 2185. 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::{Language, LanguageRegistry}; 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 _}; use util::ResultExt; pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new); pub struct BufferDiff { pub buffer_id: BufferId, inner: BufferDiffInner, 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>, 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)] pub enum DiffHunkSecondaryStatus { HasSecondaryHunk, OverlapsWithSecondaryHunk, NoSecondaryHunk, SecondaryHunkAdditionPending, 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, } /// 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>, } #[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, Default, Clone)] pub struct DiffHunkSummary { buffer_range: Range<Anchor>, } impl sum_tree::Item for InternalDiffHunk { type Summary = DiffHunkSummary; fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_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(), } } } impl sum_tree::Summary for DiffHunkSummary { type Context = text::BufferSnapshot; fn zero(_cx: &Self::Context) -> Self { Default::default() } 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); } } 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) .finish() } } impl BufferDiffSnapshot { 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 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; 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.clone(), language_registry.clone(), 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) } }); 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 base_text_exists = base_text.is_some(); let base_text_pair = base_text.map(|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), 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) } } 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..Anchor::MAX, 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, buffer); 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(buffer); } 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), buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer); unstaged_hunk_cursor.next(buffer); // 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, buffer); if let Some(unstaged_hunk) = skipped_unstaged.last() { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer); } // Find where this hunk is in the index if it doesn't overlap let mut buffer_offset_range = buffer_range.to_offset(buffer); let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end; let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot; loop { // Merge this hunk with any overlapping unstaged hunks. if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() { let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer); if unstaged_hunk_offset_range.start <= buffer_offset_range.end { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end; index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start); buffer_offset_range.start = buffer_offset_range .start .min(unstaged_hunk_offset_range.start); buffer_offset_range.end = buffer_offset_range.end.max(unstaged_hunk_offset_range.end); unstaged_hunk_cursor.next(buffer); continue; } } // If any unstaged hunks were merged, then subsequent pending hunks may // now overlap this hunk. Merge them. if let Some(next_pending_hunk) = pending_hunks_iter.peek() { let next_pending_hunk_offset_range = next_pending_hunk.buffer_range.to_offset(buffer); if next_pending_hunk_offset_range.start <= buffer_offset_range.end { buffer_offset_range.end = next_pending_hunk_offset_range.end; pending_hunks_iter.next(); continue; } } break; } let end_overshoot = buffer_offset_range .end .saturating_sub(prev_unstaged_hunk_buffer_end); let index_end = prev_unstaged_hunk_base_text_end + end_overshoot; let index_byte_range = index_start..index_end; let replacement_text = match new_status { DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { log::debug!("staging hunk {:?}", buffer_offset_range); buffer .text_for_range(buffer_offset_range) .collect::<String>() } DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { log::debug!("unstaging hunk {:?}", buffer_offset_range); head_text .chunks_in_range(diff_base_byte_range.clone()) .collect::<String>() } _ => { debug_assert!(false); continue; } }; edits.push((index_byte_range, replacement_text)); } drop(pending_hunks_iter); drop(old_pending_hunks); self.pending_hunks = pending_hunks; #[cfg(debug_assertions)] // invariants: non-overlapping and sorted { for window in edits.windows(2) { let (range_a, range_b) = (&window[0].0, &window[1].0); debug_assert!(range_a.end < range_b.start); } } let mut new_index_text = Rope::new(); let mut index_cursor = index_text.cursor(0); for (old_range, replacement_text) in edits { new_index_text.append(index_cursor.slice(old_range.start)); index_cursor.seek_forward(old_range.end); new_index_text.push(&replacement_text); } new_index_text.append(index_cursor.suffix()); Some(new_index_text) } fn hunks_intersecting_range<'a>( &'a self,