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}