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}