git_ui: Commit modal refinement (#25484)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Nate Butler 2025-02-24 14:19:06 -05:00 committed by GitHub
parent ceb7fc2cb2
commit 30af8d0a81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 510 additions and 146 deletions

View file

@ -4,13 +4,17 @@ use crate::git_panel::{commit_message_editor, GitPanel};
use crate::repository_selector::RepositorySelector;
use anyhow::Result;
use git::Commit;
use language::language_settings::LanguageSettings;
use language::Buffer;
use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
use panel::{
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, Tooltip};
use ui::{prelude::*, KeybindingHint, Tooltip};
use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
use gpui::*;
use project::git::Repository;
use project::{Fs, Project};
@ -18,6 +22,8 @@ use std::sync::Arc;
use workspace::dock::{Dock, DockPosition, PanelHandle};
use workspace::{ModalView, Workspace};
// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
@ -32,6 +38,8 @@ pub struct CommitModal {
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
current_suggestion: Option<usize>,
suggested_messages: Vec<SharedString>,
}
impl Focusable for CommitModal {
@ -114,6 +122,7 @@ impl CommitModal {
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);
@ -122,36 +131,276 @@ impl CommitModal {
cx.new(|cx| commit_message_editor(buffer, 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);
});
}
}
}
Self {
git_panel,
commit_editor,
restore_dock,
current_suggestion: None,
suggested_messages: vec![],
}
}
/// Returns container `(width, x padding, border radius)`
fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
// TODO: Let's set the width based on your set wrap guide if possible
// let settings = EditorSettings::get_global(cx);
// let first_wrap_guide = self
// .commit_editor
// .read(cx)
// .wrap_guides(cx)
// .iter()
// .next()
// .map(|(guide, active)| if *active { Some(*guide) } else { None })
// .flatten();
// let preferred_width = if let Some(guide) = first_wrap_guide {
// guide
// } else {
// 80
// };
let border_radius = 16.0;
let preferred_width = 50; // (chars wide)
let mut width = 460.0;
let padding_x = 16.0;
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
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());
let line_height = style.line_height_in_pixels(window.rem_size());
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
cx.notify();
}
// cx.notify();
(width, padding_x, border_radius)
}
// fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
// let new_index = match direction {
// Direction::Next => {
// (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
// }
// Direction::Prev => {
// (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
// .rem_euclid(self.suggested_messages.len())
// }
// };
// self.current_suggestion = Some(new_index);
// cx.notify();
// }
// fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(1);
// self.apply_suggestion(window, cx);
// }
// fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(0);
// self.apply_suggestion(window, cx);
// }
// fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
// self.commit_editor.update(cx, |editor, cx| {
// editor.set_text(message.to_string(), window, cx)
// });
// self.current_suggestion = Some(0);
// cx.notify();
// }
// fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// let suggested_messages = self.suggested_messages.clone();
// if let Some(suggestion) = self.current_suggestion {
// let suggested_message = &suggested_messages[suggestion];
// self.set_commit_message(suggested_message, window, cx);
// }
// cx.notify();
// }
fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
let mut editor = self.commit_editor.clone();
let editor_style = panel_editor_style(true, window, cx);
EditorElement::new(&self.commit_editor, editor_style)
}
pub fn render_commit_editor(
&self,
name_and_email: Option<(SharedString, SharedString)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let editor = self.commit_editor.clone();
let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let border_radius = modal_border_radius - padding_x / 2.0;
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.focus_handle(cx);
let settings = ThemeSettings::get_global(cx);
let line_height = relative(settings.buffer_line_height.value())
.to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
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());
let line_height = style.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size);
let (branch, 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).current_branch().map(|b| b.name.clone()))
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
} else {
"Commit changes to tracked files"
};
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
};
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
});
let branch_selector = 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 changes_count = self.git_panel.read(cx).total_staged_count();
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 fake_commit_kb =
ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
let commit_hint =
KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
.suffix(commit_label);
let focus_handle = self.focus_handle(cx);
// let next_suggestion_kb =
// ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
// let next_suggestion_hint = next_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
// });
// let prev_suggestion_kb =
// ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
// let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background)
// .suffix("Previous Suggestion")
// });
v_flex()
.justify_between()
.relative()
.w_full()
.h_full()
.pt_2()
.id("editor-container")
.bg(cx.theme().colors().editor_background)
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
.child(self.render_footer(window, cx))
.flex_1()
.size_full()
.rounded(px(border_radius))
.overflow_hidden()
.border_1()
.border_color(cx.theme().colors().border_variant)
.py_2()
.px_3()
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
.child(
div()
.size_full()
.flex_1()
.child(self.commit_editor_element(window, cx)),
)
.child(
h_flex()
.group("commit_editor_footer")
.flex_none()
.w_full()
.items_center()
.justify_between()
.w_full()
.pt_2()
.pb_0p5()
.gap_1()
.child(h_flex().gap_1().child(branch_selector).children(co_authors))
.child(div().flex_1())
.child(
h_flex()
.opacity(0.7)
.group_hover("commit_editor_footer", |this| this.opacity(1.0))
.items_center()
.justify_end()
.flex_none()
.px_1()
.gap_4()
.children(close_kb_hint)
// .children(next_suggestion_hint)
.child(commit_hint),
),
)
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@ -180,13 +429,10 @@ impl CommitModal {
(branch, tooltip, title, co_authors)
});
let branch_selector = Button::new("branch-selector", branch)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
let branch_selector = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
@ -197,13 +443,28 @@ impl CommitModal {
}))
.style(ButtonStyle::Transparent);
let changes_count = self.git_panel.read(cx).total_staged_count();
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
};
h_flex()
.items_center()
.h(px(36.0))
.w_full()
.justify_between()
.child(branch_selector)
.px_3()
.child(h_flex().child(branch_selector))
.child(
h_flex().children(co_authors).child(
panel_filled_button(title)
h_flex().gap_1p5().children(co_authors).child(
Button::new("stage-button", title)
.tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
.on_click(cx.listener(|this, _, window, cx| {
this.commit(&Default::default(), window, cx);
@ -212,6 +473,10 @@ impl CommitModal {
)
}
fn border_radius(&self) -> f32 {
8.0
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
@ -224,27 +489,33 @@ impl CommitModal {
impl Render for CommitModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let (width, _, border_radius) = self.container_properties(window, cx);
v_flex()
.id("commit-modal")
.key_context("GitCommit")
.elevation_3(cx)
.overflow_hidden()
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
// .on_action(cx.listener(Self::next_suggestion))
// .on_action(cx.listener(Self::prev_suggestion))
.relative()
.bg(cx.theme().colors().editor_background)
.rounded(px(16.))
.justify_between()
.bg(cx.theme().colors().elevated_surface_background)
.rounded(px(border_radius))
.border_1()
.border_color(cx.theme().colors().border)
.py_2()
.px_4()
.w(px(480.))
.min_h(rems(18.))
.w(px(width))
.h(px(360.))
.flex_1()
.overflow_hidden()
.child(
v_flex()
.flex_1()
.p_2()
.child(self.render_commit_editor(None, window, cx)),
)
// .child(self.render_footer(window, cx))
}
}

View file

@ -184,6 +184,7 @@ pub struct GitPanel {
pending_remote_operations: RemoteOperations,
pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
suggested_commit_message: Option<String>,
conflicted_count: usize,
conflicted_staged_count: usize,
current_modifiers: Modifiers,
@ -308,6 +309,7 @@ impl GitPanel {
remote_operation_id: 0,
active_repository,
commit_editor,
suggested_commit_message: None,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
@ -1038,6 +1040,10 @@ impl GitPanel {
.detach();
}
pub fn total_staged_count(&self) -> usize {
self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
}
pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
self.commit_editor
.read(cx)
@ -1200,6 +1206,57 @@ impl GitPanel {
self.pending_commit = Some(task);
}
/// Suggests a commit message based on the changed files and their statuses
pub fn suggest_commit_message(&self) -> Option<String> {
let entries = self
.entries
.iter()
.filter_map(|entry| {
if let GitListEntry::GitStatusEntry(status_entry) = entry {
Some(status_entry)
} else {
None
}
})
.collect::<Vec<&GitStatusEntry>>();
if entries.is_empty() {
None
} else if entries.len() == 1 {
let entry = &entries[0];
let file_name = entry
.repo_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
if entry.status.is_deleted() {
Some(format!("Delete {}", file_name))
} else if entry.status.is_created() {
Some(format!("Create {}", file_name))
} else if entry.status.is_modified() {
Some(format!("Update {}", file_name))
} else {
None
}
} else {
None
}
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_commit_message();
self.suggested_commit_message = suggested_commit_message.clone();
if let Some(suggested_commit_message) = suggested_commit_message {
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
});
}
cx.notify();
}
fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
let Some(repo) = self.active_repository.clone() else {
return;
@ -1444,6 +1501,7 @@ impl GitPanel {
git_panel.clear_pending();
}
git_panel.update_visible_entries(cx);
git_panel.update_editor_placeholder(cx);
})
.ok();
}