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