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:
parent
11838cf89e
commit
8ba7b349a5
20 changed files with 455 additions and 334 deletions
|
@ -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
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue