commit_modal.rs

  1#![allow(unused, dead_code)]
  2
  3use crate::git_panel::{commit_message_editor, GitPanel};
  4use crate::repository_selector::RepositorySelector;
  5use anyhow::Result;
  6use git::Commit;
  7use language::language_settings::LanguageSettings;
  8use language::Buffer;
  9use panel::{
 10    panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
 11    panel_icon_button,
 12};
 13use settings::Settings;
 14use theme::ThemeSettings;
 15use ui::{prelude::*, KeybindingHint, Tooltip};
 16
 17use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
 18use gpui::*;
 19use project::git::Repository;
 20use project::{Fs, Project};
 21use std::sync::Arc;
 22use workspace::dock::{Dock, DockPosition, PanelHandle};
 23use workspace::{ModalView, Workspace};
 24
 25// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
 26
 27pub fn init(cx: &mut App) {
 28    cx.observe_new(|workspace: &mut Workspace, window, cx| {
 29        let Some(window) = window else {
 30            return;
 31        };
 32        CommitModal::register(workspace, window, cx)
 33    })
 34    .detach();
 35}
 36
 37pub struct CommitModal {
 38    git_panel: Entity<GitPanel>,
 39    commit_editor: Entity<Editor>,
 40    restore_dock: RestoreDock,
 41    current_suggestion: Option<usize>,
 42    suggested_messages: Vec<SharedString>,
 43}
 44
 45impl Focusable for CommitModal {
 46    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 47        self.commit_editor.focus_handle(cx)
 48    }
 49}
 50
 51impl EventEmitter<DismissEvent> for CommitModal {}
 52impl ModalView for CommitModal {
 53    fn on_before_dismiss(
 54        &mut self,
 55        window: &mut Window,
 56        cx: &mut Context<Self>,
 57    ) -> workspace::DismissDecision {
 58        self.git_panel.update(cx, |git_panel, cx| {
 59            git_panel.set_modal_open(false, cx);
 60        });
 61        self.restore_dock.dock.update(cx, |dock, cx| {
 62            if let Some(active_index) = self.restore_dock.active_index {
 63                dock.activate_panel(active_index, window, cx)
 64            }
 65            dock.set_open(self.restore_dock.is_open, window, cx)
 66        });
 67        workspace::DismissDecision::Dismiss(true)
 68    }
 69}
 70
 71struct RestoreDock {
 72    dock: WeakEntity<Dock>,
 73    is_open: bool,
 74    active_index: Option<usize>,
 75}
 76
 77impl CommitModal {
 78    pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
 79        workspace.register_action(|workspace, _: &Commit, window, cx| {
 80            let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
 81                return;
 82            };
 83
 84            let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
 85                let can_commit = git_panel.can_commit();
 86                let conflict = git_panel.has_unstaged_conflicts();
 87                (can_commit, conflict)
 88            });
 89            if !can_commit {
 90                let message = if conflict {
 91                    "There are still conflicts. You must stage these before committing."
 92                } else {
 93                    "No changes to commit."
 94                };
 95                let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx);
 96                cx.spawn(|_, _| async move {
 97                    prompt.await.ok();
 98                })
 99                .detach();
