Make the branch picker in the commit modal a popover (#25697)

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Mikayla Maki 2025-02-26 17:56:07 -08:00 committed by GitHub
parent 11838cf89e
commit 8ba7b349a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 455 additions and 334 deletions

View file

@ -16,6 +16,7 @@ path = "src/git_ui.rs"
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
popover_button.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true

View file

@ -1,16 +1,16 @@
use anyhow::{anyhow, Context as _, Result};
use anyhow::{Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use git::repository::Branch;
use gpui::{
rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, WeakEntity, Window,
Task, Window,
};
use picker::{Picker, PickerDelegate};
use project::ProjectPath;
use project::{Project, ProjectPath};
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@ -23,19 +23,29 @@ pub fn init(cx: &mut App) {
}
pub fn open(
_: &mut Workspace,
workspace: &mut Workspace,
_: &zed_actions::git::Branch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let this = cx.entity().clone();
let project = workspace.project().clone();
let this = cx.entity();
let style = BranchListStyle::Modal;
cx.spawn_in(window, |_, mut cx| async move {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?;
this.update_in(&mut cx, |workspace, window, cx| {
this.update_in(&mut cx, move |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
BranchList::new(delegate, 34., window, cx)
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
let mut list = BranchList::new(project, style, 34., cx);
list._subscription = Some(_subscription);
list.picker = Some(picker);
list
})
})?;
@ -44,34 +54,86 @@ pub fn open(
.detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
}
pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
cx.new(|cx| {
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
list.reload_branches(window, cx);
list
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum BranchListStyle {
Modal,
Popover,
}
pub struct BranchList {
pub picker: Entity<Picker<BranchListDelegate>>,
rem_width: f32,
_subscription: Subscription,
popover_handle: PopoverMenuHandle<Self>,
default_focus_handle: FocusHandle,
project: Entity<Project>,
style: BranchListStyle,
pub picker: Option<Entity<Picker<BranchListDelegate>>>,
_subscription: Option<Subscription>,
}
impl popover_button::TriggerablePopover for BranchList {
fn menu_handle(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> PopoverMenuHandle<Self> {
self.popover_handle.clone()
}
}
impl BranchList {
pub fn new(
delegate: BranchListDelegate,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
fn new(project: Entity<Project>, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self {
let popover_handle = PopoverMenuHandle::default();
Self {
picker,
project,
picker: None,
rem_width,
_subscription,
popover_handle,
default_focus_handle: cx.focus_handle(),
style,
_subscription: None,
}
}
fn reload_branches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let project = self.project.clone();
let style = self.style;
cx.spawn_in(window, |this, mut cx| async move {
let delegate = BranchListDelegate::new(project, style, 20, &cx).await?;
let picker =
cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?;
this.update(&mut cx, |branch_list, cx| {
let subscription =
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent));
branch_list.picker = Some(picker);
branch_list._subscription = Some(subscription);
cx.notify();
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
impl ModalView for BranchList {}
impl EventEmitter<DismissEvent> for BranchList {}
impl Focusable for BranchList {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
self.picker
.as_ref()
.map(|picker| picker.focus_handle(cx))
.unwrap_or_else(|| self.default_focus_handle.clone())
}
}
@ -79,12 +141,27 @@ impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
.when_some(self.picker.clone(), |div, picker| {
div.child(picker.clone()).on_mouse_down_out({
let picker = picker.clone();
cx.listener(move |_, _, window, cx| {
picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
})
})
}))
})
.when_none(&self.picker, |div| {
div.child(
h_flex()
.id("branch-picker-error")
.on_click(
cx.listener(|this, _, window, cx| this.reload_branches(window, cx)),
)
.child("Could not load branches.")
.child("Click to retry"),
)
})
}
}
@ -108,7 +185,8 @@ impl BranchEntry {
pub struct BranchListDelegate {
matches: Vec<BranchEntry>,
all_branches: Vec<Branch>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
style: BranchListStyle,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
@ -116,13 +194,14 @@ pub struct BranchListDelegate {
}
impl BranchListDelegate {
pub async fn new(
workspace: Entity<Workspace>,
async fn new(
project: Entity<Project>,
style: BranchListStyle,
branch_name_trailoff_after: usize,
cx: &AsyncApp,
) -> Result<Self> {
let all_branches_request = cx.update(|cx| {
let project = workspace.read(cx).project().read(cx);
let project = project.read(cx);
let first_worktree = project
.visible_worktrees(cx)
.next()
@ -135,7 +214,8 @@ impl BranchListDelegate {
Ok(Self {
matches: vec![],
workspace: workspace.downgrade(),
project,
style,
all_branches,
selected_index: 0,
last_query: Default::default(),
@ -254,18 +334,12 @@ impl PickerDelegate for BranchListDelegate {
return;
};
let current_branch = self
.workspace
.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.active_repository(cx)
.and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
})
.ok()
.flatten();
let current_branch = self.project.update(cx, |project, cx| {
project
.active_repository(cx)
.and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
});
if current_branch == Some(branch.name().to_string()) {
cx.emit(DismissEvent);
@ -276,13 +350,7 @@ impl PickerDelegate for BranchListDelegate {
let branch = branch.clone();
|picker, mut cx| async move {
let branch_change_task = picker.update(&mut cx, |this, cx| {
let workspace = this
.delegate
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().read(cx);
let project = this.delegate.project.read(cx);
let branch_to_checkout = match branch {
BranchEntry::Branch(branch) => branch.string,
BranchEntry::History(string) => string,
@ -327,6 +395,10 @@ impl PickerDelegate for BranchListDelegate {
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.inset(true)
.spacing(match self.style {
BranchListStyle::Modal => ListItemSpacing::default(),
BranchListStyle::Popover => ListItemSpacing::ExtraDense,
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.when(matches!(hit, BranchEntry::History(_)), |el| {

View file

@ -1,8 +1,11 @@
// #![allow(unused, dead_code)]
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use git::Commit;
use panel::{panel_button, panel_editor_style, panel_filled_button};
use popover_button::TriggerablePopover;
use project::Project;
use ui::{prelude::*, KeybindingHint, Tooltip};
use editor::{Editor, EditorElement};
@ -64,6 +67,7 @@ pub fn init(cx: &mut App) {
}
pub struct CommitModal {
branch_list: Entity<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
@ -139,9 +143,11 @@ impl CommitModal {
is_open,
active_index,
};
let project = workspace.project().clone();
workspace.open_panel::<GitPanel>(window, cx);
workspace.toggle_modal(window, cx, move |window, cx| {
CommitModal::new(git_panel, restore_dock_position, window, cx)
CommitModal::new(git_panel, restore_dock_position, project, window, cx)
})
});
}
@ -149,6 +155,7 @@ impl CommitModal {
fn new(
git_panel: Entity<GitPanel>,
restore_dock: RestoreDock,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -182,14 +189,21 @@ impl CommitModal {
let focus_handle = commit_editor.focus_handle(cx);
cx.on_focus_out(&focus_handle, window, |_, _, _, cx| {
cx.emit(DismissEvent);
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
if !this
.branch_list
.focus_handle(cx)
.contains_focused(window, cx)
{
cx.emit(DismissEvent);
}
})
.detach();
let properties = ModalContainerProperties::new(window, 50);
Self {
branch_list: branch_picker::popover(project.clone(), window, cx),
git_panel,
commit_editor,
restore_dock,
@ -230,7 +244,7 @@ impl CommitModal {
)
}
fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let (branch, tooltip, commit_label, co_authors) =
@ -238,7 +252,12 @@ impl CommitModal {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
@ -248,13 +267,13 @@ impl CommitModal {
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
"Commit All"
};
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
});
let branch_selector = panel_button(branch)
let branch_picker_button = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Placeholder)
@ -269,6 +288,13 @@ impl CommitModal {
}))
.style(ButtonStyle::Transparent);
let branch_picker = popover_button::PopoverButton::new(
self.branch_list.clone(),
Corner::BottomLeft,
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
);
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
Some(
@ -303,7 +329,12 @@ impl CommitModal {
.w_full()
.h(px(self.properties.footer_height))
.gap_1()
.child(h_flex().gap_1().child(branch_selector).children(co_authors))
.child(
h_flex()
.gap_1()
.child(branch_picker.render(window, cx))
.children(co_authors),
)
.child(div().flex_1())
.child(
h_flex()
@ -340,6 +371,13 @@ impl Render for CommitModal {
.key_context("GitCommit")
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
.on_action(
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
this.branch_list.update(cx, |branch_list, cx| {
branch_list.menu_handle(window, cx).toggle(window, cx);
})
}),
)
.elevation_3(cx)
.overflow_hidden()
.flex_none()