git_panel.rs

  1use std::sync::Arc;
  2use util::TryFutureExt;
  3
  4use db::kvp::KEY_VALUE_STORE;
  5use gpui::*;
  6use project::Fs;
  7use serde::{Deserialize, Serialize};
  8use settings::Settings as _;
  9use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
 10use workspace::dock::{DockPosition, Panel, PanelEvent};
 11use workspace::Workspace;
 12
 13use crate::settings::GitPanelSettings;
 14
 15const GIT_PANEL_KEY: &str = "GitPanel";
 16
 17pub fn init(cx: &mut AppContext) {
 18    cx.observe_new_views(
 19        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
 20            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 21                workspace.toggle_panel_focus::<GitPanel>(cx);
 22            });
 23        },
 24    )
 25    .detach();
 26}
 27
 28#[derive(Serialize, Deserialize)]
 29struct SerializedGitPanel {
 30    width: Option<Pixels>,
 31}
 32
 33actions!(git_panel, [Deploy, ToggleFocus]);
 34
 35pub struct GitPanel {
 36    _workspace: WeakView<Workspace>,
 37    pending_serialization: Task<Option<()>>,
 38    fs: Arc<dyn Fs>,
 39    focus_handle: FocusHandle,
 40    width: Option<Pixels>,
 41}
 42
 43impl GitPanel {
 44    pub fn load(
 45        workspace: WeakView<Workspace>,
 46        cx: AsyncWindowContext,
 47    ) -> Task<Result<View<Self>>> {
 48        cx.spawn(|mut cx| async move {
 49            // Clippy incorrectly classifies this as a redundant closure
 50            #[allow(clippy::redundant_closure)]
 51            workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
 52        })
 53    }
 54
 55    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 56        let fs = workspace.app_state().fs.clone();
 57        let weak_workspace = workspace.weak_handle();
 58
 59        cx.new_view(|cx| Self {
 60            fs,
 61            _workspace: weak_workspace,
 62            pending_serialization: Task::ready(None),
 63            focus_handle: cx.focus_handle(),
 64            width: Some(px(360.)),
 65        })
 66    }
 67
 68    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 69        let width = self.width;
 70        self.pending_serialization = cx.background_executor().spawn(
 71            async move {
 72                KEY_VALUE_STORE
 73                    .write_kvp(
 74                        GIT_PANEL_KEY.into(),
 75                        serde_json::to_string(&SerializedGitPanel { width })?,
 76                    )
 77                    .await?;
 78                anyhow::Ok(())
 79            }
 80            .log_err(),
 81        );
 82    }
 83
 84    pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 85        h_flex()
 86            .h(px(32.))
 87            .items_center()
 88            .px_3()
 89            .bg(ElevationIndex::Surface.bg(cx))
 90            .child(
 91                h_flex()
 92                    .gap_2()
 93                    .child(Checkbox::new("all-changes", true.into()).disabled(true))
 94                    .child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
 95            )
 96            .child(div().flex_grow())
 97            .child(
 98                h_flex()
 99                    .gap_2()
100                    .child(
101                        IconButton::new("discard-changes", IconName::Undo)
102                            .icon_size(IconSize::Small)
103                            .disabled(true),
104                    )
105                    .child(
106                        Button::new("stage-all", "Stage All")
107                            .label_size(LabelSize::Small)
108                            .layer(ElevationIndex::ElevatedSurface)
109                            .size(ButtonSize::Compact)
110                            .style(ButtonStyle::Filled)
111                            .disabled(true),
112                    ),
113            )
114    }
115
116    pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
117        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
118            v_flex()
119                .h_full()
120                .py_2p5()
121                .px_3()
122                .bg(cx.theme().colors().editor_background)
123                .font_buffer(cx)
124                .text_ui_sm(cx)
125                .text_color(cx.theme().colors().text_muted)
126                .child("Add a message")
127                .gap_1()
128                .child(div().flex_grow())
129                .child(
130                    h_flex().child(div().gap_1().flex_grow()).child(
131                        Button::new("commit", "Commit")
132                            .label_size(LabelSize::Small)
133                            .layer(ElevationIndex::ElevatedSurface)
134                            .size(ButtonSize::Compact)
135                            .style(ButtonStyle::Filled)
136                            .disabled(true),
137                    ),
138                )
139                .cursor(CursorStyle::OperationNotAllowed)
140                .opacity(0.5),
141        )
142    }
143
144    fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
145        h_flex()
146            .h_full()
147            .flex_1()
148            .justify_center()
149            .items_center()
150            .child(
151                v_flex()
152                    .gap_3()
153                    .child("No changes to commit")
154                    .text_ui_sm(cx)
155                    .mx_auto()
156                    .text_color(Color::Placeholder.color(cx)),
157            )
158    }
159}
160
161impl Render for GitPanel {
162    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
163        v_flex()
164            .key_context("GitPanel")
165            .font_buffer(cx)
166            .py_1()
167            .id("git_panel")
168            .track_focus(&self.focus_handle)
169            .size_full()
170            .overflow_hidden()
171            .bg(ElevationIndex::Surface.bg(cx))
172            .child(self.render_panel_header(cx))
173            .child(
174                h_flex()
175                    .items_center()
176                    .h(px(8.))
177                    .child(Divider::horizontal_dashed().color(DividerColor::Border)),
178            )
179            .child(self.render_empty_state(cx))
180            .child(
181                h_flex()
182                    .items_center()
183                    .h(px(8.))
184                    .child(Divider::horizontal_dashed().color(DividerColor::Border)),
185            )
186            .child(self.render_commit_editor(cx))
187    }
188}
189
190impl FocusableView for GitPanel {
191    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
192        self.focus_handle.clone()
193    }
194}
195
196impl EventEmitter<PanelEvent> for GitPanel {}
197
198impl Panel for GitPanel {
199    fn persistent_name() -> &'static str {
200        "GitPanel"
201    }
202
203    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
204        GitPanelSettings::get_global(cx).dock
205    }
206
207    fn position_is_valid(&self, position: DockPosition) -> bool {
208        matches!(position, DockPosition::Left | DockPosition::Right)
209    }
210
211    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
212        settings::update_settings_file::<GitPanelSettings>(
213            self.fs.clone(),
214            cx,
215            move |settings, _| settings.dock = Some(position),
216        );
217    }
218
219    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
220        self.width
221            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
222    }
223
224    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
225        self.width = size;
226        self.serialize(cx);
227        cx.notify();
228    }
229
230    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
231        Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
232    }
233
234    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
235        Some("Git Panel")
236    }
237
238    fn toggle_action(&self) -> Box<dyn Action> {
239        Box::new(ToggleFocus)
240    }
241}