git_panel.rs

  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}