git_panel.rs

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