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        let focus_handle = commit_editor.focus_handle(cx);
153
154        cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
155            cx.emit(DismissEvent);
156        })
157        .detach();
158
159        Self {
160            git_panel,
161            commit_editor,
162            restore_dock,
163            current_suggestion: None,
164            suggested_messages: vec![],
165        }
166    }
167
168    /// Returns container `(width, x padding, border radius)`
169    fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
170        // TODO: Let's set the width based on your set wrap guide if possible
171
172        // let settings = EditorSettings::get_global(cx);
173
174        // let first_wrap_guide = self
175        //     .commit_editor
176        //     .read(cx)
177        //     .wrap_guides(cx)
178        //     .iter()
179        //     .next()
180        //     .map(|(guide, active)| if *active { Some(*guide) } else { None })
181        //     .flatten();
182
183        // let preferred_width = if let Some(guide) = first_wrap_guide {
184        //     guide
185        // } else {
186        //     80
187        // };
188
189        let border_radius = 16.0;
190
191        let preferred_width = 50; // (chars wide)
192
193        let mut width = 460.0;
194        let padding_x = 16.0;
195
196        let mut snapshot = self
197            .commit_editor
198            .update(cx, |editor, cx| editor.snapshot(window, cx));
199        let style = window.text_style().clone();
200
201        let font_id = window.text_system().resolve_font(&style.font());
202        let font_size = style.font_size.to_pixels(window.rem_size());
203        let line_height = style.line_height_in_pixels(window.rem_size());
204        if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
205            width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
206            cx.notify();
207        }
208
209        // cx.notify();
210
211        (width, padding_x, border_radius)
212    }
213
214    // fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
215    //     let new_index = match direction {
216    //         Direction::Next => {
217    //             (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
218    //         }
219    //         Direction::Prev => {
220    //             (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
221    //                 .rem_euclid(self.suggested_messages.len())
222    //         }
223    //     };
224    //     self.current_suggestion = Some(new_index);
225
226    //     cx.notify();
227    // }
228
229    // fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
230    //     self.current_suggestion = Some(1);
231    //     self.apply_suggestion(window, cx);
232    // }
233
234    // fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
235    //     self.current_suggestion = Some(0);
236    //     self.apply_suggestion(window, cx);
237    // }
238
239    // fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
240    //     self.commit_editor.update(cx, |editor, cx| {
241    //         editor.set_text(message.to_string(), window, cx)
242    //     });
243    //     self.current_suggestion = Some(0);
244    //     cx.notify();
245    // }
246
247    // fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
248    //     let suggested_messages = self.suggested_messages.clone();
249
250    //     if let Some(suggestion) = self.current_suggestion {
251    //         let suggested_message = &suggested_messages[suggestion];
252
253    //         self.set_commit_message(suggested_message, window, cx);
254    //     }
255
256    //     cx.notify();
257    // }
258
259    fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
260        let mut editor = self.commit_editor.clone();
261
262        let editor_style = panel_editor_style(true, window, cx);
263
264        EditorElement::new(&self.commit_editor, editor_style)
265    }
266
267    pub fn render_commit_editor(
268        &self,
269        name_and_email: Option<(SharedString, SharedString)>,
270        window: &mut Window,
271        cx: &mut Context<Self>,
272    ) -> impl IntoElement {
273        let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
274
275        let border_radius = modal_border_radius - padding_x / 2.0;
276
277        let editor = self.commit_editor.clone();
278        let editor_focus_handle = editor.focus_handle(cx);
279
280        let settings = ThemeSettings::get_global(cx);
281        let line_height = relative(settings.buffer_line_height.value())
282            .to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
283
284        let mut snapshot = self
285            .commit_editor
286            .update(cx, |editor, cx| editor.snapshot(window, cx));
287        let style = window.text_style().clone();
288
289        let font_id = window.text_system().resolve_font(&style.font());
290        let font_size = style.font_size.to_pixels(window.rem_size());
291        let line_height = style.line_height_in_pixels(window.rem_size());
292        let em_width = window.text_system().em_width(font_id, font_size);
293
294        let (branch, tooltip, commit_label, co_authors) =
295            self.git_panel.update(cx, |git_panel, cx| {
296                let branch = git_panel
297                    .active_repository
298                    .as_ref()
299                    .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
300                    .unwrap_or_else(|| "<no branch>".into());
301                let tooltip = if git_panel.has_staged_changes() {
302                    "Commit staged changes"
303                } else {
304                    "Commit changes to tracked files"
305                };
306                let title = if git_panel.has_staged_changes() {
307                    "Commit"
308                } else {
309                    "Commit Tracked"
310                };
311                let co_authors = git_panel.render_co_authors(cx);
312                (branch, tooltip, title, co_authors)
313            });
314
315        let branch_selector = panel_button(branch)
316            .icon(IconName::GitBranch)
317            .icon_size(IconSize::Small)
318            .icon_color(Color::Placeholder)
319            .color(Color::Muted)
320            .icon_position(IconPosition::Start)
321            .tooltip(Tooltip::for_action_title(
322                "Switch Branch",
323                &zed_actions::git::Branch,
324            ))
325            .on_click(cx.listener(|_, _, window, cx| {
326                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
327            }))
328            .style(ButtonStyle::Transparent);
329
330        let changes_count = self.git_panel.read(cx).total_staged_count();
331
332        let close_kb_hint =
333            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
334                Some(
335                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
336                        .suffix("Cancel"),
337                )
338            } else {
339                None
340            };
341
342        let fake_commit_kb =
343            ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
344
345        let commit_hint =
346            KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
347                .suffix(commit_label);
348
349        let focus_handle = self.focus_handle(cx);
350
351        // let next_suggestion_kb =
352        //     ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
353        // let next_suggestion_hint = next_suggestion_kb.map(|kb| {
354        //     KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
355        // });
356
357        // let prev_suggestion_kb =
358        //     ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
359        // let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
360        //     KeybindingHint::new(kb, cx.theme().colors().editor_background)
361        //         .suffix("Previous Suggestion")
362        // });
363
364        v_flex()
365            .id("editor-container")
366            .bg(cx.theme().colors().editor_background)
367            .flex_1()
368            .size_full()
369            .rounded(px(border_radius))
370            .overflow_hidden()
371            .border_1()
372            .border_color(cx.theme().colors().border_variant)
373            .py_2()
374            .px_3()
375            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
376                window.focus(&editor_focus_handle);
377            }))
378            .child(
379                div()
380                    .size_full()
381                    .flex_1()
382                    .child(self.commit_editor_element(window, cx)),
383            )
384            .child(
385                h_flex()
386                    .group("commit_editor_footer")
387                    .flex_none()
388                    .w_full()
389                    .items_center()
390                    .justify_between()
391                    .w_full()
392                    .pt_2()
393                    .pb_0p5()
394                    .gap_1()
395                    .child(h_flex().gap_1().child(branch_selector).children(co_authors))
396                    .child(div().flex_1())
397                    .child(
398                        h_flex()
399                            .opacity(0.7)
400                            .group_hover("commit_editor_footer", |this| this.opacity(1.0))
401                            .items_center()
402                            .justify_end()
403                            .flex_none()
404                            .px_1()
405                            .gap_4()
406                            .children(close_kb_hint)
407                            // .children(next_suggestion_hint)
408                            .child(commit_hint),
409                    ),
410            )
411    }
412
413    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
414        let (branch, tooltip, title, co_authors) = self.git_panel.update(cx, |git_panel, cx| {
415            let branch = git_panel
416                .active_repository
417                .as_ref()
418                .and_then(|repo| {
419                    repo.read(cx)
420                        .repository_entry
421                        .branch()
422                        .map(|b| b.name.clone())
423                })
424                .unwrap_or_else(|| "<no branch>".into());
425            let tooltip = if git_panel.has_staged_changes() {
426                "Commit staged changes"
427            } else {
428                "Commit changes to tracked files"
429            };
430            let title = if git_panel.has_staged_changes() {
431                "Commit"
432            } else {
433                "Commit All"
434            };
435            let co_authors = git_panel.render_co_authors(cx);
436            (branch, tooltip, title, co_authors)
437        });
438
439        let branch_selector = panel_button(branch)
440            .icon(IconName::GitBranch)
441            .icon_size(IconSize::Small)
442            .icon_color(Color::Muted)
443            .icon_position(IconPosition::Start)
444            .tooltip(Tooltip::for_action_title(
445                "Switch Branch",
446                &zed_actions::git::Branch,
447            ))
448            .on_click(cx.listener(|_, _, window, cx| {
449                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
450            }))
451            .style(ButtonStyle::Transparent);
452
453        let changes_count = self.git_panel.read(cx).total_staged_count();
454
455        let close_kb_hint =
456            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
457                Some(
458                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
459                        .suffix("Cancel"),
460                )
461            } else {
462                None
463            };
464
465        h_flex()
466            .items_center()
467            .h(px(36.0))
468            .w_full()
469            .justify_between()
470            .px_3()
471            .child(h_flex().child(branch_selector))
472            .child(
473                h_flex().gap_1p5().children(co_authors).child(
474                    Button::new("stage-button", title)
475                        .tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
476                        .on_click(cx.listener(|this, _, window, cx| {
477                            this.commit(&Default::default(), window, cx);
478                        })),
479                ),
480            )
481    }
482
483    fn border_radius(&self) -> f32 {
484        8.0
485    }
486
487    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
488        cx.emit(DismissEvent);
489    }
490    fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
491        self.git_panel
492            .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
493        cx.emit(DismissEvent);
494    }
495}
496
497impl Render for CommitModal {
498    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
499        let (width, _, border_radius) = self.container_properties(window, cx);
500
501        v_flex()
502            .id("commit-modal")
503            .key_context("GitCommit")
504            .elevation_3(cx)
505            .overflow_hidden()
506            .on_action(cx.listener(Self::dismiss))
507            .on_action(cx.listener(Self::commit))
508            // .on_action(cx.listener(Self::next_suggestion))
509            // .on_action(cx.listener(Self::prev_suggestion))
510            .relative()
511            .justify_between()
512            .bg(cx.theme().colors().elevated_surface_background)
513            .rounded(px(border_radius))
514            .border_1()
515            .border_color(cx.theme().colors().border)
516            .w(px(width))
517            .h(px(360.))
518            .flex_1()
519            .overflow_hidden()
520            .child(
521                v_flex()
522                    .flex_1()
523                    .p_2()
524                    .child(self.render_commit_editor(None, window, cx)),
525            )
526        // .child(self.render_footer(window, cx))
527    }
528}