1use std::sync::Arc;
2use util::TryFutureExt;
3
4use db::kvp::KEY_VALUE_STORE;
5use gpui::*;
6use project::{Fs, Project};
7use serde::{Deserialize, Serialize};
8use settings::Settings as _;
9use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Tooltip};
10use workspace::dock::{DockPosition, Panel, PanelEvent};
11use workspace::Workspace;
12
13use crate::settings::GitPanelSettings;
14use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
15
16actions!(git_panel, [ToggleFocus]);
17
18const GIT_PANEL_KEY: &str = "GitPanel";
19
20pub fn init(cx: &mut AppContext) {
21 cx.observe_new_views(
22 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
23 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
24 workspace.toggle_panel_focus::<GitPanel>(cx);
25 });
26 },
27 )
28 .detach();
29}
30
31#[derive(Serialize, Deserialize)]
32struct SerializedGitPanel {
33 width: Option<Pixels>,
34}
35
36pub struct GitPanel {
37 _workspace: WeakView<Workspace>,
38 focus_handle: FocusHandle,
39 fs: Arc<dyn Fs>,
40 pending_serialization: Task<Option<()>>,
41 project: Model<Project>,
42 width: Option<Pixels>,
43
44 current_modifiers: Modifiers,
45}
46
47impl GitPanel {
48 pub fn load(
49 workspace: WeakView<Workspace>,
50 cx: AsyncWindowContext,
51 ) -> Task<Result<View<Self>>> {
52 cx.spawn(|mut cx| async move {
53 // Clippy incorrectly classifies this as a redundant closure
54 #[allow(clippy::redundant_closure)]
55 workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
56 })
57 }
58
59 pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
60 let project = workspace.project().clone();
61 let fs = workspace.app_state().fs.clone();
62 let weak_workspace = workspace.weak_handle();
63
64 cx.new_view(|cx| Self {
65 _workspace: weak_workspace,
66 focus_handle: cx.focus_handle(),
67 fs,
68 pending_serialization: Task::ready(None),
69 project,
70
71 current_modifiers: cx.modifiers(),
72
73 width: Some(px(360.)),
74 })
75 }
76
77 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
78 let width = self.width;
79 self.pending_serialization = cx.background_executor().spawn(
80 async move {
81 KEY_VALUE_STORE
82 .write_kvp(
83 GIT_PANEL_KEY.into(),
84 serde_json::to_string(&SerializedGitPanel { width })?,
85 )
86 .await?;
87 anyhow::Ok(())
88 }
89 .log_err(),
90 );
91 }
92
93 fn dispatch_context(&self) -> KeyContext {
94 let mut dispatch_context = KeyContext::new_with_defaults();
95 dispatch_context.add("GitPanel");
96 dispatch_context.add("menu");
97
98 dispatch_context
99 }
100
101 fn handle_modifiers_changed(
102 &mut self,
103 event: &ModifiersChangedEvent,
104 cx: &mut ViewContext<Self>,
105 ) {
106 self.current_modifiers = event.modifiers;
107 cx.notify();
108 }
109}
110
111impl GitPanel {
112 fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
113 // todo!(): Implement stage all
114 println!("Stage all triggered");
115 }
116
117 fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
118 // todo!(): Implement unstage all
119 println!("Unstage all triggered");
120 }
121
122 fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
123 // todo!(): Implement discard all
124 println!("Discard all triggered");
125 }
126
127 /// Commit all staged changes
128 fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
129 // todo!(): Implement commit all staged
130 println!("Commit staged changes triggered");
131 }
132
133 /// Commit all changes, regardless of whether they are staged or not
134 fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
135 // todo!(): Implement commit all changes
136 println!("Commit all changes triggered");
137 }
138
139 fn all_staged(&self) -> bool {
140 // todo!(): Implement all_staged
141 true
142 }
143}
144
145impl GitPanel {
146 pub fn panel_button(
147 &self,
148 id: impl Into<SharedString>,
149 label: impl Into<SharedString>,
150 ) -> Button {
151 let id = id.into().clone();
152 let label = label.into().clone();
153
154 Button::new(id, label)
155 .label_size(LabelSize::Small)
156 .layer(ElevationIndex::ElevatedSurface)
157 .size(ButtonSize::Compact)
158 .style(ButtonStyle::Filled)
159 }
160
161 pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
162 h_flex()
163 .items_center()
164 .h(px(8.))
165 .child(Divider::horizontal_dashed().color(DividerColor::Border))
166 }
167
168 pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
169 let focus_handle = self.focus_handle(cx).clone();
170
171 h_flex()
172 .h(px(32.))
173 .items_center()
174 .px_3()
175 .bg(ElevationIndex::Surface.bg(cx))
176 .child(
177 h_flex()
178 .gap_2()
179 .child(Checkbox::new("all-changes", true.into()).disabled(true))
180 .child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
181 )
182 .child(div().flex_grow())
183 .child(
184 h_flex()
185 .gap_2()
186 .child(
187 IconButton::new("discard-changes", IconName::Undo)
188 .tooltip(move |cx| {
189 let focus_handle = focus_handle.clone();
190
191 Tooltip::for_action_in(
192 "Discard all changes",
193 &DiscardAll,
194 &focus_handle,
195 cx,
196 )
197 })
198 .icon_size(IconSize::Small)
199 .disabled(true),
200 )
201 .child(if self.all_staged() {
202 self.panel_button("unstage-all", "Unstage All").on_click(
203 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
204 )
205 } else {
206 self.panel_button("stage-all", "Stage All").on_click(
207 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
208 )
209 }),
210 )
211 }
212
213 pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
214 let focus_handle_1 = self.focus_handle(cx).clone();
215 let focus_handle_2 = self.focus_handle(cx).clone();
216
217 let commit_staged_button = self
218 .panel_button("commit-staged-changes", "Commit")
219 .tooltip(move |cx| {
220 let focus_handle = focus_handle_1.clone();
221 Tooltip::for_action_in(
222 "Commit all staged changes",
223 &CommitStagedChanges,
224 &focus_handle,
225 cx,
226 )
227 })
228 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
229 this.commit_staged_changes(&CommitStagedChanges, cx)
230 }));
231
232 let commit_all_button = self
233 .panel_button("commit-all-changes", "Commit All")
234 .tooltip(move |cx| {
235 let focus_handle = focus_handle_2.clone();
236 Tooltip::for_action_in(
237 "Commit all changes, including unstaged changes",
238 &CommitAllChanges,
239 &focus_handle,
240 cx,
241 )
242 })
243 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
244 this.commit_all_changes(&CommitAllChanges, cx)
245 }));
246
247 div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
248 v_flex()
249 .h_full()
250 .py_2p5()
251 .px_3()
252 .bg(cx.theme().colors().editor_background)
253 .font_buffer(cx)
254 .text_ui_sm(cx)
255 .text_color(cx.theme().colors().text_muted)
256 .child("Add a message")
257 .gap_1()
258 .child(div().flex_grow())
259 .child(h_flex().child(div().gap_1().flex_grow()).child(
260 if self.current_modifiers.alt {
261 commit_all_button
262 } else {
263 commit_staged_button
264 },
265 ))
266 .cursor(CursorStyle::OperationNotAllowed)
267 .opacity(0.5),
268 )
269 }
270
271 fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
272 h_flex()
273 .h_full()
274 .flex_1()
275 .justify_center()
276 .items_center()
277 .child(
278 v_flex()
279 .gap_3()
280 .child("No changes to commit")
281 .text_ui_sm(cx)
282 .mx_auto()
283 .text_color(Color::Placeholder.color(cx)),
284 )
285 }
286}
287
288impl Render for GitPanel {
289 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
290 let project = self.project.read(cx);
291
292 v_flex()
293 .id("git_panel")
294 .key_context(self.dispatch_context())
295 .track_focus(&self.focus_handle)
296 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
297 .when(!project.is_read_only(cx), |this| {
298 this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
299 .on_action(
300 cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
301 )
302 .on_action(
303 cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
304 )
305 .on_action(cx.listener(|this, &CommitStagedChanges, cx| {
306 this.commit_staged_changes(&CommitStagedChanges, cx)
307 }))
308 .on_action(cx.listener(|this, &CommitAllChanges, cx| {
309 this.commit_all_changes(&CommitAllChanges, cx)
310 }))
311 })
312 .size_full()
313 .overflow_hidden()
314 .font_buffer(cx)
315 .py_1()
316 .bg(ElevationIndex::Surface.bg(cx))
317 .child(self.render_panel_header(cx))
318 .child(self.render_divider(cx))
319 .child(self.render_empty_state(cx))
320 .child(self.render_divider(cx))
321 .child(self.render_commit_editor(cx))
322 }
323}
324
325impl FocusableView for GitPanel {
326 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
327 self.focus_handle.clone()
328 }
329}
330
331impl EventEmitter<PanelEvent> for GitPanel {}
332
333impl Panel for GitPanel {
334 fn persistent_name() -> &'static str {
335 "GitPanel"
336 }
337
338 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
339 GitPanelSettings::get_global(cx).dock
340 }
341
342 fn position_is_valid(&self, position: DockPosition) -> bool {
343 matches!(position, DockPosition::Left | DockPosition::Right)
344 }
345
346 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
347 settings::update_settings_file::<GitPanelSettings>(
348 self.fs.clone(),
349 cx,
350 move |settings, _| settings.dock = Some(position),
351 );
352 }
353
354 fn size(&self, cx: &gpui::WindowContext) -> Pixels {
355 self.width
356 .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
357 }
358
359 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
360 self.width = size;
361 self.serialize(cx);
362 cx.notify();
363 }
364
365 fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
366 Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
367 }
368
369 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
370 Some("Git Panel")
371 }
372
373 fn toggle_action(&self) -> Box<dyn Action> {
374 Box::new(ToggleFocus)
375 }
376}