2810 lines · 95.3 KB · Rust
RawDownload
use crate::{    conflict_view::ConflictAddon,    git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},    git_panel_settings::GitPanelSettings,    resolve_active_repository,};use agent_settings::AgentSettings;use anyhow::{Context as _, Result, anyhow};use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};use collections::{HashMap, HashSet};use editor::{    Addon, Editor, EditorEvent, EditorSettings, SelectionEffects, SplittableEditor,    actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent},    multibuffer_context_lines,    scroll::Autoscroll,};use git::repository::DiffType; use git::{    Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::RepoPath,    status::FileStatus,};use gpui::{    Action, AnyElement, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,    FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,};use language::{Anchor, Buffer, BufferId, 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::{DiffStat, Divider, 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 zed_actions::agent::ReviewBranchDiff;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,        /// Opens a new agent thread with the branch diff for review.        ReviewDiff,        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>,    review_comment_count: usize,    _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();        let workspace_weak = workspace.downgrade();        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(workspace_weak, window, cx);    }     fn review_diff(&mut self, _: &ReviewDiff, window: &mut Window, cx: &mut Context<Self>) {        let diff_base = self.diff_base(cx).clone();        let DiffBase::Merge { base_ref } = diff_base else {            return;        };         let Some(repo) = self.branch_diff.read(cx).repo().cloned() else {            return;        };         let diff_receiver = repo.update(cx, |repo, cx| {            repo.diff(                DiffType::MergeBase {                    base_ref: base_ref.clone(),                },                cx,            )        });         let workspace = self.workspace.clone();         window            .spawn(cx, {                let workspace = workspace.clone();                async move |cx| {                    let diff_text = diff_receiver.await??;                     if let Some(workspace) = workspace.upgrade() {                        workspace.update_in(cx, |_workspace, window, cx| {                            window.dispatch_action(                                ReviewBranchDiff {                                    diff_text: diff_text.into(),                                    base_ref: base_ref.to_string().into(),                                }                                .boxed_clone(),                                cx,                            );                        })?;                    }                     anyhow::Ok(())                }            })            .detach_and_notify_err(workspace, 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 intended_repo = resolve_active_repository(workspace, cx);         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(intended) = &intended_repo {            let needs_switch = project_diff                .read(cx)                .branch_diff                .read(cx)                .repo()                .map_or(true, |current| current.read(cx).id != intended.read(cx).id);            if needs_switch {                project_diff.update(cx, |project_diff, cx| {                    project_diff.branch_diff.update(cx, |branch_diff, cx| {                        branch_diff.set_repo(Some(intended.clone()), cx);                    });                });            }        }         if let Some(entry) = entry {            project_diff.update(cx, |project_diff, cx| {                project_diff.move_to_entry(entry, window, cx);            })        }    }     pub fn deploy_at_project_path(        workspace: &mut Workspace,        project_path: ProjectPath,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        telemetry::event!("Git Diff Opened", source = "Agent Panel");        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        };        project_diff.update(cx, |project_diff, cx| {            project_diff.move_to_project_path(&project_path, window, cx);        });    }     pub 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);            })        })    }     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(                EditorSettings::get_global(cx).diff_view_style,                multibuffer.clone(),                project.clone(),                workspace.clone(),                window,                cx,            );            match branch_diff.read(cx).diff_base() {                DiffBase::Head => {}                DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls(                    Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),                    cx,                ),            }            diff_display_editor.rhs_editor().update(cx, |editor, cx| {                editor.disable_diagnostics(cx);                editor.set_show_diff_review_button(true, 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();                    }                }            });            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, 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,            review_comment_count: 0,            _task: task,            _subscription: Subscription::join(                branch_diff_subscription,                Subscription::join(editor_subscription, review_comment_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 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 sort_prefix = sort_prefix(&git_repo.read(cx), &repo_path, status, cx);        let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.as_ref().clone());        self.move_to_path(path_key, window, cx)    }

Lines 1–500 of 2810.