ZIm/crates/git_ui/src/commit_modal.rs
Joseph T. Lyons 8c4da9fba0
Disable Git panel button to open commit editor in certain cases (#26000)
Also:

- Internally renames a bit of code to make it easy to identify between
when we are disabling the buttons that open and close the modal editor
(in Git Panel and Project Diff) vs when we are disabling the commit
buttons (in Git Panel and Git commit editor modal).
- Deletes some unused code.

Release Notes:

- Unified disabling / enabling the button to open the Git commit editor
modal in the Git panel with the Project Diff commit button.
- Unified disabling / enabling the commit buttons, for the same cases,
between the Git panel and Git commit editor modal.
2025-03-04 05:35:18 +00:00

410 lines
14 KiB
Rust

// #![allow(unused, dead_code)]
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use git::{Commit, ShowCommitEditor};
use panel::{panel_button, panel_editor_style, panel_filled_button};
use project::Project;
use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
use editor::{Editor, EditorElement};
use gpui::*;
use util::ResultExt;
use workspace::{
dock::{Dock, PanelHandle},
ModalView, Workspace,
};
// nate: It is a pain to get editors to size correctly and not overflow.
//
// this can get replaced with a simple flex layout with more time/a more thoughtful approach.
#[derive(Debug, Clone, Copy)]
pub struct ModalContainerProperties {
pub modal_width: f32,
pub editor_height: f32,
pub footer_height: f32,
pub container_padding: f32,
pub modal_border_radius: f32,
}
impl ModalContainerProperties {
pub fn new(window: &Window, preferred_char_width: usize) -> Self {
let container_padding = 5.0;
// Calculate width based on character width
let mut modal_width = 460.0;
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
modal_width = preferred_char_width as f32 * em_width.0 + (container_padding * 2.0);
}
Self {
modal_width,
editor_height: 300.0,
footer_height: 24.0,
container_padding,
modal_border_radius: 12.0,
}
}
pub fn editor_border_radius(&self) -> Pixels {
px(self.modal_border_radius - self.container_padding / 2.0)
}
}
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
};
CommitModal::register(workspace, window, cx)
})
.detach();
}
pub struct CommitModal {
branch_list: Entity<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
properties: ModalContainerProperties,
}
impl Focusable for CommitModal {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.commit_editor.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for CommitModal {}
impl ModalView for CommitModal {
fn on_before_dismiss(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> workspace::DismissDecision {
self.git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(false, cx);
});
self.restore_dock
.dock
.update(cx, |dock, cx| {
if let Some(active_index) = self.restore_dock.active_index {
dock.activate_panel(active_index, window, cx)
}
dock.set_open(self.restore_dock.is_open, window, cx)
})
.log_err();
workspace::DismissDecision::Dismiss(true)
}
}
struct RestoreDock {
dock: WeakEntity<Dock>,
is_open: bool,
active_index: Option<usize>,
}
impl CommitModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
workspace.register_action(|workspace, _: &ShowCommitEditor, window, cx| {
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
return;
};
let (can_open_commit_editor, conflict) = git_panel.update(cx, |git_panel, cx| {
let can_open_commit_editor = git_panel.can_open_commit_editor();
let conflict = git_panel.has_unstaged_conflicts();
if can_open_commit_editor {
git_panel.set_modal_open(true, cx);
}
(can_open_commit_editor, conflict)
});
if !can_open_commit_editor {
let message = if conflict {
"There are still conflicts. You must stage these before committing."
} else {
"No changes to commit."
};
let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
cx.spawn(|_, _| async move {
prompt.await.ok();
})
.detach();
return;
}
let dock = workspace.dock_at_position(git_panel.position(window, cx));
let is_open = dock.read(cx).is_open();
let active_index = dock.read(cx).active_panel_index();
let dock = dock.downgrade();
let restore_dock_position = RestoreDock {
dock,
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, project, window, cx)
})
});
}
fn new(
git_panel: Entity<GitPanel>,
restore_dock: RestoreDock,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
let buffer = git_panel.commit_message_buffer(cx).clone();
let project = git_panel.project.clone();
cx.new(|cx| commit_message_editor(buffer, None, project.clone(), false, window, cx))
});
let commit_message = commit_editor.read(cx).text(cx);
if let Some(suggested_message) = suggested_message {
if commit_message.is_empty() {
commit_editor.update(cx, |editor, cx| {
editor.set_text(suggested_message, window, cx);
editor.select_all(&Default::default(), window, cx);
});
} else {
if commit_message.as_str().trim() == suggested_message.trim() {
commit_editor.update(cx, |editor, cx| {
// select the message to make it easy to delete
editor.select_all(&Default::default(), window, cx);
});
}
}
}
let focus_handle = commit_editor.focus_handle(cx);
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,
properties,
}
}
fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
let editor_style = panel_editor_style(true, window, cx);
EditorElement::new(&self.commit_editor, editor_style)
}
pub fn render_commit_editor(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let properties = self.properties;
let padding_t = 3.0;
let padding_b = 6.0;
// magic number for editor not to overflow the container??
let extra_space_hack = 1.5 * window.line_height();
v_flex()
.h(px(properties.editor_height + padding_b + padding_t) + extra_space_hack)
.w_full()
.flex_none()
.rounded(properties.editor_border_radius())
.overflow_hidden()
.px_1p5()
.pt(px(padding_t))
.pb(px(padding_b))
.child(
div()
.h(px(properties.editor_height))
.w_full()
.child(self.commit_editor_element(window, cx)),
)
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let (branch, can_commit, tooltip, commit_label, co_authors) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
(branch, can_commit, tooltip, title, co_authors)
});
let branch_picker_button = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Placeholder)
.color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
&zed_actions::git::Branch,
))
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
}))
.style(ButtonStyle::Transparent);
let branch_picker = 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(
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
.suffix("Cancel"),
)
} else {
None
};
let panel_editor_focus_handle =
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
let commit_button = panel_filled_button(commit_label)
.tooltip(move |window, cx| {
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
})
.disabled(!can_commit)
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
this.git_panel
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
cx.emit(DismissEvent);
}));
h_flex()
.group("commit_editor_footer")
.flex_none()
.w_full()
.items_center()
.justify_between()
.w_full()
.h(px(self.properties.footer_height))
.gap_1()
.child(
h_flex()
.gap_1()
.child(branch_picker.render(window, cx))
.children(co_authors),
)
.child(div().flex_1())
.child(
h_flex()
.items_center()
.justify_end()
.flex_none()
.px_1()
.gap_4()
.children(close_kb_hint)
.child(commit_button),
)
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
self.git_panel
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
cx.emit(DismissEvent);
}
}
impl Render for CommitModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let properties = self.properties;
let width = px(properties.modal_width);
let container_padding = px(properties.container_padding);
let border_radius = properties.modal_border_radius;
let editor_focus_handle = self.commit_editor.focus_handle(cx);
v_flex()
.id("commit-modal")
.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()
.relative()
.bg(cx.theme().colors().elevated_surface_background)
.rounded(px(border_radius))
.border_1()
.border_color(cx.theme().colors().border)
.w(width)
.p(container_padding)
.child(
v_flex()
.id("editor-container")
.justify_between()
.p_2()
.size_full()
.gap_2()
.rounded(properties.editor_border_radius())
.overflow_hidden()
.cursor_text()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
.child(
div()
.flex_1()
.size_full()
.child(self.render_commit_editor(window, cx)),
)
.child(self.render_footer(window, cx)),
)
}
}