1038 lines · 37.4 KB · Rust
RawDownload
use crate::{    conflict_view,    git_panel::{GitPanel, GitStatusEntry},    git_panel_settings::GitPanelSettings,};use anyhow::Result;use buffer_diff::BufferDiff;use collections::{HashMap, HashSet};use editor::{    EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::GoToHunk,    multibuffer_context_lines, scroll::Autoscroll,};use futures_lite::future::yield_now;use git::{repository::RepoPath, status::FileStatus};use gpui::{    App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render,    SharedString, Subscription, Task, WeakEntity,};use language::{Anchor, Buffer, BufferId, Capability, OffsetRangeExt};use multi_buffer::{MultiBuffer, PathKey};use project::{    ConflictSet, Project, ProjectPath,    git_store::{        Repository,        diff_buffer_list::{self, BranchDiffEvent, DiffBase},    },};use settings::{GitPanelGroupBy, GitPanelSortBy, Settings, SettingsStore};use std::{collections::BTreeMap, sync::Arc};use theme::ActiveTheme;use ui::{CommonAnimationExt as _, KeyBinding, prelude::*};use util::{ResultExt as _, rel_path::RelPath};use workspace::{    CloseActiveItem, ItemNavHistory, Workspace,    item::{Item, SaveOptions},};use ztracing::instrument; struct BufferSubscriptions {    _diff: Entity<BufferDiff>,    display_buffer: Entity<Buffer>,    _diff_subscription: Subscription,    _conflict_set: Option<Entity<ConflictSet>>,    _conflict_set_subscription: Option<Subscription>,} pub struct DiffMultibuffer {    multibuffer: Entity<MultiBuffer>,    branch_diff: Entity<diff_buffer_list::DiffBufferList>,    editor: Entity<SplittableEditor>,    buffer_subscriptions: HashMap<RepoPath, BufferSubscriptions>,    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    pending_scroll: Option<PathKey>,    review_comment_count: usize,    empty_label: SharedString,    _task: Task<Result<()>>,    _subscription: Subscription,} impl DiffMultibuffer {    pub(crate) fn new(        branch_diff: Entity<diff_buffer_list::DiffBufferList>,        multibuffer_capability: Capability,        empty_label: impl Into<SharedString>,        configure_editor: impl FnOnce(&mut SplittableEditor, &mut Context<SplittableEditor>) + 'static,        project: Entity<Project>,        workspace: Entity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();        let multibuffer = cx.new(|cx| {            let mut multibuffer = MultiBuffer::new(multibuffer_capability);            multibuffer.set_all_diff_hunks_expanded(cx);            multibuffer        });        let editor = cx.new(|cx| {            let mut diff_display_editor = SplittableEditor::new(                EditorSettings::get_global(cx).diff_view_style,                multibuffer.clone(),                project.clone(),                workspace.clone(),                window,                cx,            );            configure_editor(&mut diff_display_editor, cx);            diff_display_editor.rhs_editor().update(cx, |editor, cx| {                editor.set_show_diff_review_button(true, cx);            });            diff_display_editor        });        let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event);         let primary_editor = editor.read(cx).rhs_editor().clone();        let review_comment_subscription =            cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| {                if let EditorEvent::ReviewCommentsChanged { total_count } = event {                    this.review_comment_count = *total_count;                    cx.notify();                }            });         let branch_diff_subscription = cx.subscribe_in(            &branch_diff,            window,            move |this, _git_store, event, window, cx| match event {                BranchDiffEvent::FileListChanged => {                    this._task = window.spawn(cx, {                        let this = cx.weak_entity();                        async |cx| Self::refresh(this, cx).await                    })                }                BranchDiffEvent::DiffBaseChanged => {                    this.pending_scroll.take();                    this._task = window.spawn(cx, {                        let this = cx.weak_entity();                        async |cx| Self::refresh(this, cx).await                    })                }            },        );         let mut was_sort_by = GitPanelSettings::get_global(cx).sort_by;        let mut was_group_by = GitPanelSettings::get_global(cx).group_by;        let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;        let mut was_collapse_untracked_diff =            GitPanelSettings::get_global(cx).collapse_untracked_diff;        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {            let settings = GitPanelSettings::get_global(cx);            let sort_by = settings.sort_by;            let group_by = settings.group_by;            let tree_view = settings.tree_view;            let is_collapse_untracked_diff = settings.collapse_untracked_diff;            if sort_by != was_sort_by                || group_by != was_group_by                || tree_view != was_tree_view                || is_collapse_untracked_diff != was_collapse_untracked_diff            {                this._task = {                    window.spawn(cx, {                        let this = cx.weak_entity();                        async |cx| Self::refresh(this, cx).await                    })                }            }            was_sort_by = sort_by;            was_group_by = group_by;            was_tree_view = tree_view;            was_collapse_untracked_diff = is_collapse_untracked_diff;        })        .detach();         let task = window.spawn(cx, {            let this = cx.weak_entity();            async |cx| Self::refresh(this, cx).await        });         Self {            workspace: workspace.downgrade(),            branch_diff,            focus_handle,            editor,            multibuffer,            buffer_subscriptions: Default::default(),            pending_scroll: None,            review_comment_count: 0,            empty_label: empty_label.into(),            _task: task,            _subscription: Subscription::join(                branch_diff_subscription,                Subscription::join(editor_subscription, review_comment_subscription),            ),        }    }     pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {        self.branch_diff.read(cx).diff_base()    }     pub(crate) fn branch_diff(&self) -> &Entity<diff_buffer_list::DiffBufferList> {        &self.branch_diff    }     pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> {        self.branch_diff.read(cx).repo().cloned()    }     pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) {        self.branch_diff.update(cx, |branch_diff, cx| {            branch_diff.set_repo(repo, cx);        });    }     pub(crate) fn is_dirty(&self, cx: &App) -> bool {        self.multibuffer.read(cx).is_dirty(cx)    }     pub(crate) fn has_conflict(&self, cx: &App) -> bool {        self.multibuffer.read(cx).has_conflict(cx)    }     pub(crate) fn multibuffer(&self) -> &Entity<MultiBuffer> {        &self.multibuffer    }     pub(crate) fn move_to_entry(        &mut self,        entry: GitStatusEntry,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(git_repo) = self.branch_diff.read(cx).repo() else {            return;        };        let repo = git_repo.read(cx);        let path_key = project_diff_path_key(repo, &entry.repo_path, entry.status, cx);         self.move_to_path(path_key, window, cx)    }     pub(crate) fn move_to_project_path(        &mut self,        project_path: &ProjectPath,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(git_repo) = self.branch_diff.read(cx).repo() else {            return;        };        let Some(repo_path) = git_repo            .read(cx)            .project_path_to_repo_path(project_path, cx)        else {            return;        };        let status = git_repo            .read(cx)            .status_for_path(&repo_path)            .map(|entry| entry.status)            .unwrap_or(FileStatus::Untracked);        let path_key = project_diff_path_key(&git_repo.read(cx), &repo_path, status, cx);        self.move_to_path(path_key, window, cx)    }     pub(crate) fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            editor.rhs_editor().update(cx, |editor, cx| {                editor.change_selections(Default::default(), window, cx, |s| {                    s.select_ranges(vec![multi_buffer::Anchor::Min..multi_buffer::Anchor::Min]);                });            });        });    }     pub(crate) fn move_to_path(        &mut self,        path_key: PathKey,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {            self.editor.update(cx, |editor, cx| {                editor.rhs_editor().update(cx, |editor, cx| {                    editor.change_selections(                        SelectionEffects::scroll(Autoscroll::focused()),                        window,                        cx,                        |s| {                            s.select_ranges([position..position]);                        },                    )                })            });        } else {            self.pending_scroll = Some(path_key);        }    }     pub(crate) fn autoscroll(&self, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            editor.rhs_editor().update(cx, |editor, cx| {                editor.request_autoscroll(Autoscroll::fit(), cx);            })        })    }     pub(crate) fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {        self.multibuffer.read(cx).snapshot(cx).total_changed_lines()    }     /// Returns the total count of review comments across all hunks/files.    pub(crate) fn total_review_comment_count(&self) -> usize {        self.review_comment_count    }     /// Returns a reference to the splittable editor.    pub(crate) fn editor(&self) -> &Entity<SplittableEditor> {        &self.editor    }     pub(crate) fn selected_ranges(        &self,        cx: &App,    ) -> (bool, Vec<std::ops::Range<multi_buffer::Anchor>>) {        let editor = self.editor.read(cx).rhs_editor().read(cx);        let snapshot = self.multibuffer.read(cx).snapshot(cx);        let mut selection = true;        let mut ranges = editor            .selections            .disjoint_anchor_ranges()            .collect::<Vec<_>>();        if !ranges.iter().any(|range| range.start != range.end) {            selection = false;            let anchor = editor.selections.newest_anchor().head();            if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor)                && let Some(range) = snapshot                    .anchor_in_buffer(excerpt_range.context.start)                    .zip(snapshot.anchor_in_buffer(excerpt_range.context.end))                    .map(|(start, end)| start..end)            {                ranges = vec![range];            } else {                ranges = Vec::default();            };        }         (selection, ranges)    }     /// Ranges for a toolbar stage/unstage action: the selection, or the cursor    /// (a zero-width range that resolves to the single hunk under it) when    /// there is no selection. Unlike [`Self::selected_ranges`], this never    /// widens to the whole excerpt, so actions affect one hunk at a time.    fn hunk_action_ranges(&self, cx: &App) -> Vec<std::ops::Range<multi_buffer::Anchor>> {        self.editor            .read(cx)            .rhs_editor()            .read(cx)            .selections            .disjoint_anchor_ranges()            .collect()    }     pub(crate) fn stage_or_unstage_selected_hunks(        &mut self,        stage: bool,        move_to_next: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let editor = self.editor.read(cx).rhs_editor().clone();        let ranges = self.hunk_action_ranges(cx);        // Route through the editor's delegated stage path, the same path taken        // by the hunk buttons (on either side of a split) and the keyboard.        // For staging, dirty buffers are saved first, exactly as they are when        // staging from the uncommitted diff or a normal editor.        editor.update(cx, |editor, cx| {            editor.stage_or_unstage_diff_hunks(stage, ranges, window, cx);        });        if move_to_next {            editor                .focus_handle(cx)                .dispatch_action(&GoToHunk, window, cx);        }    }     pub(crate) fn restore_selected_hunks(        &mut self,        move_to_next: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let editor = self.editor.read(cx).rhs_editor().clone();        let ranges = self.hunk_action_ranges(cx);        editor.update(cx, |editor, cx| {            let snapshot = editor.buffer().read(cx).snapshot(cx);            let hunks: Vec<_> = editor.diff_hunks_in_ranges(&ranges, &snapshot).collect();            if !hunks.is_empty() {                editor.apply_restore(hunks, window, cx);            }        });        if move_to_next {            editor                .focus_handle(cx)                .dispatch_action(&GoToHunk, window, cx);        }    }     fn handle_editor_event(        &mut self,        editor: &Entity<SplittableEditor>,        event: &EditorEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match event {            EditorEvent::SelectionsChanged { local: true } => {                // Only follow the git panel selection from the view the user is                // actually interacting with. Background (non-active) diff views                // refresh on their own and must not hijack the panel selection.                if !editor.focus_handle(cx).contains_focused(window, cx) {                    return;                }                let Some(project_path) = self.active_project_path(cx) else {                    return;                };                self.workspace                    .update(cx, |workspace, cx| {                        if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {                            git_panel.update(cx, |git_panel, cx| {                                git_panel.select_entry_by_path(project_path, window, cx)                            })                        }                    })                    .ok();            }            EditorEvent::Saved => {                self._task =                    cx.spawn_in(window, async move |this, cx| Self::refresh(this, cx).await);            }             _ => {}        }        if editor.focus_handle(cx).contains_focused(window, cx)            && self.multibuffer.read(cx).is_empty()        {            self.focus_handle.focus(window, cx)        }    }     #[instrument(skip_all)]    fn register_buffer(        &mut self,        repo_path: RepoPath,        path_key: PathKey,        file_status: FileStatus,        display_buffer: Entity<Buffer>,        main_buffer: Entity<Buffer>,        diff: Entity<BufferDiff>,        conflict_set: Option<Entity<ConflictSet>>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Option<BufferId> {        let diff_subscription = cx.subscribe_in(&diff, window, {            let repo_path = repo_path.clone();            let path_key = path_key.clone();            let display_buffer = display_buffer.clone();            let main_buffer = main_buffer.clone();            let diff = diff.clone();            let conflict_set = conflict_set.clone();            move |this, _, event, window, cx| match event {                buffer_diff::BufferDiffEvent::DiffChanged(_) => {                    this.buffer_ranges_changed(                        repo_path.clone(),                        path_key.clone(),                        file_status,                        display_buffer.clone(),                        main_buffer.clone(),                        diff.clone(),                        conflict_set.clone(),                        window,                        cx,                    );                }                buffer_diff::BufferDiffEvent::BaseTextChanged => {}            }        });        let conflict_set_subscription = conflict_set.as_ref().map(|conflict_set| {            cx.subscribe_in(conflict_set, window, {                let repo_path = repo_path.clone();                let path_key = path_key.clone();                let display_buffer = display_buffer.clone();                let main_buffer = main_buffer.clone();                let diff = diff.clone();                let conflict_set = Some(conflict_set.clone());                move |this, _, _, window, cx| {                    this.buffer_ranges_changed(                        repo_path.clone(),                        path_key.clone(),                        file_status,                        display_buffer.clone(),                        main_buffer.clone(),                        diff.clone(),                        conflict_set.clone(),                        window,                        cx,                    )                }            })        });        self.buffer_subscriptions.insert(            repo_path,            BufferSubscriptions {                _diff: diff.clone(),                display_buffer: display_buffer.clone(),                _diff_subscription: diff_subscription,                _conflict_set: conflict_set.clone(),                _conflict_set_subscription: conflict_set_subscription,            },

Lines 1–500 of 1038.