commit_modal.rs

  1// #![allow(unused, dead_code)]
  2
  3use crate::branch_picker::{self, BranchList};
  4use crate::git_panel::{commit_message_editor, GitPanel};
  5use git::{Commit, GenerateCommitMessage};
  6use panel::{panel_button, panel_editor_style, panel_filled_button};
  7use ui::{prelude::*, KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip};
  8
  9use editor::{Editor, EditorElement};
 10use gpui::*;
 11use util::ResultExt;
 12use workspace::{
 13    dock::{Dock, PanelHandle},
 14    ModalView, Workspace,
 15};
 16
 17// nate: It is a pain to get editors to size correctly and not overflow.
 18//
 19// this can get replaced with a simple flex layout with more time/a more thoughtful approach.
 20#[derive(Debug, Clone, Copy)]
 21pub struct ModalContainerProperties {
 22    pub modal_width: f32,
 23    pub editor_height: f32,
 24    pub footer_height: f32,
 25    pub container_padding: f32,
 26    pub modal_border_radius: f32,
 27}
 28
 29impl ModalContainerProperties {
 30    pub fn new(window: &Window, preferred_char_width: usize) -> Self {
 31        let container_padding = 5.0;
 32
 33        // Calculate width based on character width
 34        let mut modal_width = 460.0;
 35        let style = window.text_style().clone();
 36        let font_id = window.text_system().resolve_font(&style.font());
 37        let font_size = style.font_size.to_pixels(window.rem_size());
 38
 39        if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
 40            modal_width = preferred_char_width as f32 * em_width.0 + (container_padding * 2.0);
 41        }
 42
 43        Self {
 44            modal_width,
 45            editor_height: 300.0,
 46            footer_height: 24.0,
 47            container_padding,
 48            modal_border_radius: 12.0,
 49        }
 50    }
 51
 52    pub fn editor_border_radius(&self) -> Pixels {
 53        px(self.modal_border_radius - self.container_padding / 2.0)
 54    }
 55}
 56
 57pub struct CommitModal {
 58    git_panel: Entity<GitPanel>,
 59    commit_editor: Entity<Editor>,
 60    restore_dock: RestoreDock,
 61    properties: ModalContainerProperties,
 62    branch_list_handle: PopoverMenuHandle<BranchList>,
 63}
 64
 65impl Focusable for CommitModal {
 66    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 67        self.commit_editor.focus_handle(cx)
 68    }
 69}
 70
 71impl EventEmitter<DismissEvent> for CommitModal {}
 72impl ModalView for CommitModal {
 73    fn on_before_dismiss(
 74        &mut self,
 75        window: &mut Window,
 76        cx: &mut Context<Self>,
 77    ) -> workspace::DismissDecision {
 78        self.git_panel.update(cx, |git_panel, cx| {
 79            git_panel.set_modal_open(false, cx);
 80        });
 81        self.restore_dock
 82            .dock
 83            .update(cx, |dock, cx| {
 84                if let Some(active_index) = self.restore_dock.active_index {
 85                    dock.activate_panel(active_index, window, cx)
 86                }
 87                dock.set_open(self.restore_dock.is_open, window, cx)
 88            })
 89            .log_err();
 90        workspace::DismissDecision::Dismiss(true)
 91    }
 92}
 93
 94struct RestoreDock {
 95    dock: WeakEntity<Dock>,
 96    is_open: bool,
 97    active_index: Option<usize>,
 98}
 99
