2365 lines · 78.3 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,    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, AnyView, 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 std::any::{Any, TypeId};use std::ops::Range;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::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},    notifications::NotifyTaskExt,    searchable::SearchableItemHandle,}; 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    ]); pub struct ProjectDiff {    project: Entity<Project>,    multibuffer: Entity<MultiBuffer>,    branch_diff: Entity<branch_diff::BranchDiff>,    editor: Entity<Editor>,    buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>,    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    pending_scroll: Option<PathKey>,    _task: Task<Result<()>>,    _subscription: Subscription,} 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 {            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.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());        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(|_| MultiBuffer::new(Capability::ReadWrite));         let editor = cx.new(|cx| {            let mut diff_display_editor =                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);            diff_display_editor.disable_diagnostics(cx);            diff_display_editor.set_expand_all_diff_hunks(cx);             match branch_diff.read(cx).diff_base() {                DiffBase::Head => {                    diff_display_editor.register_addon(GitPanelAddon {                        workspace: workspace.downgrade(),                    });                }                DiffBase::Merge { .. } => {                    diff_display_editor.register_addon(BranchDiffAddon {                        branch_diff: branch_diff.clone(),                    });                    diff_display_editor.start_temporary_diff_override();                    diff_display_editor.set_render_diff_hunk_controls(                        Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),                        cx,                    );                    //                }            }            diff_display_editor        });        window.defer(cx, {            let workspace = workspace.clone();            let editor = editor.clone();            move |window, cx| {                workspace.update(cx, |workspace, cx| {                    editor.update(cx, |editor, cx| {                        editor.added_to_workspace(workspace, window, cx);                    })                });            }        });        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, 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, 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, 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.file_status(repo), cx);        let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0);         self.move_to_path(path_key, window, cx)    }     pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {        let editor = self.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_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.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);        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, buffer, range)) = self.editor.read(cx).active_excerpt(cx) {                ranges = vec![multi_buffer::Anchor::range_in_buffer(                    excerpt_id,                    buffer.read(cx).remote_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.secondary_status {                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<Editor>,        event: &EditorEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if let EditorEvent::SelectionsChanged { local: true } = event {            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();        }        if editor.focus_handle(cx).contains_focused(window, cx)            && self.multibuffer.read(cx).is_empty()        {            self.focus_handle.focus(window)        }    }     fn register_buffer(        &mut self,        path_key: PathKey,        file_status: FileStatus,        buffer: Entity<Buffer>,        diff: Entity<BufferDiff>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| {            this._task = window.spawn(cx, {                let this = cx.weak_entity();                async |cx| Self::refresh(this, cx).await            })        });        self.buffer_diff_subscriptions            .insert(path_key.path.clone(), (diff.clone(), subscription));         let conflict_addon = self            .editor            .read(cx)            .addon::<ConflictAddon>()            .expect("project diff editor should have a conflict addon");         let snapshot = buffer.read(cx).snapshot();        let diff_read = diff.read(cx);        let diff_hunk_ranges = diff_read            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)            .map(|diff_hunk| diff_hunk.buffer_range);        let conflicts = conflict_addon            .conflict_set(snapshot.remote_id())            .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts)            .unwrap_or_default();        let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());         let excerpt_ranges =            merge_anchor_ranges(diff_hunk_ranges.into_iter(), conflicts, &snapshot)

Lines 1–500 of 2365.