100            }
101
102            let dock = workspace.dock_at_position(git_panel.position(window, cx));
103            let is_open = dock.read(cx).is_open();
104            let active_index = dock.read(cx).active_panel_index();
105            let dock = dock.downgrade();
106            let restore_dock_position = RestoreDock {
107                dock,
108                is_open,
109                active_index,
110            };
111            workspace.open_panel::<GitPanel>(window, cx);
112            workspace.toggle_modal(window, cx, move |window, cx| {
113                CommitModal::new(git_panel, restore_dock_position, window, cx)
114            })
115        });
116    }
117
118    fn new(
119        git_panel: Entity<GitPanel>,
120        restore_dock: RestoreDock,
121        window: &mut Window,
122        cx: &mut Context<Self>,
123    ) -> Self {
124        let panel = git_panel.read(cx);
125        let suggested_message = panel.suggest_commit_message();
126
127        let commit_editor = git_panel.update(cx, |git_panel, cx| {
128            git_panel.set_modal_open(true, cx);
129            let buffer = git_panel.commit_message_buffer(cx).clone();
130            let project = git_panel.project.clone();
131            cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
132        });
133
134        let commit_message = commit_editor.read(cx).text(cx);
135
136        if let Some(suggested_message) = suggested_message {
137            if commit_message.is_empty() {
138                commit_editor.update(cx, |editor, cx| {
139                    editor.set_text(suggested_message, window, cx);
140                    editor.select_all(&Default::default(), window, cx);
141                });
142            } else {
143                if commit_message.as_str().trim() == suggested_message.trim() {
144                    commit_editor.update(cx, |editor, cx| {
145                        // select the message to make it easy to delete
146                        editor.select_all(&Default::default(), window, cx);
147                    });
148                }
149            }
150        }
151
152        Self {
153            git_panel,
154            commit_editor,
155            restore_dock,
156            current_suggestion: None,
157            suggested_messages: vec![],
158        }
159    }
160
161    /// Returns container `(width, x padding, border radius)`
162    fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
163        // TODO: Let's set the width based on your set wrap guide if possible
164
165        // let settings = EditorSettings::get_global(cx);
166
167        // let first_wrap_guide = self
168        //     .commit_editor
169        //     .read(cx)
170        //     .wrap_guides(cx)
171        //     .iter()
172        //     .next()
173        //     .map(|(guide, active)| if *active { Some(*guide) } else { None })
174        //     .flatten();
175
176        // let preferred_width = if let Some(guide) = first_wrap_guide {
177        //     guide
178        // } else {
179        //     80
180        // };
181
182        let border_radius = 16.0;
183
184        let preferred_width = 50; // (chars wide)
185
186        let mut width = 460.0;
187        let padding_x = 16.0;
188
189        let mut snapshot = self
190            .commit_editor
191            .update(cx, |editor, cx| editor.snapshot(window, cx));
192        let style = window.text_style().clone();
193
194        let font_id = window.text_system().resolve_font(&style.font());
195        let font_size = style.font_size.to_pixels(window.rem_size());
196        let line_height = style.line_height_in_pixels(window.rem_size());
197        if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
198            width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
199            cx.notify();
200        }
201
202        // cx.notify();
203
204        (width, padding_x, border_radius)
205    }
206
207    // fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
208    //     let new_index = match direction {
209    //         Direction::Next => {
210    //             (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
211    //         }
212    //         Direction::Prev => {
213    //             (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
214    //                 .rem_euclid(self.suggested_messages.len())
215    //         }
216    //     };
217    //     self.current_suggestion = Some(new_index);
218
219    //     cx.notify();
220    // }
221
222    // fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
223    //     self.current_suggestion = Some(1);
224    //     self.apply_suggestion(window, cx);
225    // }
226
227    // fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
228    //     self.current_suggestion = Some(0);
229    //     self.apply_suggestion(window, cx);
230    // }
231
232    // fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
233    //     self.commit_editor.update(cx, |editor, cx| {
234    //         editor.set_text(message.to_string(), window, cx)
235    //     });
236    //     self.current_suggestion = Some(0);
237    //     cx.notify();
238    // }
239
240    // fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
241    //     let suggested_messages = self.suggested_messages.clone();
242
243    //     if let Some(suggestion) = self.current_suggestion {
244    //         let suggested_message = &suggested_messages[suggestion];
245
246    //         self.set_commit_message(suggested_message, window, cx);
247    //     }
248
249    //     cx.notify();
250    // }
251
252    fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
253        let mut editor = self.commit_editor.clone();
254
255        let editor_style = panel_editor_style(true, window, cx);
256
257        EditorElement::new(&self.commit_editor, editor_style)
258    }
259
260    pub fn render_commit_editor(
261        &self,
262        name_and_email: Option<(SharedString, SharedString)>,
263        window: &mut Window,
264        cx: &mut Context<Self>,
265    ) -> impl IntoElement {
266        let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
267
268        let border_radius = modal_border_radius - padding_x / 2.0;
269
270        let editor = self.commit_editor.clone();
271        let editor_focus_handle = editor.focus_handle(cx);
272
273        let settings = ThemeSettings::get_global(cx);
274        let line_height = relative(settings.buffer_line_height.value())
275            .to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
276
277        let mut snapshot = self
278            .commit_editor
279            .update(cx, |editor, cx| editor.snapshot(window, cx));
280        let style = window.text_style().clone();
281
282        let font_id = window.text_system().resolve_font(&style.font());
283        let font_size = style.font_size.to_pixels(window.rem_size());
284        let line_height = style.line_height_in_pixels(window.rem_size());
285        let em_width = window.text_system().em_width(font_id, font_size);
286
287        let (branch, tooltip, commit_label, co_authors) =
288            self.git_panel.update(cx, |git_panel, cx| {
289                let branch = git_panel
290                    .active_repository
291                    .as_ref()
292                    .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
293                    .unwrap_or_else(|| "<no branch>".into());
294                let tooltip = if git_panel.has_staged_changes() {
295                    "Commit staged changes"
296                } else {
297                    "Commit changes to tracked files"
298                };
299                let title = if git_panel.has_staged_changes() {
300                    "Commit"
301                } else {
302                    "Commit Tracked"
303                };
304                let co_authors = git_panel.render_co_authors(cx);
305                (branch, tooltip, title, co_authors)
306            });
307
308        let branch_selector = panel_button(branch)
309            .icon(IconName::GitBranch)
310            .icon_size(IconSize::Small)
311            .icon_color(Color::Placeholder)
312            .color(Color::Muted)
313            .icon_position(IconPosition::Start)
314            .tooltip(Tooltip::for_action_title(
315                "Switch Branch",
316                &zed_actions::git::Branch,
317            ))
318            .on_click(cx.listener(|_, _, window, cx| {
319                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
320            }))
321            .style(ButtonStyle::Transparent);
322
323        let changes_count = self.git_panel.read(cx).total_staged_count();
324
325        let close_kb_hint =
326            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
327                Some(
328                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
329                        .suffix("Cancel"),
330                )
331            } else {
332                None
333            };
334
335        let fake_commit_kb =
336            ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
337
338        let commit_hint =
339            KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
340                .suffix(commit_label);
341
342        let focus_handle = self.focus_handle(cx);
343
344        // let next_suggestion_kb =
345        //     ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
346        // let next_suggestion_hint = next_suggestion_kb.map(|kb| {
347        //     KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
348        // });
349
350        // let prev_suggestion_kb =
351        //     ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
352        // let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
353        //     KeybindingHint::new(kb, cx.theme().colors().editor_background)
354        //         .suffix("Previous Suggestion")
355        // });
356
357        v_flex()
358            .id("editor-container")
359            .bg(cx.theme().colors().editor_background)
360            .flex_1()
361            .size_full()
362            .rounded(px(border_radius))
363            .overflow_hidden()
364            .border_1()
365            .border_color(cx.theme().colors().border_variant)
366            .py_2()
367            .px_3()
368            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
369                window.focus(&editor_focus_handle);
370            }))
371            .child(
372                div()
373                    .size_full()
374                    .flex_1()
375                    .child(self.commit_editor_element(window, cx)),
376            )
377            .child(
378                h_flex()
379                    .group("commit_editor_footer")
380                    .flex_none()
381                    .w_full()
382                    .items_center()
383                    .justify_between()
384                    .w_full()
385                    .pt_2()
386                    .pb_0p5()
387                    .gap_1()
388                    .child(h_flex().gap_1().child(branch_selector).children(co_authors))
389                    .child(div().flex_1())
390                    .child(
391                        h_flex()
392                            .opacity(0.7)
393                            .group_hover("commit_editor_footer", |this| this.opacity(1.0))
394                            .items_center()
395                            .justify_end()
396                            .flex_none()
397                            .px_1()
398                            .gap_4()
399                            .children(close_kb_hint)
400                            // .children(next_suggestion_hint)
401                            .child(commit_hint),
402                    ),
403            )
404    }
405
406    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
407        let (branch, tooltip, title, co_authors) = self.git_panel.update(cx, |git_panel, cx| {
408            let branch = git_panel
409                .active_repository
410                .as_ref()
411                .and_then(|repo| {
412                    repo.read(cx)
413                        .repository_entry
414                        .branch()
415                        .map(|b| b.name.clone())
416                })
417                .unwrap_or_else(|| "<no branch>".into());
418            let tooltip = if git_panel.has_staged_changes() {
419                "Commit staged changes"
420            } else {
421                "Commit changes to tracked files"
422            };
423            let title = if git_panel.has_staged_changes() {
424                "Commit"
425            } else {
426                "Commit All"
427            };
428            let co_authors = git_panel.render_co_authors(cx);
429            (branch, tooltip, title, co_authors)
430        });
431
432        let branch_selector = panel_button(branch)
433            .icon(IconName::GitBranch)
434            .icon_size(IconSize::Small)
435            .icon_color(Color::Muted)
436            .icon_position(IconPosition::Start)
437            .tooltip(Tooltip::for_action_title(
438                "Switch Branch",
439                &zed_actions::git::Branch,
440            ))
441            .on_click(cx.listener(|_, _, window, cx| {
442                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
443            }))
444            .style(ButtonStyle::Transparent);
445
446        let changes_count = self.git_panel.read(cx).total_staged_count();
447
448        let close_kb_hint =
449            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
450                Some(
451                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
452                        .suffix("Cancel"),
453                )
454            } else {
455                None
456            };
457
458        h_flex()
459            .items_center()
460            .h(px(36.0))
461            .w_full()
462            .justify_between()
463            .px_3()
464            .child(h_flex().child(branch_selector))
465            .child(
466                h_flex().gap_1p5().children(co_authors).child(
467                    Button::new("stage-button", title)
468                        .tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
469                        .on_click(cx.listener(|this, _, window, cx| {
470                            this.commit(&Default::default(), window, cx);
471                        })),
472                ),
473            )
474    }
475
476    fn border_radius(&self) -> f32 {
477        8.0
478    }
479
480    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
481        cx.emit(DismissEvent);
482    }
483    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
484        self.git_panel
485            .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
486        cx.emit(DismissEvent);
487    }
488}
489
490impl Render for CommitModal {
491    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
492        let (width, _, border_radius) = self.container_properties(window, cx);
493
494        v_flex()
495            .id("commit-modal")
496            .key_context("GitCommit")
497            .elevation_3(cx)
498            .overflow_hidden()
499            .on_action(cx.listener(Self::dismiss))
500            .on_action(cx.listener(Self::commit))
501            // .on_action(cx.listener(Self::next_suggestion))
502            // .on_action(cx.listener(Self::prev_suggestion))
503            .relative()
504            .justify_between()
505            .bg(cx.theme().colors().elevated_surface_background)
506            .rounded(px(border_radius))
507            .border_1()
508            .border_color(cx.theme().colors().border)
509            .w(px(width))
510            .h(px(360.))
511            .flex_1()
512            .overflow_hidden()
513            .child(
514                v_flex()
515                    .flex_1()
516                    .p_2()
517                    .child(self.render_commit_editor(None, window, cx)),
518            )
519        // .child(self.render_footer(window, cx))
520    }
521}