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