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}