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