From cb543f9546beadc0605dc6cde92678282abde6c4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 7 Mar 2025 17:49:06 -0800 Subject: [PATCH] Git UI papercuts (#26316) Release Notes: - Git Beta: added `git:Add` as an alias for the existing `git::Diff` - Git Beta: Fixed a bug where the 'generate commit message' keybinding wasn't working. - Git Beta: Made the empty project diff state a little more helpful with a button to push, and a button to close the item. --- crates/git_ui/src/commit_modal.rs | 7 +- crates/git_ui/src/git_panel.rs | 383 ++---------------------------- crates/git_ui/src/git_ui.rs | 350 ++++++++++++++++++++++++++- crates/git_ui/src/project_diff.rs | 93 +++++++- 4 files changed, 460 insertions(+), 373 deletions(-) diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index b2e751bd41629c..8c50accfdec117 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -2,7 +2,7 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{commit_message_editor, GitPanel}; -use git::Commit; +use git::{Commit, GenerateCommitMessage}; use panel::{panel_button, panel_editor_style, panel_filled_button}; use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip}; @@ -372,6 +372,11 @@ impl Render for CommitModal { .key_context("GitCommit") .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::commit)) + .on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| { + this.git_panel.update(cx, |panel, cx| { + panel.generate_commit_message(cx); + }) + })) .on_action( cx.listener(|this, _: &zed_actions::git::Branch, window, cx| { toggle_branch_picker(this, window, cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 46e16dd9a8c994..df384cffcbe104 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,9 +1,9 @@ use crate::askpass_modal::AskPassModal; -use crate::branch_picker; use crate::commit_modal::CommitModal; use crate::git_panel_settings::StatusStyle; use crate::remote_output_toast::{RemoteAction, RemoteOutputToast}; use crate::repository_selector::filtered_repository_entries; +use crate::{branch_picker, render_remote_button}; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; @@ -26,12 +26,11 @@ use git::status::StageStatus; use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged}; use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; use gpui::{ - actions, anchored, deferred, hsla, percentage, point, uniform_list, Action, Animation, - AnimationExt as _, AnyView, BoxShadow, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, - Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, PromptLevel, - ScrollStrategy, Stateful, Subscription, Task, Transformation, UniformListScrollHandle, - WeakEntity, + actions, anchored, deferred, percentage, uniform_list, Action, Animation, AnimationExt as _, + ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, + ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent, + MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Stateful, Subscription, Task, + Transformation, UniformListScrollHandle, WeakEntity, }; use itertools::Itertools; use language::{Buffer, File}; @@ -49,7 +48,6 @@ use project::{ }; use serde::{Deserialize, Serialize}; use settings::Settings as _; -use smallvec::smallvec; use std::cell::RefCell; use std::future::Future; use std::path::{Path, PathBuf}; @@ -58,8 +56,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, - ScrollbarState, Tooltip, + prelude::*, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, + Tooltip, }; use util::{maybe, post_inc, ResultExt, TryFutureExt}; use workspace::{AppState, OpenOptions, OpenVisible}; @@ -1748,7 +1746,7 @@ impl GitPanel { } fn can_push_and_pull(&self, cx: &App) -> bool { - !self.project.read(cx).is_via_collab() + crate::can_push_and_pull(&self.project, cx) } fn get_current_remote( @@ -3313,159 +3311,6 @@ impl Render for GitPanelMessageTooltip { } } -fn git_action_tooltip( - label: impl Into, - action: &dyn Action, - command: impl Into, - focus_handle: Option, - window: &mut Window, - cx: &mut App, -) -> AnyView { - let label = label.into(); - let command = command.into(); - - if let Some(handle) = focus_handle { - Tooltip::with_meta_in( - label.clone(), - Some(action), - command.clone(), - &handle, - window, - cx, - ) - } else { - Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) - } -} - -#[derive(IntoElement)] -struct SplitButton { - pub left: ButtonLike, - pub right: AnyElement, -} - -impl SplitButton { - fn new( - id: impl Into, - left_label: impl Into, - ahead_count: usize, - behind_count: usize, - left_icon: Option, - left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - let id = id.into(); - - fn count(count: usize) -> impl IntoElement { - h_flex() - .ml_neg_px() - .h(rems(0.875)) - .items_center() - .overflow_hidden() - .px_0p5() - .child( - Label::new(count.to_string()) - .size(LabelSize::XSmall) - .line_height_style(LineHeightStyle::UiLabel), - ) - } - - let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0); - - let left = ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", id).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .when(should_render_counts, |this| { - this.child( - h_flex() - .ml_neg_0p5() - .mr_1() - .when(behind_count > 0, |this| { - this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) - .child(count(behind_count)) - }) - .when(ahead_count > 0, |this| { - this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) - .child(count(ahead_count)) - }), - ) - }) - .when_some(left_icon, |this, left_icon| { - this.child( - h_flex() - .ml_neg_0p5() - .mr_1() - .child(Icon::new(left_icon).size(IconSize::XSmall)), - ) - }) - .child( - div() - .child(Label::new(left_label).size(LabelSize::Small)) - .mr_0p5(), - ) - .on_click(left_on_click) - .tooltip(tooltip); - - let right = - render_git_action_menu(ElementId::Name(format!("split-button-right-{}", id).into())) - .into_any_element(); - // .on_click(right_on_click); - - Self { left, right } - } -} - -impl RenderOnce for SplitButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().text_muted.alpha(0.12)) - .child(self.left) - .child( - div() - .h_full() - .w_px() - .bg(cx.theme().colors().text_muted.alpha(0.16)), - ) - .child(self.right) - .bg(ElevationIndex::Surface.on_elevation_bg(cx)) - .shadow(smallvec![BoxShadow { - color: hsla(0.0, 0.0, 0.0, 0.16), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - } -} - -fn render_git_action_menu(id: impl Into) -> impl IntoElement { - PopoverMenu::new(id.into()) - .trigger( - ui::ButtonLike::new_rounded_right("split-button-right") - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::None) - .child( - div() - .px_1() - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), - ), - ) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |context_menu, _, _| { - context_menu - .action("Fetch", git::Fetch.boxed_clone()) - .action("Pull", git::Pull.boxed_clone()) - .separator() - .action("Push", git::Push.boxed_clone()) - .action("Force Push", git::ForcePush.boxed_clone()) - })) - }) - .anchor(Corner::TopRight) -} - #[derive(IntoElement, IntoComponent)] #[component(scope = "Version Control")] pub struct PanelRepoFooter { @@ -3516,200 +3361,6 @@ impl PanelRepoFooter { .menu(move |window, cx| Some(git_panel_context_menu(window, cx))) .anchor(Corner::TopRight) } - - fn panel_focus_handle(&self, cx: &App) -> Option { - if let Some(git_panel) = self.git_panel.clone() { - Some(git_panel.focus_handle(cx)) - } else { - None - } - } - - fn render_push_button(&self, id: SharedString, ahead: u32, cx: &mut App) -> SplitButton { - let panel = self.git_panel.clone(); - let panel_focus_handle = self.panel_focus_handle(cx); - - SplitButton::new( - id, - "Push", - ahead as usize, - 0, - None, - move |_, window, cx| { - if let Some(panel) = panel.as_ref() { - panel.update(cx, |panel, cx| { - panel.push(false, window, cx); - }); - } - }, - move |window, cx| { - git_action_tooltip( - "Push committed changes to remote", - &git::Push, - "git push", - panel_focus_handle.clone(), - window, - cx, - ) - }, - ) - } - - fn render_pull_button( - &self, - id: SharedString, - ahead: u32, - behind: u32, - cx: &mut App, - ) -> SplitButton { - let panel = self.git_panel.clone(); - let panel_focus_handle = self.panel_focus_handle(cx); - - SplitButton::new( - id, - "Pull", - ahead as usize, - behind as usize, - None, - move |_, window, cx| { - if let Some(panel) = panel.as_ref() { - panel.update(cx, |panel, cx| { - panel.pull(window, cx); - }); - } - }, - move |window, cx| { - git_action_tooltip( - "Pull", - &git::Pull, - "git pull", - panel_focus_handle.clone(), - window, - cx, - ) - }, - ) - } - - fn render_fetch_button(&self, id: SharedString, cx: &mut App) -> SplitButton { - let panel = self.git_panel.clone(); - let panel_focus_handle = self.panel_focus_handle(cx); - - SplitButton::new( - id, - "Fetch", - 0, - 0, - Some(IconName::ArrowCircle), - move |_, window, cx| { - if let Some(panel) = panel.as_ref() { - panel.update(cx, |panel, cx| { - panel.fetch(window, cx); - }); - } - }, - move |window, cx| { - git_action_tooltip( - "Fetch updates from remote", - &git::Fetch, - "git fetch", - panel_focus_handle.clone(), - window, - cx, - ) - }, - ) - } - - fn render_publish_button(&self, id: SharedString, cx: &mut App) -> SplitButton { - let panel = self.git_panel.clone(); - let panel_focus_handle = self.panel_focus_handle(cx); - - SplitButton::new( - id, - "Publish", - 0, - 0, - Some(IconName::ArrowUpFromLine), - move |_, window, cx| { - if let Some(panel) = panel.as_ref() { - panel.update(cx, |panel, cx| { - panel.push(false, window, cx); - }); - } - }, - move |window, cx| { - git_action_tooltip( - "Publish branch to remote", - &git::Push, - "git push --set-upstream", - panel_focus_handle.clone(), - window, - cx, - ) - }, - ) - } - - fn render_republish_button(&self, id: SharedString, cx: &mut App) -> SplitButton { - let panel = self.git_panel.clone(); - let panel_focus_handle = self.panel_focus_handle(cx); - - SplitButton::new( - id, - "Republish", - 0, - 0, - Some(IconName::ArrowUpFromLine), - move |_, window, cx| { - if let Some(panel) = panel.as_ref() { - panel.update(cx, |panel, cx| { - panel.push(false, window, cx); - }); - } - }, - move |window, cx| { - git_action_tooltip( - "Re-publish branch to remote", - &git::Push, - "git push --set-upstream", - panel_focus_handle.clone(), - window, - cx, - ) - }, - ) - } - - fn render_relevant_button( - &self, - id: impl Into, - branch: &Branch, - cx: &mut App, - ) -> Option { - if let Some(git_panel) = self.git_panel.as_ref() { - if !git_panel.read(cx).can_push_and_pull(cx) { - return None; - } - } - let id = id.into(); - let upstream = branch.upstream.as_ref(); - Some(match upstream { - Some(Upstream { - tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }), - .. - }) => match (*ahead, *behind) { - (0, 0) => self.render_fetch_button(id, cx), - (ahead, 0) => self.render_push_button(id, ahead, cx), - (ahead, behind) => self.render_pull_button(id, ahead, behind, cx), - }, - Some(Upstream { - tracking: UpstreamTracking::Gone, - .. - }) => self.render_republish_button(id, cx), - None => self.render_publish_button(id, cx), - }) - } } impl RenderOnce for PanelRepoFooter { @@ -3825,8 +3476,20 @@ impl RenderOnce for PanelRepoFooter { .children(spinner) .child(self.render_overflow_menu(overflow_menu_id)) .when_some(branch, |this, branch| { - let button = self.render_relevant_button(self.id.clone(), &branch, cx); - this.children(button) + let mut focus_handle = None; + if let Some(git_panel) = self.git_panel.as_ref() { + if !git_panel.read(cx).can_push_and_pull(cx) { + return this; + } + focus_handle = Some(git_panel.focus_handle(cx)); + } + + this.children(render_remote_button( + self.id.clone(), + &branch, + focus_handle, + true, + )) }), ) } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 8414e7737798ff..80ac9c113cf3a9 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -1,9 +1,13 @@ use ::settings::Settings; -use git::status::FileStatus; +use git::{ + repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, + status::FileStatus, +}; use git_panel_settings::GitPanelSettings; -use gpui::App; +use gpui::{App, Entity, FocusHandle}; +use project::Project; use project_diff::ProjectDiff; -use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; +use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString}; use workspace::Workspace; mod askpass_modal; @@ -89,3 +93,343 @@ pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement { }; Icon::new(icon_name).color(Color::Custom(color)) } + +fn can_push_and_pull(project: &Entity, cx: &App) -> bool { + !project.read(cx).is_via_collab() +} + +fn render_remote_button( + id: impl Into, + branch: &Branch, + keybinding_target: Option, + show_fetch_button: bool, +) -> Option { + let id = id.into(); + let upstream = branch.upstream.as_ref(); + match upstream { + Some(Upstream { + tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }), + .. + }) => match (*ahead, *behind) { + (0, 0) if show_fetch_button => { + Some(remote_button::render_fetch_button(keybinding_target, id)) + } + (0, 0) => None, + (ahead, 0) => Some(remote_button::render_push_button( + keybinding_target.clone(), + id, + ahead, + )), + (ahead, behind) => Some(remote_button::render_pull_button( + keybinding_target.clone(), + id, + ahead, + behind, + )), + }, + Some(Upstream { + tracking: UpstreamTracking::Gone, + .. + }) => Some(remote_button::render_republish_button( + keybinding_target, + id, + )), + None => Some(remote_button::render_publish_button(keybinding_target, id)), + } +} + +mod remote_button { + use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle}; + use ui::{ + div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable, + ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize, + IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu, + RenderOnce, SharedString, Styled, Tooltip, Window, + }; + + pub fn render_fetch_button( + keybinding_target: Option, + id: SharedString, + ) -> SplitButton { + SplitButton::new( + id, + "Fetch", + 0, + 0, + Some(IconName::ArrowCircle), + move |_, window, cx| { + window.dispatch_action(Box::new(git::Fetch), cx); + }, + move |window, cx| { + git_action_tooltip( + "Fetch updates from remote", + &git::Fetch, + "git fetch", + keybinding_target.clone(), + window, + cx, + ) + }, + ) + } + + pub fn render_push_button( + keybinding_target: Option, + id: SharedString, + ahead: u32, + ) -> SplitButton { + SplitButton::new( + id, + "Push", + ahead as usize, + 0, + None, + move |_, window, cx| { + window.dispatch_action(Box::new(git::Push), cx); + }, + move |window, cx| { + git_action_tooltip( + "Push committed changes to remote", + &git::Push, + "git push", + keybinding_target.clone(), + window, + cx, + ) + }, + ) + } + + pub fn render_pull_button( + keybinding_target: Option, + id: SharedString, + ahead: u32, + behind: u32, + ) -> SplitButton { + SplitButton::new( + id, + "Pull", + ahead as usize, + behind as usize, + None, + move |_, window, cx| { + window.dispatch_action(Box::new(git::Pull), cx); + }, + move |window, cx| { + git_action_tooltip( + "Pull", + &git::Pull, + "git pull", + keybinding_target.clone(), + window, + cx, + ) + }, + ) + } + + pub fn render_publish_button( + keybinding_target: Option, + id: SharedString, + ) -> SplitButton { + SplitButton::new( + id, + "Publish", + 0, + 0, + Some(IconName::ArrowUpFromLine), + move |_, window, cx| { + window.dispatch_action(Box::new(git::Push), cx); + }, + move |window, cx| { + git_action_tooltip( + "Publish branch to remote", + &git::Push, + "git push --set-upstream", + keybinding_target.clone(), + window, + cx, + ) + }, + ) + } + + pub fn render_republish_button( + keybinding_target: Option, + id: SharedString, + ) -> SplitButton { + SplitButton::new( + id, + "Republish", + 0, + 0, + Some(IconName::ArrowUpFromLine), + move |_, window, cx| { + window.dispatch_action(Box::new(git::Push), cx); + }, + move |window, cx| { + git_action_tooltip( + "Re-publish branch to remote", + &git::Push, + "git push --set-upstream", + keybinding_target.clone(), + window, + cx, + ) + }, + ) + } + + fn git_action_tooltip( + label: impl Into, + action: &dyn Action, + command: impl Into, + focus_handle: Option, + window: &mut Window, + cx: &mut App, + ) -> AnyView { + let label = label.into(); + let command = command.into(); + + if let Some(handle) = focus_handle { + Tooltip::with_meta_in( + label.clone(), + Some(action), + command.clone(), + &handle, + window, + cx, + ) + } else { + Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) + } + } + + fn render_git_action_menu(id: impl Into) -> impl IntoElement { + PopoverMenu::new(id.into()) + .trigger( + ui::ButtonLike::new_rounded_right("split-button-right") + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::None) + .child( + div() + .px_1() + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + ), + ) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, |context_menu, _, _| { + context_menu + .action("Fetch", git::Fetch.boxed_clone()) + .action("Pull", git::Pull.boxed_clone()) + .separator() + .action("Push", git::Push.boxed_clone()) + .action("Force Push", git::ForcePush.boxed_clone()) + })) + }) + .anchor(Corner::TopRight) + } + + #[derive(IntoElement)] + pub struct SplitButton { + pub left: ButtonLike, + pub right: AnyElement, + } + + impl SplitButton { + fn new( + id: impl Into, + left_label: impl Into, + ahead_count: usize, + behind_count: usize, + left_icon: Option, + left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + let id = id.into(); + + fn count(count: usize) -> impl IntoElement { + h_flex() + .ml_neg_px() + .h(rems(0.875)) + .items_center() + .overflow_hidden() + .px_0p5() + .child( + Label::new(count.to_string()) + .size(LabelSize::XSmall) + .line_height_style(LineHeightStyle::UiLabel), + ) + } + + let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0); + + let left = ui::ButtonLike::new_rounded_left(ElementId::Name( + format!("split-button-left-{}", id).into(), + )) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .when(should_render_counts, |this| { + this.child( + h_flex() + .ml_neg_0p5() + .mr_1() + .when(behind_count > 0, |this| { + this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) + .child(count(behind_count)) + }) + .when(ahead_count > 0, |this| { + this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) + .child(count(ahead_count)) + }), + ) + }) + .when_some(left_icon, |this, left_icon| { + this.child( + h_flex() + .ml_neg_0p5() + .mr_1() + .child(Icon::new(left_icon).size(IconSize::XSmall)), + ) + }) + .child( + div() + .child(Label::new(left_label).size(LabelSize::Small)) + .mr_0p5(), + ) + .on_click(left_on_click) + .tooltip(tooltip); + + let right = render_git_action_menu(ElementId::Name( + format!("split-button-right-{}", id).into(), + )) + .into_any_element(); + + Self { left, right } + } + } + + impl RenderOnce for SplitButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().text_muted.alpha(0.12)) + .child(div().flex_grow().child(self.left)) + .child( + div() + .h_full() + .w_px() + .bg(cx.theme().colors().text_muted.alpha(0.16)), + ) + .child(self.right) + .bg(ElevationIndex::Surface.on_elevation_bg(cx)) + .shadow(smallvec::smallvec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.16), + offset: point(px(0.), px(1.)), + blur_radius: px(0.), + spread_radius: px(0.), + }]) + } + } +} diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index a3b9591fbc6534..973e78270c8ae3 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -10,7 +10,8 @@ use editor::{ use feature_flags::FeatureFlagViewExt; use futures::StreamExt; use git::{ - status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, + repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, + UnstageAll, UnstageAndNext, }; use gpui::{ actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, @@ -24,27 +25,27 @@ use project::{ }; use std::any::{Any, TypeId}; use theme::ActiveTheme; -use ui::{prelude::*, vertical_divider, Tooltip}; +use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip}; use util::ResultExt as _; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, - ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, - Workspace, + CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, + ToolbarItemView, Workspace, }; -actions!(git, [Diff]); +actions!(git, [Diff, Add]); pub struct ProjectDiff { + project: Entity, multibuffer: Entity, editor: Entity, - project: Entity, git_store: Entity, workspace: WeakEntity, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option, - + current_branch: Option, _task: Task>, _subscription: Subscription, } @@ -70,6 +71,9 @@ impl ProjectDiff { let Some(window) = window else { return }; cx.when_flag_enabled::(window, |workspace, _, _cx| { workspace.register_action(Self::deploy); + workspace.register_action(|workspace, _: &Add, window, cx| { + Self::deploy(workspace, &Diff, window, cx); + }); }); workspace::register_serializable_item::(cx); @@ -179,6 +183,7 @@ impl ProjectDiff { multibuffer, pending_scroll: None, update_needed: send, + current_branch: None, _task: worker, _subscription: git_store_subscription, } @@ -444,6 +449,20 @@ impl ProjectDiff { mut cx: AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { + this.update(&mut cx, |this, cx| { + let new_branch = + this.git_store + .read(cx) + .active_repository() + .and_then(|active_repository| { + active_repository.read(cx).current_branch().cloned() + }); + if new_branch != this.current_branch { + this.current_branch = new_branch; + cx.notify(); + } + })?; + let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?; for buffer_to_load in buffers_to_load { if let Some(buffer) = buffer_to_load.await.log_err() { @@ -642,9 +661,11 @@ impl Item for ProjectDiff { } impl Render for ProjectDiff { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_empty = self.multibuffer.read(cx).is_empty(); + let can_push_and_pull = crate::can_push_and_pull(&self.project, cx); + div() .track_focus(&self.focus_handle) .key_context(if is_empty { "EmptyPane" } else { "GitDiff" }) @@ -654,7 +675,61 @@ impl Render for ProjectDiff { .justify_center() .size_full() .when(is_empty, |el| { - el.child(Label::new("No uncommitted changes")) + el.child( + v_flex() + .gap_1() + .child( + h_flex() + .justify_around() + .child(Label::new("No uncommitted changes")), + ) + .when(can_push_and_pull, |this_div| { + let keybinding_focus_handle = self.focus_handle(cx); + + this_div.when_some(self.current_branch.as_ref(), |this_div, branch| { + let remote_button = crate::render_remote_button( + "project-diff-remote-button", + branch, + Some(keybinding_focus_handle.clone()), + false, + ); + + match remote_button { + Some(button) => { + this_div.child(h_flex().justify_around().child(button)) + } + None => this_div.child( + h_flex() + .justify_around() + .child(Label::new("Remote up to date")), + ), + } + }) + }) + .map(|this| { + let keybinding_focus_handle = self.focus_handle(cx).clone(); + + this.child( + h_flex().justify_around().mt_1().child( + Button::new("project-diff-close-button", "Close") + // .style(ButtonStyle::Transparent) + .key_binding(KeyBinding::for_action_in( + &CloseActiveItem::default(), + &keybinding_focus_handle, + window, + cx, + )) + .on_click(move |_, window, cx| { + window.focus(&keybinding_focus_handle); + window.dispatch_action( + Box::new(CloseActiveItem::default()), + cx, + ); + }), + ), + ) + }), + ) }) .when(!is_empty, |el| el.child(self.editor.clone())) }