2481 lines · 80.8 KB · Rust
RawDownload
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 2481.