2440 lines · 81.6 KB · Rust
RawDownload
use crate::{    conflict_view::ConflictAddon,    git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},    git_panel_settings::GitPanelSettings,    remote_button::{render_publish_button, render_push_button},};use anyhow::{Context as _, Result, anyhow};use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};use collections::{HashMap, HashSet};use editor::{    Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor,    actions::{GoToHunk, GoToPreviousHunk},    multibuffer_context_lines,    scroll::Autoscroll,};use git::{    Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,    repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},    status::FileStatus,};use gpui::{    Action, AnyElement, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,    FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,};use language::{Anchor, Buffer, Capability, OffsetRangeExt};use multi_buffer::{MultiBuffer, PathKey};use project::{    Project, ProjectPath,    git_store::{        Repository,        branch_diff::{self, BranchDiffEvent, DiffBase},    },};use settings::{Settings, SettingsStore};use smol::future::yield_now;use std::any::{Any, TypeId};use std::sync::Arc;use theme::ActiveTheme;use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};use util::{ResultExt as _, rel_path::RelPath};use workspace::{    CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,    ToolbarItemView, Workspace,    item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},    notifications::NotifyTaskExt,    searchable::SearchableItemHandle,};use ztracing::instrument; actions!(    git,    [        /// Shows the diff between the working directory and the index.        Diff,        /// Adds files to the git staging area.        Add,        /// Shows the diff between the working directory and your default        /// branch (typically main or master).        BranchDiff,        LeaderAndFollower,    ]); pub struct ProjectDiff {    project: Entity<Project>,    multibuffer: Entity<MultiBuffer>,    branch_diff: Entity<branch_diff::BranchDiff>,    editor: Entity<SplittableEditor>,    buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>,    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    pending_scroll: Option<PathKey>,    _task: Task<Result<()>>,    _subscription: Subscription,} #[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum RefreshReason {    DiffChanged,    StatusesChanged,    EditorSaved,} const CONFLICT_SORT_PREFIX: u64 = 1;const TRACKED_SORT_PREFIX: u64 = 2;const NEW_SORT_PREFIX: u64 = 3; impl ProjectDiff {    pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {        workspace.register_action(Self::deploy);        workspace.register_action(Self::deploy_branch_diff);        workspace.register_action(|workspace, _: &Add, window, cx| {            Self::deploy(workspace, &Diff, window, cx);        });        workspace::register_serializable_item::<ProjectDiff>(cx);    }     fn deploy(        workspace: &mut Workspace,        _: &Diff,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        Self::deploy_at(workspace, None, window, cx)    }     fn deploy_branch_diff(        workspace: &mut Workspace,        _: &BranchDiff,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        telemetry::event!("Git Branch Diff Opened");        let project = workspace.project().clone();         let existing = workspace            .items_of_type::<Self>(cx)            .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Merge { .. }));        if let Some(existing) = existing {            workspace.activate_item(&existing, true, true, window, cx);            return;        }        let workspace = cx.entity();        window            .spawn(cx, async move |cx| {                let this = cx                    .update(|window, cx| {                        Self::new_with_default_branch(project, workspace.clone(), window, cx)                    })?                    .await?;                workspace                    .update_in(cx, |workspace, window, cx| {                        workspace.add_item_to_active_pane(Box::new(this), None, true, window, cx);                    })                    .ok();                anyhow::Ok(())            })            .detach_and_notify_err(window, cx);    }     pub fn deploy_at(        workspace: &mut Workspace,        entry: Option<GitStatusEntry>,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        telemetry::event!(            "Git Diff Opened",            source = if entry.is_some() {                "Git Panel"            } else {                "Action"            }        );        let existing = workspace            .items_of_type::<Self>(cx)            .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head));        let project_diff = if let Some(existing) = existing {            existing.update(cx, |project_diff, cx| {                project_diff.move_to_beginning(window, cx);            });             workspace.activate_item(&existing, true, true, window, cx);            existing        } else {            let workspace_handle = cx.entity();            let project_diff =                cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));            workspace.add_item_to_active_pane(                Box::new(project_diff.clone()),                None,                true,                window,                cx,            );            project_diff        };        if let Some(entry) = entry {            project_diff.update(cx, |project_diff, cx| {                project_diff.move_to_entry(entry, window, cx);            })        }    }     pub fn autoscroll(&self, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            editor.primary_editor().update(cx, |editor, cx| {                editor.request_autoscroll(Autoscroll::fit(), cx);            })        })    }     fn new_with_default_branch(        project: Entity<Project>,        workspace: Entity<Workspace>,        window: &mut Window,        cx: &mut App,    ) -> Task<Result<Entity<Self>>> {        let Some(repo) = project.read(cx).git_store().read(cx).active_repository() else {            return Task::ready(Err(anyhow!("No active repository")));        };        let main_branch = repo.update(cx, |repo, _| repo.default_branch(true));        window.spawn(cx, async move |cx| {            let main_branch = main_branch                .await??                .context("Could not determine default branch")?;             let branch_diff = cx.new_window_entity(|window, cx| {                branch_diff::BranchDiff::new(                    DiffBase::Merge {                        base_ref: main_branch,                    },                    project.clone(),                    window,                    cx,                )            })?;            cx.new_window_entity(|window, cx| {                Self::new_impl(branch_diff, project, workspace, window, cx)            })        })    }     fn new(        project: Entity<Project>,        workspace: Entity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let branch_diff =            cx.new(|cx| branch_diff::BranchDiff::new(DiffBase::Head, project.clone(), window, cx));        Self::new_impl(branch_diff, project, workspace, window, cx)    }     fn new_impl(        branch_diff: Entity<branch_diff::BranchDiff>,        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(Capability::ReadWrite);            multibuffer.set_all_diff_hunks_expanded(cx);            multibuffer        });         let editor = cx.new(|cx| {            let diff_display_editor = SplittableEditor::new_unsplit(                multibuffer.clone(),                project.clone(),                workspace.clone(),                window,                cx,            );            diff_display_editor                .primary_editor()                .update(cx, |editor, cx| {                    editor.disable_diagnostics(cx);                     match branch_diff.read(cx).diff_base() {                        DiffBase::Head => {                            editor.register_addon(GitPanelAddon {                                workspace: workspace.downgrade(),                            });                        }                        DiffBase::Merge { .. } => {                            editor.register_addon(BranchDiffAddon {                                branch_diff: branch_diff.clone(),                            });                            editor.start_temporary_diff_override();                            editor.set_render_diff_hunk_controls(                                Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),                                cx,                            );                        }                    }                });            diff_display_editor        });        cx.subscribe_in(&editor, window, Self::handle_editor_event)            .detach();         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, RefreshReason::StatusesChanged, cx).await                    })                }            },        );         let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;        let mut was_collapse_untracked_diff =            GitPanelSettings::get_global(cx).collapse_untracked_diff;        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {            let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;            let is_collapse_untracked_diff =                GitPanelSettings::get_global(cx).collapse_untracked_diff;            if is_sort_by_path != was_sort_by_path                || is_collapse_untracked_diff != was_collapse_untracked_diff            {                this._task = {                    window.spawn(cx, {                        let this = cx.weak_entity();                        async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await                    })                }            }            was_sort_by_path = is_sort_by_path;            was_collapse_untracked_diff = is_collapse_untracked_diff;        })        .detach();         let task = window.spawn(cx, {            let this = cx.weak_entity();            async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await        });         Self {            project,            workspace: workspace.downgrade(),            branch_diff,            focus_handle,            editor,            multibuffer,            buffer_diff_subscriptions: Default::default(),            pending_scroll: None,            _task: task,            _subscription: branch_diff_subscription,        }    }     pub fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {        self.branch_diff.read(cx).diff_base()    }     pub 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 sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);        let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone());         self.move_to_path(path_key, window, cx)    }     pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {        let editor = self.editor.read(cx).last_selected_editor().read(cx);        let position = editor.selections.newest_anchor().head();        let multi_buffer = editor.buffer().read(cx);        let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?;         let file = buffer.read(cx).file()?;        Some(ProjectPath {            worktree_id: file.worktree_id(cx),            path: file.path().clone(),        })    }     fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            editor.primary_editor().update(cx, |editor, cx| {                editor.move_to_beginning(&Default::default(), window, cx);            });        });    }     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.primary_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);        }    }     fn button_states(&self, cx: &App) -> ButtonStates {        let editor = self.editor.read(cx).primary_editor().read(cx);        let snapshot = self.multibuffer.read(cx).snapshot(cx);        let prev_next = snapshot.diff_hunks().nth(1).is_some();        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;            if let Some((excerpt_id, _, range)) = self                .editor                .read(cx)                .primary_editor()                .read(cx)                .active_excerpt(cx)            {                ranges = vec![multi_buffer::Anchor::range_in_buffer(excerpt_id, range)];            } else {                ranges = Vec::default();            }        }        let mut has_staged_hunks = false;        let mut has_unstaged_hunks = false;        for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) {            match hunk.status.secondary {                DiffHunkSecondaryStatus::HasSecondaryHunk                | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {                    has_unstaged_hunks = true;                }                DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => {                    has_staged_hunks = true;                    has_unstaged_hunks = true;                }                DiffHunkSecondaryStatus::NoSecondaryHunk                | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {                    has_staged_hunks = true;                }            }        }        let mut stage_all = false;        let mut unstage_all = false;        self.workspace            .read_with(cx, |workspace, cx| {                if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {                    let git_panel = git_panel.read(cx);                    stage_all = git_panel.can_stage_all();                    unstage_all = git_panel.can_unstage_all();                }            })            .ok();         ButtonStates {            stage: has_unstaged_hunks,            unstage: has_staged_hunks,            prev_next,            selection,            stage_all,            unstage_all,        }    }     fn handle_editor_event(        &mut self,        editor: &Entity<SplittableEditor>,        event: &EditorEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match event {            EditorEvent::SelectionsChanged { local: true } => {                let Some(project_path) = self.active_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, RefreshReason::EditorSaved, 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(

Lines 1–500 of 2440.