1use anyhow::Result;
2use collections::HashMap;
3use db::kvp::KEY_VALUE_STORE;
4use git::repository::GitFileStatus;
5use gpui::{
6 actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
7 CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
8 ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
9 MouseButton, Stateful, Task, UniformListScrollHandle, View, WeakView,
10};
11use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
12use serde::{Deserialize, Serialize};
13use settings::Settings as _;
14use std::{
15 cell::OnceCell,
16 collections::HashSet,
17 ffi::OsStr,
18 ops::Range,
19 path::{Path, PathBuf},
20 sync::Arc,
21 time::Duration,
22};
23use ui::{
24 prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
25};
26use util::{ResultExt, TryFutureExt};
27use workspace::dock::{DockPosition, Panel, PanelEvent};
28use workspace::Workspace;
29
30use crate::{git_status_icon, settings::GitPanelSettings};
31use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
32
33actions!(git_panel, [ToggleFocus]);
34
35const GIT_PANEL_KEY: &str = "GitPanel";
36
37pub fn init(cx: &mut AppContext) {
38 cx.observe_new_views(
39 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
40 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
41 workspace.toggle_panel_focus::<GitPanel>(cx);
42 });
43 },
44 )
45 .detach();
46}
47
48#[derive(Debug)]
49pub enum Event {
50 Focus,
51}
52
53pub struct GitStatusEntry {}
54
55#[derive(Debug, PartialEq, Eq, Clone)]
56struct EntryDetails {
57 filename: String,
58 display_name: String,
59 path: Arc<Path>,
60 kind: EntryKind,
61 depth: usize,
62 is_expanded: bool,
63 status: Option<GitFileStatus>,
64}
65
66impl EntryDetails {
67 pub fn is_dir(&self) -> bool {
68 self.kind.is_dir()
69 }
70}
71
72#[derive(Serialize, Deserialize)]
73struct SerializedGitPanel {
74 width: Option<Pixels>,
75}
76
77pub struct GitPanel {
78 _workspace: WeakView<Workspace>,
79 current_modifiers: Modifiers,
80 focus_handle: FocusHandle,
81 fs: Arc<dyn Fs>,
82 hide_scrollbar_task: Option<Task<()>>,
83 pending_serialization: Task<Option<()>>,
84 project: Model<Project>,
85 scroll_handle: UniformListScrollHandle,
86 scrollbar_state: ScrollbarState,
87 selected_item: Option<usize>,
88 show_scrollbar: bool,
89 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
90
91 // The entries that are currently shown in the panel, aka
92 // not hidden by folding or such
93 visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
94 width: Option<Pixels>,
95}
96
97impl GitPanel {
98 pub fn load(
99 workspace: WeakView<Workspace>,
100 cx: AsyncWindowContext,
101 ) -> Task<Result<View<Self>>> {
102 cx.spawn(|mut cx| async move {
103 // Clippy incorrectly classifies this as a redundant closure
104 #[allow(clippy::redundant_closure)]
105 workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
106 })
107 }
108
109 pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
110 let fs = workspace.app_state().fs.clone();
111 let weak_workspace = workspace.weak_handle();
112 let project = workspace.project().clone();
113
114 let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
115 let focus_handle = cx.focus_handle();
116 cx.on_focus(&focus_handle, Self::focus_in).detach();
117 cx.on_focus_out(&focus_handle, |this, _, cx| {
118 this.hide_scrollbar(cx);
119 })
120 .detach();
121 cx.subscribe(&project, |this, _project, event, cx| match event {
122 project::Event::WorktreeRemoved(id) => {
123 this.expanded_dir_ids.remove(id);
124 this.update_visible_entries(None, cx);
125 cx.notify();
126 }
127 project::Event::WorktreeUpdatedEntries(_, _)
128 | project::Event::WorktreeAdded(_)
129 | project::Event::WorktreeOrderChanged => {
130 this.update_visible_entries(None, cx);
131 cx.notify();
132 }
133 _ => {}
134 })
135 .detach();
136
137 let scroll_handle = UniformListScrollHandle::new();
138
139 let mut this = Self {
140 _workspace: weak_workspace,
141 focus_handle: cx.focus_handle(),
142 fs,
143 pending_serialization: Task::ready(None),
144 project,
145 visible_entries: Vec::new(),
146 current_modifiers: cx.modifiers(),
147 expanded_dir_ids: Default::default(),
148
149 width: Some(px(360.)),
150 scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
151 scroll_handle,
152 selected_item: None,
153 show_scrollbar: !Self::should_autohide_scrollbar(cx),
154 hide_scrollbar_task: None,
155 };
156 this.update_visible_entries(None, cx);
157 this
158 });
159
160 git_panel
161 }
162
163 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
164 let width = self.width;
165 self.pending_serialization = cx.background_executor().spawn(
166 async move {
167 KEY_VALUE_STORE
168 .write_kvp(
169 GIT_PANEL_KEY.into(),
170 serde_json::to_string(&SerializedGitPanel { width })?,
171 )
172 .await?;
173 anyhow::Ok(())
174 }
175 .log_err(),
176 );
177 }
178
179 fn dispatch_context(&self) -> KeyContext {
180 let mut dispatch_context = KeyContext::new_with_defaults();
181 dispatch_context.add("GitPanel");
182 dispatch_context.add("menu");
183
184 dispatch_context
185 }
186
187 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
188 if !self.focus_handle.contains_focused(cx) {
189 cx.emit(Event::Focus);
190 }
191 }
192
193 fn should_show_scrollbar(_cx: &AppContext) -> bool {
194 // TODO: plug into settings
195 true
196 }
197
198 fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
199 // TODO: plug into settings
200 true
201 }
202
203 fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
204 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
205 if !Self::should_autohide_scrollbar(cx) {
206 return;
207 }
208 self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
209 cx.background_executor()
210 .timer(SCROLLBAR_SHOW_INTERVAL)
211 .await;
212 panel
213 .update(&mut cx, |panel, cx| {
214 panel.show_scrollbar = false;
215 cx.notify();
216 })
217 .log_err();
218 }))
219 }
220
221 fn handle_modifiers_changed(
222 &mut self,
223 event: &ModifiersChangedEvent,
224 cx: &mut ViewContext<Self>,
225 ) {
226 self.current_modifiers = event.modifiers;
227 cx.notify();
228 }
229
230 fn calculate_depth_and_difference(
231 entry: &Entry,
232 visible_worktree_entries: &HashSet<Arc<Path>>,
233 ) -> (usize, usize) {
234 let (depth, difference) = entry
235 .path
236 .ancestors()
237 .skip(1) // Skip the entry itself
238 .find_map(|ancestor| {
239 if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
240 let entry_path_components_count = entry.path.components().count();
241 let parent_path_components_count = parent_entry.components().count();
242 let difference = entry_path_components_count - parent_path_components_count;
243 let depth = parent_entry
244 .ancestors()
245 .skip(1)
246 .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
247 .count();
248 Some((depth + 1, difference))
249 } else {
250 None
251 }
252 })
253 .unwrap_or((0, 0));
254
255 (depth, difference)
256 }
257}
258
259impl GitPanel {
260 fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
261 // TODO: Implement stage all
262 println!("Stage all triggered");
263 }
264
265 fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
266 // TODO: Implement unstage all
267 println!("Unstage all triggered");
268 }
269
270 fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
271 // TODO: Implement discard all
272 println!("Discard all triggered");
273 }
274
275 /// Commit all staged changes
276 fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
277 // TODO: Implement commit all staged
278 println!("Commit staged changes triggered");
279 }
280
281 /// Commit all changes, regardless of whether they are staged or not
282 fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
283 // TODO: Implement commit all changes
284 println!("Commit all changes triggered");
285 }
286
287 fn all_staged(&self) -> bool {
288 // TODO: Implement all_staged
289 true
290 }
291
292 fn no_entries(&self) -> bool {
293 self.visible_entries.is_empty()
294 }
295
296 fn entry_count(&self) -> usize {
297 self.visible_entries
298 .iter()
299 .map(|(_, entries, _)| {
300 entries
301 .iter()
302 .filter(|entry| entry.git_status.is_some())
303 .count()
304 })
305 .sum()
306 }
307
308 fn for_each_visible_entry(
309 &self,
310 range: Range<usize>,
311 cx: &mut ViewContext<Self>,
312 mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
313 ) {
314 let mut ix = 0;
315 for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
316 if ix >= range.end {
317 return;
318 }
319
320 if ix + visible_worktree_entries.len() <= range.start {
321 ix += visible_worktree_entries.len();
322 continue;
323 }
324
325 let end_ix = range.end.min(ix + visible_worktree_entries.len());
326 // let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
327 if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
328 let snapshot = worktree.read(cx).snapshot();
329 let root_name = OsStr::new(snapshot.root_name());
330 let expanded_entry_ids = self
331 .expanded_dir_ids
332 .get(&snapshot.id())
333 .map(Vec::as_slice)
334 .unwrap_or(&[]);
335
336 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
337 let entries = entries_paths.get_or_init(|| {
338 visible_worktree_entries
339 .iter()
340 .map(|e| (e.path.clone()))
341 .collect()
342 });
343
344 for entry in visible_worktree_entries[entry_range].iter() {
345 let status = entry.git_status;
346 let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
347
348 let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
349
350 let filename = match difference {
351 diff if diff > 1 => entry
352 .path
353 .iter()
354 .skip(entry.path.components().count() - diff)
355 .collect::<PathBuf>()
356 .to_str()
357 .unwrap_or_default()
358 .to_string(),
359 _ => entry
360 .path
361 .file_name()
362 .map(|name| name.to_string_lossy().into_owned())
363 .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
364 };
365
366 let display_name = entry.path.to_string_lossy().into_owned();
367
368 let details = EntryDetails {
369 filename,
370 display_name,
371 kind: entry.kind,
372 is_expanded,
373 path: entry.path.clone(),
374 status,
375 depth,
376 };
377 callback(entry.id, details, cx);
378 }
379 }
380 ix = end_ix;
381 }
382 }
383
384 // TODO: Update expanded directory state
385 fn update_visible_entries(
386 &mut self,
387 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
388 cx: &mut ViewContext<Self>,
389 ) {
390 let project = self.project.read(cx);
391 self.visible_entries.clear();
392 for worktree in project.visible_worktrees(cx) {
393 let snapshot = worktree.read(cx).snapshot();
394 let worktree_id = snapshot.id();
395
396 let mut visible_worktree_entries = Vec::new();
397 let mut entry_iter = snapshot.entries(true, 0);
398 while let Some(entry) = entry_iter.entry() {
399 // Only include entries with a git status
400 if entry.git_status.is_some() {
401 visible_worktree_entries.push(entry.clone());
402 }
403 entry_iter.advance();
404 }
405
406 snapshot.propagate_git_statuses(&mut visible_worktree_entries);
407 project::sort_worktree_entries(&mut visible_worktree_entries);
408
409 if !visible_worktree_entries.is_empty() {
410 self.visible_entries
411 .push((worktree_id, visible_worktree_entries, OnceCell::new()));
412 }
413 }
414
415 if let Some((worktree_id, entry_id)) = new_selected_entry {
416 self.selected_item = self.visible_entries.iter().enumerate().find_map(
417 |(worktree_index, (id, entries, _))| {
418 if *id == worktree_id {
419 entries
420 .iter()
421 .position(|entry| entry.id == entry_id)
422 .map(|entry_index| worktree_index * entries.len() + entry_index)
423 } else {
424 None
425 }
426 },
427 );
428 }
429
430 cx.notify();
431 }
432}
433
434impl GitPanel {
435 pub fn panel_button(
436 &self,
437 id: impl Into<SharedString>,
438 label: impl Into<SharedString>,
439 ) -> Button {
440 let id = id.into().clone();
441 let label = label.into().clone();
442
443 Button::new(id, label)
444 .label_size(LabelSize::Small)
445 .layer(ElevationIndex::ElevatedSurface)
446 .size(ButtonSize::Compact)
447 .style(ButtonStyle::Filled)
448 }
449
450 pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
451 h_flex()
452 .items_center()
453 .h(px(8.))
454 .child(Divider::horizontal_dashed().color(DividerColor::Border))
455 }
456
457 pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
458 let focus_handle = self.focus_handle(cx).clone();
459
460 let changes_string = format!("{} changes", self.entry_count());
461
462 h_flex()
463 .h(px(32.))
464 .items_center()
465 .px_3()
466 .bg(ElevationIndex::Surface.bg(cx))
467 .child(
468 h_flex()
469 .gap_2()
470 .child(Checkbox::new("all-changes", true.into()).disabled(true))
471 .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
472 )
473 .child(div().flex_grow())
474 .child(
475 h_flex()
476 .gap_2()
477 .child(
478 IconButton::new("discard-changes", IconName::Undo)
479 .tooltip(move |cx| {
480 let focus_handle = focus_handle.clone();
481
482 Tooltip::for_action_in(
483 "Discard all changes",
484 &DiscardAll,
485 &focus_handle,
486 cx,
487 )
488 })
489 .icon_size(IconSize::Small)
490 .disabled(true),
491 )
492 .child(if self.all_staged() {
493 self.panel_button("unstage-all", "Unstage All").on_click(
494 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
495 )
496 } else {
497 self.panel_button("stage-all", "Stage All").on_click(
498 cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
499 )
500 }),
501 )
502 }
503
504 pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
505 let focus_handle_1 = self.focus_handle(cx).clone();
506 let focus_handle_2 = self.focus_handle(cx).clone();
507
508 let commit_staged_button = self
509 .panel_button("commit-staged-changes", "Commit")
510 .tooltip(move |cx| {
511 let focus_handle = focus_handle_1.clone();
512 Tooltip::for_action_in(
513 "Commit all staged changes",
514 &CommitStagedChanges,
515 &focus_handle,
516 cx,
517 )
518 })
519 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
520 this.commit_staged_changes(&CommitStagedChanges, cx)
521 }));
522
523 let commit_all_button = self
524 .panel_button("commit-all-changes", "Commit All")
525 .tooltip(move |cx| {
526 let focus_handle = focus_handle_2.clone();
527 Tooltip::for_action_in(
528 "Commit all changes, including unstaged changes",
529 &CommitAllChanges,
530 &focus_handle,
531 cx,
532 )
533 })
534 .on_click(cx.listener(|this, _: &ClickEvent, cx| {
535 this.commit_all_changes(&CommitAllChanges, cx)
536 }));
537
538 div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
539 v_flex()
540 .h_full()
541 .py_2p5()
542 .px_3()
543 .bg(cx.theme().colors().editor_background)
544 .font_buffer(cx)
545 .text_ui_sm(cx)
546 .text_color(cx.theme().colors().text_muted)
547 .child("Add a message")
548 .gap_1()
549 .child(div().flex_grow())
550 .child(h_flex().child(div().gap_1().flex_grow()).child(
551 if self.current_modifiers.alt {
552 commit_all_button
553 } else {
554 commit_staged_button
555 },
556 ))
557 .cursor(CursorStyle::OperationNotAllowed)
558 .opacity(0.5),
559 )
560 }
561
562 fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
563 h_flex()
564 .h_full()
565 .flex_1()
566 .justify_center()
567 .items_center()
568 .child(
569 v_flex()
570 .gap_3()
571 .child("No changes to commit")
572 .text_ui_sm(cx)
573 .mx_auto()
574 .text_color(Color::Placeholder.color(cx)),
575 )
576 }
577
578 fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
579 if !Self::should_show_scrollbar(cx)
580 || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
581 {
582 return None;
583 }
584 Some(
585 div()
586 .occlude()
587 .id("project-panel-vertical-scroll")
588 .on_mouse_move(cx.listener(|_, _, cx| {
589 cx.notify();
590 cx.stop_propagation()
591 }))
592 .on_hover(|_, cx| {
593 cx.stop_propagation();
594 })
595 .on_any_mouse_down(|_, cx| {
596 cx.stop_propagation();
597 })
598 .on_mouse_up(
599 MouseButton::Left,
600 cx.listener(|this, _, cx| {
601 if !this.scrollbar_state.is_dragging()
602 && !this.focus_handle.contains_focused(cx)
603 {
604 this.hide_scrollbar(cx);
605 cx.notify();
606 }
607
608 cx.stop_propagation();
609 }),
610 )
611 .on_scroll_wheel(cx.listener(|_, _, cx| {
612 cx.notify();
613 }))
614 .h_full()
615 .absolute()
616 .right_1()
617 .top_1()
618 .bottom_1()
619 .w(px(12.))
620 .cursor_default()
621 .children(Scrollbar::vertical(
622 // percentage as f32..end_offset as f32,
623 self.scrollbar_state.clone(),
624 )),
625 )
626 }
627
628 fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
629 let item_count = self
630 .visible_entries
631 .iter()
632 .map(|(_, worktree_entries, _)| worktree_entries.len())
633 .sum();
634 h_flex()
635 .size_full()
636 .overflow_hidden()
637 .child(
638 uniform_list(cx.view().clone(), "entries", item_count, {
639 |this, range, cx| {
640 let mut items = Vec::with_capacity(range.end - range.start);
641 this.for_each_visible_entry(range, cx, |id, details, cx| {
642 items.push(this.render_entry(id, details, cx));
643 });
644 items
645 }
646 })
647 .size_full()
648 .with_sizing_behavior(ListSizingBehavior::Infer)
649 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
650 // .with_width_from_item(self.max_width_item_index)
651 .track_scroll(self.scroll_handle.clone()),
652 )
653 .children(self.render_scrollbar(cx))
654 }
655
656 fn render_entry(
657 &self,
658 id: ProjectEntryId,
659 details: EntryDetails,
660 cx: &ViewContext<Self>,
661 ) -> impl IntoElement {
662 let id = id.to_proto() as usize;
663 let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
664 let is_staged = ToggleState::Selected;
665
666 h_flex()
667 .id(id)
668 .h(px(28.))
669 .w_full()
670 .pl(px(12. + 12. * details.depth as f32))
671 .pr(px(4.))
672 .items_center()
673 .gap_2()
674 .font_buffer(cx)
675 .text_ui_sm(cx)
676 .when(!details.is_dir(), |this| {
677 this.child(Checkbox::new(checkbox_id, is_staged))
678 })
679 .when_some(details.status, |this, status| {
680 this.child(git_status_icon(status))
681 })
682 .child(h_flex().gap_1p5().child(details.display_name.clone()))
683 }
684}
685
686impl Render for GitPanel {
687 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
688 let project = self.project.read(cx);
689
690 v_flex()
691 .id("git_panel")
692 .key_context(self.dispatch_context())
693 .track_focus(&self.focus_handle)
694 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
695 .when(!project.is_read_only(cx), |this| {
696 this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
697 .on_action(
698 cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
699 )
700 .on_action(
701 cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
702 )
703 .on_action(cx.listener(|this, &CommitStagedChanges, cx| {
704 this.commit_staged_changes(&CommitStagedChanges, cx)
705 }))
706 .on_action(cx.listener(|this, &CommitAllChanges, cx| {
707 this.commit_all_changes(&CommitAllChanges, cx)
708 }))
709 })
710 .on_hover(cx.listener(|this, hovered, cx| {
711 if *hovered {
712 this.show_scrollbar = true;
713 this.hide_scrollbar_task.take();
714 cx.notify();
715 } else if !this.focus_handle.contains_focused(cx) {
716 this.hide_scrollbar(cx);
717 }
718 }))
719 .size_full()
720 .overflow_hidden()
721 .font_buffer(cx)
722 .py_1()
723 .bg(ElevationIndex::Surface.bg(cx))
724 .child(self.render_panel_header(cx))
725 .child(self.render_divider(cx))
726 .child(if !self.no_entries() {
727 self.render_entries(cx).into_any_element()
728 } else {
729 self.render_empty_state(cx).into_any_element()
730 })
731 .child(self.render_divider(cx))
732 .child(self.render_commit_editor(cx))
733 }
734}
735
736impl FocusableView for GitPanel {
737 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
738 self.focus_handle.clone()
739 }
740}
741
742impl EventEmitter<Event> for GitPanel {}
743
744impl EventEmitter<PanelEvent> for GitPanel {}
745
746impl Panel for GitPanel {
747 fn persistent_name() -> &'static str {
748 "GitPanel"
749 }
750
751 fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
752 GitPanelSettings::get_global(cx).dock
753 }
754
755 fn position_is_valid(&self, position: DockPosition) -> bool {
756 matches!(position, DockPosition::Left | DockPosition::Right)
757 }
758
759 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
760 settings::update_settings_file::<GitPanelSettings>(
761 self.fs.clone(),
762 cx,
763 move |settings, _| settings.dock = Some(position),
764 );
765 }
766
767 fn size(&self, cx: &gpui::WindowContext) -> Pixels {
768 self.width
769 .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
770 }
771
772 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
773 self.width = size;
774 self.serialize(cx);
775 cx.notify();
776 }
777
778 fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
779 Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
780 }
781
782 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
783 Some("Git Panel")
784 }
785
786 fn toggle_action(&self) -> Box<dyn Action> {
787 Box::new(ToggleFocus)
788 }
789}