1199 lines · 40.4 KB · Rust
RawDownload
use crate::{    branch_picker,    diff_multibuffer::DiffMultibuffer,    project_diff::{        self, CompareWithBranch, DeployBranchDiff, 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::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(Self::deploy_branch_diff);        workspace.register_action(Self::compare_with_branch);        workspace::register_serializable_item::<Self>(cx);    }     fn deploy_branch_diff(        workspace: &mut Workspace,        _: &DeployBranchDiff,        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??                    .context("Could not determine default branch")?;                 workspace.update_in(cx, |workspace, window, cx| {                    Self::deploy_branch_diff_with_base_ref(                        workspace,                        project,                        intended_repo,                        base_ref,                        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,                            window,                            cx,                        );                    })                    .ok();            }        });         workspace.toggle_modal(window, cx, |window, cx| {            branch_picker::select_modal(                workspace_handle,                Some(repository),                selected_branch,                on_select,                window,                cx,            )        });    }     fn deploy_branch_diff_with_base_ref(        workspace: &mut Workspace,        project: Entity<Project>,        intended_repo: Entity<Repository>,        base_ref: SharedString,        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            )        });        if let Some(existing) = existing {            workspace.activate_item(&existing, true, true, window, cx);             let needs_switch = existing.read(cx).repo(cx).map_or(true, |current| {                current.read(cx).id != intended_repo.read(cx).id            });             if needs_switch {                existing.update(cx, |branch_diff, cx| {                    branch_diff.set_repo(Some(intended_repo), 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,                            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| {                cx.new(|cx| {                    Self::new_with_base_ref(project, workspace, base_ref, Some(repo), window, cx)                })            })        })    }     pub(crate) fn new_with_branch_base(        project: Entity<Project>,        workspace: Entity<Workspace>,        base_ref: SharedString,        repo: Entity<Repository>,        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), window, cx)                })            })        })    }     pub(crate) fn new_with_base_ref(        project: Entity<Project>,        workspace: Entity<Workspace>,        base_ref: SharedString,        repo: Option<Entity<Repository>>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let branch_diff = cx.new(|cx| {            let mut branch_diff = diff_buffer_list::DiffBufferList::new(                DiffBase::Merge { base_ref },                project.clone(),                window,                cx,            );            if repo.is_some() {                branch_diff.set_repo(repo, cx);            }            branch_diff        });        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)    }     pub(crate) fn set_repo(&mut self, repo: Option<Entity<Repository>>, cx: &mut Context<Self>) {        self.diff.update(cx, |diff, cx| diff.set_repo(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()))    }     fn for_each_project_item(        &self,        cx: &App,        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),    ) {        self.diff.read(cx).for_each_project_item(cx, f);    }     fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {        self.diff.read(cx).active_project_path(cx)    }     fn set_nav_history(        &mut self,        nav_history: ItemNavHistory,        _: &mut Window,        cx: &mut Context<Self>,    ) {        self.diff            .update(cx, |diff, cx| diff.set_nav_history(nav_history, cx));    }     fn can_split(&self) -> bool {        true    }     fn clone_on_split(        &self,        _workspace_id: Option<workspace::WorkspaceId>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Task<Option<Entity<Self>>>    where        Self: Sized,

Lines 1–500 of 1199.