100impl CommitModal {
101    pub fn register(workspace: &mut Workspace) {
102        workspace.register_action(|workspace, _: &Commit, window, cx| {
103            CommitModal::toggle(workspace, window, cx);
104        });
105    }
106
107    pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<'_, Workspace>) {
108        let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
109            return;
110        };
111
112        git_panel.update(cx, |git_panel, cx| {
113            git_panel.set_modal_open(true, cx);
114        });
115
116        let dock = workspace.dock_at_position(git_panel.position(window, cx));
117        let is_open = dock.read(cx).is_open();
118        let active_index = dock.read(cx).active_panel_index();
119        let dock = dock.downgrade();
120        let restore_dock_position = RestoreDock {
121            dock,
122            is_open,
123            active_index,
124        };
125
126        workspace.open_panel::<GitPanel>(window, cx);
127        workspace.toggle_modal(window, cx, move |window, cx| {
128            CommitModal::new(git_panel, restore_dock_position, window, cx)
129        })
130    }
131
132    fn new(
133        git_panel: Entity<GitPanel>,
134        restore_dock: RestoreDock,
135        window: &mut Window,
136        cx: &mut Context<Self>,
137    ) -> Self {
138        let panel = git_panel.read(cx);
139        let suggested_commit_message = panel.suggest_commit_message(cx);
140
141        let commit_editor = git_panel.update(cx, |git_panel, cx| {
142            git_panel.set_modal_open(true, cx);
143            let buffer = git_panel.commit_message_buffer(cx).clone();
144            let panel_editor = git_panel.commit_editor.clone();
145            let project = git_panel.project.clone();
146
147            cx.new(|cx| {
148                let mut editor =
149                    commit_message_editor(buffer, None, project.clone(), false, window, cx);
150                editor.sync_selections(panel_editor, cx).detach();
151
152                editor
153            })
154        });
155
156        let commit_message = commit_editor.read(cx).text(cx);
157
158        if let Some(suggested_commit_message) = suggested_commit_message {
159            if commit_message.is_empty() {
160                commit_editor.update(cx, |editor, cx| {
161                    editor.set_placeholder_text(suggested_commit_message, cx);
162                });
163            }
164        }
165
166        let focus_handle = commit_editor.focus_handle(cx);
167
168        cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
169            if !this.branch_list_handle.is_focused(window, cx) {
170                cx.emit(DismissEvent);
171            }
172        })
173        .detach();
174
175        let properties = ModalContainerProperties::new(window, 50);
176
177        Self {
178            git_panel,
179            commit_editor,
180            restore_dock,
181            properties,
182            branch_list_handle: PopoverMenuHandle::default(),
183        }
184    }
185
186    fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
187        let editor_style = panel_editor_style(true, window, cx);
188        EditorElement::new(&self.commit_editor, editor_style)
189    }
190
191    pub fn render_commit_editor(
192        &self,
193        window: &mut Window,
194        cx: &mut Context<Self>,
195    ) -> impl IntoElement {
196        let properties = self.properties;
197        let padding_t = 3.0;
198        let padding_b = 6.0;
199        // magic number for editor not to overflow the container??
200        let extra_space_hack = 1.5 * window.line_height();
201
202        v_flex()
203            .h(px(properties.editor_height + padding_b + padding_t) + extra_space_hack)
204            .w_full()
205            .flex_none()
206            .rounded(properties.editor_border_radius())
207            .overflow_hidden()
208            .px_1p5()
209            .pt(px(padding_t))
210            .pb(px(padding_b))
211            .child(
212                div()
213                    .h(px(properties.editor_height))
214                    .w_full()
215                    .child(self.commit_editor_element(window, cx)),
216            )
217    }
218
219    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
220        let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
221            self.git_panel.update(cx, |git_panel, cx| {
222                let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
223                let title = git_panel.commit_button_title();
224                let co_authors = git_panel.render_co_authors(cx);
225                let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
226                let active_repo = git_panel.active_repository.clone();
227                (
228                    can_commit,
229                    tooltip,
230                    title,
231                    co_authors,
232                    generate_commit_message,
233                    active_repo,
234                )
235            });
236
237        let branch = active_repo
238            .as_ref()
239            .and_then(|repo| repo.read(cx).repository_entry.branch())
240            .map(|b| b.name.clone())
241            .unwrap_or_else(|| "<no branch>".into());
242
243        let branch_picker_button = panel_button(branch)
244            .icon(IconName::GitBranch)
245            .icon_size(IconSize::Small)
246            .icon_color(Color::Placeholder)
247            .color(Color::Muted)
248            .icon_position(IconPosition::Start)
249            .tooltip(Tooltip::for_action_title(
250                "Switch Branch",
251                &zed_actions::git::Branch,
252            ))
253            .on_click(cx.listener(|_, _, window, cx| {
254                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
255            }))
256            .style(ButtonStyle::Transparent);
257
258        let branch_picker = PopoverMenu::new("popover-button")
259            .menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
260            .with_handle(self.branch_list_handle.clone())
261            .trigger_with_tooltip(
262                branch_picker_button,
263                Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
264            )
265            .anchor(Corner::BottomLeft)
266            .offset(gpui::Point {
267                x: px(0.0),
268                y: px(-2.0),
269            });
270        let focus_handle = self.focus_handle(cx);
271
272        let close_kb_hint =
273            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
274                Some(
275                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
276                        .suffix("Cancel"),
277                )
278            } else {
279                None
280            };
281
282        let commit_button = panel_filled_button(commit_label)
283            .tooltip({
284                let panel_editor_focus_handle = focus_handle.clone();
285                move |window, cx| {
286                    Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
287                }
288            })
289            .disabled(!can_commit)
290            .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
291                telemetry::event!("Git Committed", source = "Git Modal");
292                this.git_panel
293                    .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
294                cx.emit(DismissEvent);
295            }));
296
297        h_flex()
298            .group("commit_editor_footer")
299            .flex_none()
300            .w_full()
301            .items_center()
302            .justify_between()
303            .w_full()
304            .h(px(self.properties.footer_height))
305            .gap_1()
306            .child(
307                h_flex()
308                    .gap_1()
309                    .flex_shrink()
310                    .overflow_x_hidden()
311                    .child(
312                        h_flex()
313                            .flex_shrink()
314                            .overflow_x_hidden()
315                            .child(branch_picker),
316                    )
317                    .children(generate_commit_message)
318                    .children(co_authors),
319            )
320            .child(div().flex_1())
321            .child(
322                h_flex()
323                    .items_center()
324                    .justify_end()
325                    .flex_none()
326                    .px_1()
327                    .gap_4()
328                    .children(close_kb_hint)
329                    .child(commit_button),
330            )
331    }
332
333    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
334        cx.emit(DismissEvent);
335    }
336
337    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
338        telemetry::event!("Git Committed", source = "Git Modal");
339        self.git_panel
340            .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
341        cx.emit(DismissEvent);
342    }
343
344    fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
345        if self.branch_list_handle.is_focused(window, cx) {
346            self.focus_handle(cx).focus(window)
347        } else {
348            self.branch_list_handle.toggle(window, cx);
349        }
350    }
351}
352
353impl Render for CommitModal {
354    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
355        let properties = self.properties;
356        let width = px(properties.modal_width);
357        let container_padding = px(properties.container_padding);
358        let border_radius = properties.modal_border_radius;
359        let editor_focus_handle = self.commit_editor.focus_handle(cx);
360
361        v_flex()
362            .id("commit-modal")
363            .key_context("GitCommit")
364            .on_action(cx.listener(Self::dismiss))
365            .on_action(cx.listener(Self::commit))
366            .on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
367                this.git_panel.update(cx, |panel, cx| {
368                    panel.generate_commit_message(cx);
369                })
370            }))
371            .on_action(
372                cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
373                    this.toggle_branch_selector(window, cx);
374                }),
375            )
376            .on_action(
377                cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
378                    this.toggle_branch_selector(window, cx);
379                }),
380            )
381            .on_action(
382                cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
383                    this.toggle_branch_selector(window, cx);
384                }),
385            )
386            .elevation_3(cx)
387            .overflow_hidden()
388            .flex_none()
389            .relative()
390            .bg(cx.theme().colors().elevated_surface_background)
391            .rounded(px(border_radius))
392            .border_1()
393            .border_color(cx.theme().colors().border)
394            .w(width)
395            .p(container_padding)
396            .child(
397                v_flex()
398                    .id("editor-container")
399                    .justify_between()
400                    .p_2()
401                    .size_full()
402                    .gap_2()
403                    .rounded(properties.editor_border_radius())
404                    .overflow_hidden()
405                    .cursor_text()
406                    .bg(cx.theme().colors().editor_background)
407                    .border_1()
408                    .border_color(cx.theme().colors().border_variant)
409                    .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
410                        window.focus(&editor_focus_handle);
411                    }))
412                    .child(
413                        div()
414                            .flex_1()
415                            .size_full()
416                            .child(self.render_commit_editor(window, cx)),
417                    )
418                    .child(self.render_footer(window, cx)),
419            )
420    }
421}