1283 lines · 44.5 KB · Rust
RawDownload
use crate::{    branch_picker,    diff_multibuffer::DiffMultibuffer,    project_diff::{        self, CompareWithBranch, DeployBranchDiff, ProjectDiff, ReviewDiff,        render_send_review_to_agent_button,    },};use agent_settings::AgentSettings;use anyhow::{Context as _, Result, anyhow};use editor::{    Addon, Editor, EditorEvent, RestoreOnlyDiffHunkDelegate, SplittableEditor,    actions::SendReviewToAgent,};use git::{repository::DiffType, status::FileStatus};use gpui::{    Action, AnyElement, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, Render,    SharedString, Subscription, Task, WeakEntity,};use language::{BufferId, Capability};use project::{    Project, ProjectPath,    git_store::{        Repository,        diff_buffer_list::{self, DiffBase},    },};use settings::{GitDiffBaseSetting, Settings};use std::{    any::{Any, TypeId},    sync::Arc,};use ui::{DiffStat, Divider, PopoverMenu, Tooltip, prelude::*};use workspace::{    ItemHandle, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,    ToolbarItemView, Workspace,    item::{Item, ItemEvent, SaveOptions, TabContentParams},    notifications::NotifyTaskExt,    searchable::SearchableItemHandle,};use zed_actions::agent::ReviewBranchDiff; /// The workspace item for a branch (merge-base) diff: "Changes since {branch}"./// It wraps a single [`DiffMultibuffer`] over [`DiffBase::Merge`] and delegates/// the [`Item`] surface to it. The merge base can be changed in place via the/// [`BranchDiffToolbar`]'s branch picker, which reloads without reconfiguring/// the editor (the merge styling is identical for every base ref).pub struct BranchDiff {    diff: Entity<DiffMultibuffer>,    project: Entity<Project>,    workspace: WeakEntity<Workspace>,    _diff_event_subscription: Subscription,} struct BranchDiffAddon {    branch_diff: Entity<diff_buffer_list::DiffBufferList>,} impl Addon for BranchDiffAddon {    fn to_any(&self) -> &dyn std::any::Any {        self    }     fn override_status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {        self.branch_diff            .read(cx)            .status_for_buffer_id(buffer_id, cx)    }} impl BranchDiff {    pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {        workspace.register_action(|workspace, _: &DeployBranchDiff, window, cx| {            Self::deploy_branch_diff(workspace, window, cx)        });        workspace.register_action(Self::compare_with_branch);        workspace::register_serializable_item::<Self>(cx);    }     pub(crate) fn deploy_branch_diff(        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        telemetry::event!("Git Branch Diff Opened");        let project = workspace.project().clone();        let Some(intended_repo) = project.read(cx).active_repository(cx) else {            let workspace = cx.entity().downgrade();            window                .spawn(cx, async |_cx| {                    let result: Result<()> = Err(anyhow!("No active repository"));                    result                })                .detach_and_notify_err(workspace, window, cx);            return;        };         let default_branch = intended_repo.update(cx, |repo, _| repo.default_branch(true));        let workspace = cx.entity();        let workspace_weak = workspace.downgrade();        window            .spawn(cx, async move |cx| {                let base_ref = default_branch.await??;                workspace.update_in(cx, |workspace, window, cx| {                    let git_store = project.read(cx).git_store().clone();                    let Some(base_ref) = base_ref else {                        ProjectDiff::deploy_at(workspace, None, window, cx);                        return;                    };                    let branch_diff =                        if git_store.read(cx).diff_base() == GitDiffBaseSetting::DefaultBranch {                            Some(git_store.update(cx, |git_store, cx| {                                git_store.ensure_display_diff(                                    intended_repo.clone(),                                    base_ref.clone(),                                    cx,                                )                            }))                        } else {                            None                        };                    Self::deploy_branch_diff_with_base_ref(                        workspace,                        project,                        intended_repo,                        base_ref,                        branch_diff,                        window,                        cx,                    );                })?;                 anyhow::Ok(())            })            .detach_and_notify_err(workspace_weak, window, cx);    }     fn compare_with_branch(        workspace: &mut Workspace,        _: &CompareWithBranch,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let project = workspace.project().clone();        let Some(repository) = project.read(cx).active_repository(cx) else {            let workspace = cx.entity().downgrade();            window                .spawn(cx, async |_cx| {                    let result: Result<()> = Err(anyhow!("No active repository"));                    result                })                .detach_and_notify_err(workspace, window, cx);            return;        };        let selected_branch = workspace.active_item_as::<Self>(cx).and_then(|item| {            match item.read(cx).diff_base(cx) {                DiffBase::Merge { base_ref } => Some(base_ref.clone()),                DiffBase::Head | DiffBase::Index | DiffBase::Staged => None,            }        });        let workspace_handle = workspace.weak_handle();        let on_select = Arc::new({            let repository = repository.clone();            let workspace = workspace_handle.clone();            move |branch: git::repository::Branch, window: &mut Window, cx: &mut App| {                let base_ref: SharedString = branch.name().to_owned().into();                workspace                    .update(cx, |workspace, cx| {                        Self::deploy_branch_diff_with_base_ref(                            workspace,                            project.clone(),                            repository.clone(),                            base_ref,                            None,                            window,                            cx,                        );                    })                    .ok();            }        });         workspace.toggle_modal(window, cx, |window, cx| {            branch_picker::select_modal(                workspace_handle,                Some(repository),                selected_branch,                on_select,                window,                cx,            )        });    }     pub(crate) fn deploy_branch_diff_with_base_ref(        workspace: &mut Workspace,        project: Entity<Project>,        intended_repo: Entity<Repository>,        base_ref: SharedString,        branch_diff: Option<Entity<diff_buffer_list::DiffBufferList>>,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let existing = workspace.items_of_type::<Self>(cx).find(|item| {            let item = item.read(cx);            matches!(                item.diff_base(cx),                DiffBase::Merge { base_ref: existing_base_ref } if existing_base_ref == &base_ref            ) && item                .repo(cx)                .is_some_and(|repo| repo.read(cx).id == intended_repo.read(cx).id)                && branch_diff.as_ref().is_none_or(|expected| {                    item.diff.read(cx).branch_diff().entity_id() == expected.entity_id()                })        });        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_branch_base(                            project,                            workspace.clone(),                            base_ref,                            intended_repo,                            branch_diff,                            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);    }     #[cfg(any(test, feature = "test-support"))]    pub 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 base_ref = main_branch                .await??                .context("Could not determine default branch")?;            cx.update(|window, cx| {                let git_store = project.read(cx).git_store().clone();                let branch_diff =                    if git_store.read(cx).diff_base() == GitDiffBaseSetting::DefaultBranch {                        Some(git_store.update(cx, |git_store, cx| {                            git_store.ensure_display_diff(repo.clone(), base_ref.clone(), cx)                        }))                    } else {                        None                    };                cx.new(|cx| {                    Self::new_with_base_ref(                        project,                        workspace,                        base_ref,                        Some(repo),                        branch_diff,                        window,                        cx,                    )                })            })        })    }     pub(crate) fn new_with_branch_base(        project: Entity<Project>,        workspace: Entity<Workspace>,        base_ref: SharedString,        repo: Entity<Repository>,        branch_diff: Option<Entity<diff_buffer_list::DiffBufferList>>,        window: &mut Window,        cx: &mut App,    ) -> Task<Result<Entity<Self>>> {        window.spawn(cx, async move |cx| {            cx.update(|window, cx| {                cx.new(|cx| {                    Self::new_with_base_ref(                        project,                        workspace,                        base_ref,                        Some(repo),                        branch_diff,                        window,                        cx,                    )                })            })        })    }     pub(crate) fn new_with_base_ref(        project: Entity<Project>,        workspace: Entity<Workspace>,        base_ref: SharedString,        repo: Option<Entity<Repository>>,        branch_diff: Option<Entity<diff_buffer_list::DiffBufferList>>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let branch_diff = branch_diff.unwrap_or_else(|| {            let git_store = project.read(cx).git_store().clone();            cx.new(|cx| {                diff_buffer_list::DiffBufferList::new(                    DiffBase::Merge { base_ref },                    git_store,                    repo,                    cx,                )            })        });        let branch_diff_for_addon = branch_diff.clone();        let diff = cx.new(|cx| {            DiffMultibuffer::new(                branch_diff,                Capability::ReadWrite,                "No changes",                move |editor, cx| {                    editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyDiffHunkDelegate)), cx);                    editor.rhs_editor().update(cx, move |rhs_editor, _cx| {                        rhs_editor.set_read_only(false);                        rhs_editor.register_addon(BranchDiffAddon {                            branch_diff: branch_diff_for_addon,                        });                    });                },                project.clone(),                workspace.clone(),                window,                cx,            )        });        Self::from_diff(diff, project, workspace, cx)    }     fn from_diff(        diff: Entity<DiffMultibuffer>,        project: Entity<Project>,        workspace: Entity<Workspace>,        cx: &mut Context<Self>,    ) -> Self {        let diff_event_subscription = cx.subscribe(&diff, |_, _, event: &EditorEvent, cx| {            cx.emit(event.clone())        });        Self {            diff,            project,            workspace: workspace.downgrade(),            _diff_event_subscription: diff_event_subscription,        }    }     pub(crate) fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {        self.diff.read(cx).diff_base(cx)    }     pub(crate) fn repo(&self, cx: &App) -> Option<Entity<Repository>> {        self.diff.read(cx).repo(cx)    }     fn set_merge_base(&mut self, base_ref: SharedString, cx: &mut Context<Self>) {        self.diff.update(cx, |diff, cx| {            diff.branch_diff().update(cx, |branch_diff, cx| {                branch_diff.set_diff_base(DiffBase::Merge { base_ref }, cx);            });        });    }     fn review_diff(&mut self, _: &ReviewDiff, window: &mut Window, cx: &mut Context<Self>) {        let DiffBase::Merge { base_ref } = self.diff_base(cx).clone() else {            return;        };        let Some(repo) = self.repo(cx) 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,                                }                                .boxed_clone(),                                cx,                            );                        })?;                    }                     anyhow::Ok(())                }            })            .detach_and_notify_err(workspace, window, cx);    }     #[cfg(any(test, feature = "test-support"))]    pub fn editor(&self, cx: &App) -> Entity<SplittableEditor> {        self.diff.read(cx).editor().clone()    }} impl EventEmitter<EditorEvent> for BranchDiff {} impl Focusable for BranchDiff {    fn focus_handle(&self, cx: &App) -> FocusHandle {        self.diff.read(cx).focus_handle(cx)    }} impl Item for BranchDiff {    type Event = EditorEvent;     fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {        Some(Icon::new(IconName::GitBranch).color(Color::Muted))    }     fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {        Editor::to_item_events(event, f)    }     fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.diff            .update(cx, |diff, cx| diff.deactivated(window, cx));    }     fn navigate(        &mut self,        data: Arc<dyn Any + Send>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> bool {        self.diff            .update(cx, |diff, cx| diff.navigate(data, window, cx))    }     fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {        Some(self.tab_content_text(0, cx))    }     fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {        Label::new(self.tab_content_text(0, cx))            .color(if params.selected {                Color::Default            } else {                Color::Muted            })            .into_any_element()    }     fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {        match self.diff_base(cx) {            DiffBase::Merge { base_ref } => format!("Changes since {}", base_ref).into(),            DiffBase::Head | DiffBase::Index | DiffBase::Staged => "Changes".into(),        }    }     fn telemetry_event_text(&self) -> Option<&'static str> {        Some("Branch Diff Opened")    }     fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {        Some(Box::new(self.diff.read(cx).editor().clone()))

Lines 1–500 of 1283.