git_panel.rs

   1use crate::git_panel_settings::StatusStyle;
   2use crate::{git_panel_settings::GitPanelSettings, git_status_icon};
   3use anyhow::{Context as _, Result};
   4use db::kvp::KEY_VALUE_STORE;
   5use editor::scroll::ScrollbarAutoHide;
   6use editor::{Editor, EditorSettings, ShowScrollbar};
   7use git::repository::{GitRepository, RepoPath};
   8use git::status::FileStatus;
   9use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
  10use gpui::*;
  11use language::Buffer;
  12use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
  13use project::git::GitState;
  14use project::{Fs, Project, ProjectPath, WorktreeId};
  15use serde::{Deserialize, Serialize};
  16use settings::Settings as _;
  17use std::sync::atomic::{AtomicBool, Ordering};
  18use std::{collections::HashSet, ops::Range, path::PathBuf, sync::Arc, time::Duration, usize};
  19use theme::ThemeSettings;
  20use ui::{
  21    prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
  22};
  23use util::{maybe, ResultExt, TryFutureExt};
  24use workspace::notifications::DetachAndPromptErr;
  25use workspace::{
  26    dock::{DockPosition, Panel, PanelEvent},
  27    Workspace,
  28};
  29use worktree::RepositoryEntry;
  30
  31actions!(
  32    git_panel,
  33    [
  34        Close,
  35        ToggleFocus,
  36        OpenMenu,
  37        OpenSelected,
  38        FocusEditor,
  39        FocusChanges
  40    ]
  41);
  42
  43const GIT_PANEL_KEY: &str = "GitPanel";
  44
  45const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  46
  47pub fn init(cx: &mut AppContext) {
  48    cx.observe_new_views(
  49        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
  50            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
  51                workspace.toggle_panel_focus::<GitPanel>(cx);
  52            });
  53        },
  54    )
  55    .detach();
  56}
  57
  58#[derive(Debug, Clone)]
  59pub enum Event {
  60    Focus,
  61    OpenedEntry { path: ProjectPath },
  62}
  63
  64#[derive(Serialize, Deserialize)]
  65struct SerializedGitPanel {
  66    width: Option<Pixels>,
  67}
  68
  69#[derive(Debug, PartialEq, Eq, Clone)]
  70pub struct GitListEntry {
  71    depth: usize,
  72    display_name: String,
  73    repo_path: RepoPath,
  74    status: FileStatus,
  75    is_staged: Option<bool>,
  76}
  77
  78pub struct GitPanel {
  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_entry: Option<usize>,
  88    show_scrollbar: bool,
  89    rebuild_requested: Arc<AtomicBool>,
  90    commit_editor: View<Editor>,
  91    visible_entries: Vec<GitListEntry>,
  92    all_staged: Option<bool>,
  93    width: Option<Pixels>,
  94    reveal_in_editor: Task<()>,
  95}
  96
  97fn first_worktree_repository(
  98    project: &Model<Project>,
  99    worktree_id: WorktreeId,
 100    cx: &mut AppContext,
 101) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
 102    project
 103        .read(cx)
 104        .worktree_for_id(worktree_id, cx)
 105        .and_then(|worktree| {
 106            let snapshot = worktree.read(cx).snapshot();
 107            let repo = snapshot.repositories().iter().next()?.clone();
 108            let git_repo = worktree
 109                .read(cx)
 110                .as_local()?
 111                .get_local_repo(&repo)?
 112                .repo()
 113                .clone();
 114            Some((repo, git_repo))
 115        })
 116}
 117
 118fn first_repository_in_project(
 119    project: &Model<Project>,
 120    cx: &mut AppContext,
 121) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
 122    project.read(cx).worktrees(cx).next().and_then(|worktree| {
 123        let snapshot = worktree.read(cx).snapshot();
 124        let repo = snapshot.repositories().iter().next()?.clone();
 125        let git_repo = worktree
 126            .read(cx)
 127            .as_local()?
 128            .get_local_repo(&repo)?
 129            .repo()
 130            .clone();
 131        Some((snapshot.id(), repo, git_repo))
 132    })
 133}
 134
 135impl GitPanel {
 136    pub fn load(
 137        workspace: WeakView<Workspace>,
 138        cx: AsyncWindowContext,
 139    ) -> Task<Result<View<Self>>> {
 140        cx.spawn(|mut cx| async move { workspace.update(&mut cx, Self::new) })
 141    }
 142
 143    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 144        let fs = workspace.app_state().fs.clone();
 145        let project = workspace.project().clone();
 146        let git_state = project.read(cx).git_state().cloned();
 147        let language_registry = workspace.app_state().languages.clone();
 148        let current_commit_message = git_state
 149            .as_ref()
 150            .and_then(|git_state| git_state.read(cx).commit_message.clone());
 151
 152        let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
 153            let focus_handle = cx.focus_handle();
 154            cx.on_focus(&focus_handle, Self::focus_in).detach();
 155            cx.on_focus_out(&focus_handle, |this, _, cx| {
 156                this.hide_scrollbar(cx);
 157            })
 158            .detach();
 159            cx.subscribe(&project, move |this, project, event, cx| {
 160                use project::Event;
 161
 162                let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| {
 163                    let snapshot = worktree.read(cx).snapshot();
 164                    snapshot.id()
 165                });
 166                let first_repo_in_project = first_repository_in_project(&project, cx);
 167
 168                let Some(git_state) = project.read(cx).git_state().cloned() else {
 169                    return;
 170                };
 171                git_state.update(cx, |git_state, _| {
 172                    match event {
 173                        project::Event::WorktreeRemoved(id) => {
 174                            let Some((worktree_id, _, _)) = git_state.active_repository.as_ref()
 175                            else {
 176                                return;
 177                            };
 178                            if worktree_id == id {
 179                                git_state.active_repository = first_repo_in_project;
 180                                this.schedule_update();
 181                            }
 182                        }
 183                        project::Event::WorktreeOrderChanged => {
 184                            // activate the new first worktree if the first was moved
 185                            let Some(first_id) = first_worktree_id else {
 186                                return;
 187                            };
 188                            if !git_state
 189                                .active_repository
 190                                .as_ref()
 191                                .is_some_and(|(id, _, _)| id == &first_id)
 192                            {
 193                                git_state.active_repository = first_repo_in_project;
 194                                this.schedule_update();
 195                            }
 196                        }
 197                        Event::WorktreeAdded(_) => {
 198                            let Some(first_id) = first_worktree_id else {
 199                                return;
 200                            };
 201                            if !git_state
 202                                .active_repository
 203                                .as_ref()
 204                                .is_some_and(|(id, _, _)| id == &first_id)
 205                            {
 206                                git_state.active_repository = first_repo_in_project;
 207                                this.schedule_update();
 208                            }
 209                        }
 210                        project::Event::WorktreeUpdatedEntries(id, _) => {
 211                            if git_state
 212                                .active_repository
 213                                .as_ref()
 214                                .is_some_and(|(active_id, _, _)| active_id == id)
 215                            {
 216                                git_state.active_repository = first_repo_in_project;
 217                                this.schedule_update();
 218                            }
 219                        }
 220                        project::Event::WorktreeUpdatedGitRepositories(_) => {
 221                            let Some(first) = first_repo_in_project else {
 222                                return;
 223                            };
 224                            git_state.active_repository = Some(first);
 225                            this.schedule_update();
 226                        }
 227                        project::Event::Closed => {
 228                            this.reveal_in_editor = Task::ready(());
 229                            this.visible_entries.clear();
 230                        }
 231                        _ => {}
 232                    };
 233                });
 234            })
 235            .detach();
 236
 237            let commit_editor = cx.new_view(|cx| {
 238                let theme = ThemeSettings::get_global(cx);
 239
 240                let mut text_style = cx.text_style();
 241                let refinement = TextStyleRefinement {
 242                    font_family: Some(theme.buffer_font.family.clone()),
 243                    font_features: Some(FontFeatures::disable_ligatures()),
 244                    font_size: Some(px(12.).into()),
 245                    color: Some(cx.theme().colors().editor_foreground),
 246                    background_color: Some(gpui::transparent_black()),
 247                    ..Default::default()
 248                };
 249
 250                text_style.refine(&refinement);
 251
 252                let mut commit_editor = Editor::auto_height(10, cx);
 253                if let Some(message) = current_commit_message {
 254                    commit_editor.set_text(message, cx);
 255                } else {
 256                    commit_editor.set_text("", cx);
 257                }
 258                commit_editor.set_use_autoclose(false);
 259                commit_editor.set_show_gutter(false, cx);
 260                commit_editor.set_show_wrap_guides(false, cx);
 261                commit_editor.set_show_indent_guides(false, cx);
 262                commit_editor.set_text_style_refinement(refinement);
 263                commit_editor.set_placeholder_text("Enter commit message", cx);
 264                commit_editor
 265            });
 266
 267            let buffer = commit_editor
 268                .read(cx)
 269                .buffer()
 270                .read(cx)
 271                .as_singleton()
 272                .expect("commit editor must be singleton");
 273
 274            cx.subscribe(&buffer, Self::on_buffer_event).detach();
 275
 276            let markdown = language_registry.language_for_name("Markdown");
 277            cx.spawn(|_, mut cx| async move {
 278                let markdown = markdown.await.context("failed to load Markdown language")?;
 279                buffer.update(&mut cx, |buffer, cx| {
 280                    buffer.set_language(Some(markdown), cx)
 281                })
 282            })
 283            .detach_and_log_err(cx);
 284
 285            let scroll_handle = UniformListScrollHandle::new();
 286
 287            let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
 288            let first_worktree = visible_worktrees.next();
 289            drop(visible_worktrees);
 290            if let Some(first_worktree) = first_worktree {
 291                let snapshot = first_worktree.read(cx).snapshot();
 292
 293                if let Some(((repo, git_repo), git_state)) =
 294                    first_worktree_repository(&project, snapshot.id(), cx).zip(git_state)
 295                {
 296                    git_state.update(cx, |git_state, _| {
 297                        git_state.activate_repository(snapshot.id(), repo, git_repo);
 298                    });
 299                }
 300            };
 301
 302            let rebuild_requested = Arc::new(AtomicBool::new(false));
 303            let flag = rebuild_requested.clone();
 304            let handle = cx.view().downgrade();
 305            cx.spawn(|_, mut cx| async move {
 306                loop {
 307                    cx.background_executor().timer(UPDATE_DEBOUNCE).await;
 308                    if flag.load(Ordering::Relaxed) {
 309                        if let Some(this) = handle.upgrade() {
 310                            this.update(&mut cx, |this, cx| {
 311                                this.update_visible_entries(cx);
 312                            })
 313                            .ok();
 314                        }
 315                        flag.store(false, Ordering::Relaxed);
 316                    }
 317                }
 318            })
 319            .detach();
 320
 321            let mut git_panel = Self {
 322                focus_handle: cx.focus_handle(),
 323                fs,
 324                pending_serialization: Task::ready(None),
 325                visible_entries: Vec::new(),
 326                all_staged: None,
 327                current_modifiers: cx.modifiers(),
 328                width: Some(px(360.)),
 329                scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
 330                scroll_handle,
 331                selected_entry: None,
 332                show_scrollbar: false,
 333                hide_scrollbar_task: None,
 334                rebuild_requested,
 335                commit_editor,
 336                reveal_in_editor: Task::ready(()),
 337                project,
 338            };
 339            git_panel.schedule_update();
 340            git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
 341            git_panel
 342        });
 343
 344        cx.subscribe(
 345            &git_panel,
 346            move |workspace, _, event: &Event, cx| match event.clone() {
 347                Event::OpenedEntry { path } => {
 348                    workspace
 349                        .open_path_preview(path, None, false, false, cx)
 350                        .detach_and_prompt_err("Failed to open file", cx, |e, _| {
 351                            Some(format!("{e}"))
 352                        });
 353                }
 354                Event::Focus => { /* TODO */ }
 355            },
 356        )
 357        .detach();
 358
 359        git_panel
 360    }
 361
 362    fn git_state(&self, cx: &AppContext) -> Option<Model<GitState>> {
 363        self.project.read(cx).git_state().cloned()
 364    }
 365
 366    fn active_repository<'a>(
 367        &self,
 368        cx: &'a AppContext,
 369    ) -> Option<&'a (WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
 370        let git_state = self.git_state(cx)?;
 371        let active_repository = git_state.read(cx).active_repository.as_ref()?;
 372        Some(active_repository)
 373    }
 374
 375    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 376        // TODO: we can store stage status here
 377        let width = self.width;
 378        self.pending_serialization = cx.background_executor().spawn(
 379            async move {
 380                KEY_VALUE_STORE
 381                    .write_kvp(
 382                        GIT_PANEL_KEY.into(),
 383                        serde_json::to_string(&SerializedGitPanel { width })?,
 384                    )
 385                    .await?;
 386                anyhow::Ok(())
 387            }
 388            .log_err(),
 389        );
 390    }
 391
 392    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
 393        let mut dispatch_context = KeyContext::new_with_defaults();
 394        dispatch_context.add("GitPanel");
 395
 396        if self.is_focused(cx) {
 397            dispatch_context.add("menu");
 398            dispatch_context.add("ChangesList");
 399        }
 400
 401        if self.commit_editor.read(cx).is_focused(cx) {
 402            dispatch_context.add("CommitEditor");
 403        }
 404
 405        dispatch_context
 406    }
 407
 408    fn is_focused(&self, cx: &ViewContext<Self>) -> bool {
 409        cx.focused()
 410            .map_or(false, |focused| self.focus_handle == focused)
 411    }
 412
 413    fn close_panel(&mut self, _: &Close, cx: &mut ViewContext<Self>) {
 414        cx.emit(PanelEvent::Close);
 415    }
 416
 417    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 418        if !self.focus_handle.contains_focused(cx) {
 419            cx.emit(Event::Focus);
 420        }
 421    }
 422
 423    fn show_scrollbar(&self, cx: &mut ViewContext<Self>) -> ShowScrollbar {
 424        GitPanelSettings::get_global(cx)
 425            .scrollbar
 426            .show
 427            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
 428    }
 429
 430    fn should_show_scrollbar(&self, cx: &mut ViewContext<Self>) -> bool {
 431        let show = self.show_scrollbar(cx);
 432        match show {
 433            ShowScrollbar::Auto => true,
 434            ShowScrollbar::System => true,
 435            ShowScrollbar::Always => true,
 436            ShowScrollbar::Never => false,
 437        }
 438    }
 439
 440    fn should_autohide_scrollbar(&self, cx: &mut ViewContext<Self>) -> bool {
 441        let show = self.show_scrollbar(cx);
 442        match show {
 443            ShowScrollbar::Auto => true,
 444            ShowScrollbar::System => cx
 445                .try_global::<ScrollbarAutoHide>()
 446                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 447            ShowScrollbar::Always => false,
 448            ShowScrollbar::Never => true,
 449        }
 450    }
 451
 452    fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
 453        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 454        if !self.should_autohide_scrollbar(cx) {
 455            return;
 456        }
 457        self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
 458            cx.background_executor()
 459                .timer(SCROLLBAR_SHOW_INTERVAL)
 460                .await;
 461            panel
 462                .update(&mut cx, |panel, cx| {
 463                    panel.show_scrollbar = false;
 464                    cx.notify();
 465                })
 466                .log_err();
 467        }))
 468    }
 469
 470    fn handle_modifiers_changed(
 471        &mut self,
 472        event: &ModifiersChangedEvent,
 473        cx: &mut ViewContext<Self>,
 474    ) {
 475        self.current_modifiers = event.modifiers;
 476        cx.notify();
 477    }
 478
 479    fn calculate_depth_and_difference(
 480        repo_path: &RepoPath,
 481        visible_entries: &HashSet<RepoPath>,
 482    ) -> (usize, usize) {
 483        let ancestors = repo_path.ancestors().skip(1);
 484        for ancestor in ancestors {
 485            if let Some(parent_entry) = visible_entries.get(ancestor) {
 486                let entry_component_count = repo_path.components().count();
 487                let parent_component_count = parent_entry.components().count();
 488
 489                let difference = entry_component_count - parent_component_count;
 490
 491                let parent_depth = parent_entry
 492                    .ancestors()
 493                    .skip(1) // Skip the parent itself
 494                    .filter(|ancestor| visible_entries.contains(*ancestor))
 495                    .count();
 496
 497                return (parent_depth + 1, difference);
 498            }
 499        }
 500
 501        (0, 0)
 502    }
 503
 504    fn scroll_to_selected_entry(&mut self, cx: &mut ViewContext<Self>) {
 505        if let Some(selected_entry) = self.selected_entry {
 506            self.scroll_handle
 507                .scroll_to_item(selected_entry, ScrollStrategy::Center);
 508        }
 509
 510        cx.notify();
 511    }
 512
 513    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
 514        if self.visible_entries.first().is_some() {
 515            self.selected_entry = Some(0);
 516            self.scroll_to_selected_entry(cx);
 517        }
 518    }
 519
 520    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 521        let item_count = self.visible_entries.len();
 522        if item_count == 0 {
 523            return;
 524        }
 525
 526        if let Some(selected_entry) = self.selected_entry {
 527            let new_selected_entry = if selected_entry > 0 {
 528                selected_entry - 1
 529            } else {
 530                selected_entry
 531            };
 532
 533            self.selected_entry = Some(new_selected_entry);
 534
 535            self.scroll_to_selected_entry(cx);
 536        }
 537
 538        cx.notify();
 539    }
 540
 541    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 542        let item_count = self.visible_entries.len();
 543        if item_count == 0 {
 544            return;
 545        }
 546
 547        if let Some(selected_entry) = self.selected_entry {
 548            let new_selected_entry = if selected_entry < item_count - 1 {
 549                selected_entry + 1
 550            } else {
 551                selected_entry
 552            };
 553
 554            self.selected_entry = Some(new_selected_entry);
 555
 556            self.scroll_to_selected_entry(cx);
 557        }
 558
 559        cx.notify();
 560    }
 561
 562    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
 563        if self.visible_entries.last().is_some() {
 564            self.selected_entry = Some(self.visible_entries.len() - 1);
 565            self.scroll_to_selected_entry(cx);
 566        }
 567    }
 568
 569    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
 570        self.commit_editor.update(cx, |editor, cx| {
 571            editor.focus(cx);
 572        });
 573        cx.notify();
 574    }
 575
 576    fn select_first_entry_if_none(&mut self, cx: &mut ViewContext<Self>) {
 577        if !self.no_entries(cx) && self.selected_entry.is_none() {
 578            self.selected_entry = Some(0);
 579            self.scroll_to_selected_entry(cx);
 580            cx.notify();
 581        }
 582    }
 583
 584    fn focus_changes_list(&mut self, _: &FocusChanges, cx: &mut ViewContext<Self>) {
 585        self.select_first_entry_if_none(cx);
 586
 587        cx.focus_self();
 588        cx.notify();
 589    }
 590
 591    fn get_selected_entry(&self) -> Option<&GitListEntry> {
 592        self.selected_entry
 593            .and_then(|i| self.visible_entries.get(i))
 594    }
 595
 596    fn open_selected(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 597        if let Some(entry) = self
 598            .selected_entry
 599            .and_then(|i| self.visible_entries.get(i))
 600        {
 601            self.open_entry(entry, cx);
 602        }
 603    }
 604
 605    fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
 606        let Some(git_state) = self.git_state(cx) else {
 607            return;
 608        };
 609        git_state.update(cx, |git_state, _| {
 610            if entry.status.is_staged().unwrap_or(false) {
 611                git_state.stage_entries(vec![entry.repo_path.clone()]);
 612            } else {
 613                git_state.stage_entries(vec![entry.repo_path.clone()]);
 614            }
 615        });
 616        cx.notify();
 617    }
 618
 619    fn toggle_staged_for_selected(&mut self, _: &git::ToggleStaged, cx: &mut ViewContext<Self>) {
 620        if let Some(selected_entry) = self.get_selected_entry().cloned() {
 621            self.toggle_staged_for_entry(&selected_entry, cx);
 622        }
 623    }
 624
 625    fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
 626        let Some((worktree_id, path)) = maybe!({
 627            let git_state = self.git_state(cx)?;
 628            let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?;
 629            let path = repo.work_directory.unrelativize(&entry.repo_path)?;
 630            Some((*id, path))
 631        }) else {
 632            return;
 633        };
 634        let path = (worktree_id, path).into();
 635        let path_exists = self.project.update(cx, |project, cx| {
 636            project.entry_for_path(&path, cx).is_some()
 637        });
 638        if !path_exists {
 639            return;
 640        }
 641        cx.emit(Event::OpenedEntry { path });
 642    }
 643
 644    fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext<Self>) {
 645        let Some(git_state) = self.git_state(cx) else {
 646            return;
 647        };
 648        for entry in &mut self.visible_entries {
 649            entry.is_staged = Some(true);
 650        }
 651        self.all_staged = Some(true);
 652        git_state.read(cx).stage_all();
 653    }
 654
 655    fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
 656        let Some(git_state) = self.git_state(cx) else {
 657            return;
 658        };
 659        for entry in &mut self.visible_entries {
 660            entry.is_staged = Some(false);
 661        }
 662        self.all_staged = Some(false);
 663        git_state.read(cx).unstage_all();
 664    }
 665
 666    fn discard_all(&mut self, _: &git::RevertAll, _cx: &mut ViewContext<Self>) {
 667        // TODO: Implement discard all
 668        println!("Discard all triggered");
 669    }
 670
 671    fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
 672        let Some(git_state) = self.git_state(cx) else {
 673            return;
 674        };
 675        git_state.update(cx, |git_state, _| {
 676            git_state.clear_commit_message();
 677        });
 678        self.commit_editor
 679            .update(cx, |editor, cx| editor.set_text("", cx));
 680    }
 681
 682    fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
 683        let Some(git_state) = self.git_state(cx) else {
 684            return false;
 685        };
 686        let has_message = !self.commit_editor.read(cx).text(cx).is_empty();
 687        let has_changes = git_state.read(cx).entry_count() > 0;
 688        let has_staged_changes = self
 689            .visible_entries
 690            .iter()
 691            .any(|entry| entry.is_staged == Some(true));
 692
 693        has_message && (commit_all || has_staged_changes) && has_changes
 694    }
 695
 696    /// Commit all staged changes
 697    fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
 698        self.clear_message(cx);
 699
 700        if !self.can_commit(false, cx) {
 701            return;
 702        }
 703
 704        // TODO: Implement commit all staged
 705        println!("Commit staged changes triggered");
 706    }
 707
 708    /// Commit all changes, regardless of whether they are staged or not
 709    fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
 710        self.clear_message(cx);
 711
 712        if !self.can_commit(true, cx) {
 713            return;
 714        }
 715
 716        // TODO: Implement commit all changes
 717        println!("Commit all changes triggered");
 718    }
 719
 720    fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
 721        self.git_state(cx)
 722            .map_or(true, |git_state| git_state.read(cx).entry_count() == 0)
 723    }
 724
 725    fn for_each_visible_entry(
 726        &self,
 727        range: Range<usize>,
 728        cx: &mut ViewContext<Self>,
 729        mut callback: impl FnMut(usize, GitListEntry, &mut ViewContext<Self>),
 730    ) {
 731        let visible_entries = &self.visible_entries;
 732
 733        for (ix, entry) in visible_entries
 734            .iter()
 735            .enumerate()
 736            .skip(range.start)
 737            .take(range.end - range.start)
 738        {
 739            let status = entry.status;
 740            let filename = entry
 741                .repo_path
 742                .file_name()
 743                .map(|name| name.to_string_lossy().into_owned())
 744                .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned());
 745
 746            let details = GitListEntry {
 747                repo_path: entry.repo_path.clone(),
 748                status,
 749                depth: 0,
 750                display_name: filename,
 751                is_staged: entry.is_staged,
 752            };
 753
 754            callback(ix, details, cx);
 755        }
 756    }
 757
 758    fn schedule_update(&mut self) {
 759        self.rebuild_requested.store(true, Ordering::Relaxed);
 760    }
 761
 762    #[track_caller]
 763    fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
 764        self.visible_entries.clear();
 765
 766        let Some((_, repo, _)) = self.active_repository(cx) else {
 767            // Just clear entries if no repository is active.
 768            cx.notify();
 769            return;
 770        };
 771
 772        // First pass - collect all paths
 773        let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
 774
 775        // Second pass - create entries with proper depth calculation
 776        let mut all_staged = None;
 777        for (ix, entry) in repo.status().enumerate() {
 778            let (depth, difference) =
 779                Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
 780            let is_staged = entry.status.is_staged();
 781            all_staged = if ix == 0 {
 782                is_staged
 783            } else {
 784                match (all_staged, is_staged) {
 785                    (None, _) | (_, None) => None,
 786                    (Some(a), Some(b)) => (a == b).then_some(a),
 787                }
 788            };
 789
 790            let display_name = if difference > 1 {
 791                // Show partial path for deeply nested files
 792                entry
 793                    .repo_path
 794                    .as_ref()
 795                    .iter()
 796                    .skip(entry.repo_path.components().count() - difference)
 797                    .collect::<PathBuf>()
 798                    .to_string_lossy()
 799                    .into_owned()
 800            } else {
 801                // Just show filename
 802                entry
 803                    .repo_path
 804                    .file_name()
 805                    .map(|name| name.to_string_lossy().into_owned())
 806                    .unwrap_or_default()
 807            };
 808
 809            let entry = GitListEntry {
 810                depth,
 811                display_name,
 812                repo_path: entry.repo_path,
 813                status: entry.status,
 814                is_staged,
 815            };
 816
 817            self.visible_entries.push(entry);
 818        }
 819        self.all_staged = all_staged;
 820
 821        // Sort entries by path to maintain consistent order
 822        self.visible_entries
 823            .sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
 824
 825        self.select_first_entry_if_none(cx);
 826
 827        cx.notify();
 828    }
 829
 830    fn on_buffer_event(
 831        &mut self,
 832        _buffer: Model<Buffer>,
 833        event: &language::BufferEvent,
 834        cx: &mut ViewContext<Self>,
 835    ) {
 836        if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
 837            let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx));
 838
 839            let Some(git_state) = self.git_state(cx) else {
 840                return;
 841            };
 842            git_state.update(cx, |git_state, _| {
 843                git_state.commit_message = Some(commit_message.into())
 844            });
 845
 846            cx.notify();
 847        }
 848    }
 849}
 850
 851// GitPanel –– Render
 852impl GitPanel {
 853    pub fn panel_button(
 854        &self,
 855        id: impl Into<SharedString>,
 856        label: impl Into<SharedString>,
 857    ) -> Button {
 858        let id = id.into().clone();
 859        let label = label.into().clone();
 860
 861        Button::new(id, label)
 862            .label_size(LabelSize::Small)
 863            .layer(ElevationIndex::ElevatedSurface)
 864            .size(ButtonSize::Compact)
 865            .style(ButtonStyle::Filled)
 866    }
 867
 868    pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 869        h_flex()
 870            .items_center()
 871            .h(px(8.))
 872            .child(Divider::horizontal_dashed().color(DividerColor::Border))
 873    }
 874
 875    pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 876        let focus_handle = self.focus_handle(cx).clone();
 877        let entry_count = self
 878            .git_state(cx)
 879            .map_or(0, |git_state| git_state.read(cx).entry_count());
 880
 881        let changes_string = match entry_count {
 882            0 => "No changes".to_string(),
 883            1 => "1 change".to_string(),
 884            n => format!("{} changes", n),
 885        };
 886
 887        // for our use case treat None as false
 888        let all_staged = self.all_staged.unwrap_or(false);
 889
 890        h_flex()
 891            .h(px(32.))
 892            .items_center()
 893            .px_2()
 894            .bg(ElevationIndex::Surface.bg(cx))
 895            .child(
 896                h_flex()
 897                    .gap_2()
 898                    .child(
 899                        Checkbox::new(
 900                            "all-changes",
 901                            if self.no_entries(cx) {
 902                                ToggleState::Selected
 903                            } else {
 904                                self.all_staged
 905                                    .map_or(ToggleState::Indeterminate, ToggleState::from)
 906                            },
 907                        )
 908                        .fill()
 909                        .elevation(ElevationIndex::Surface)
 910                        .tooltip(move |cx| {
 911                            if all_staged {
 912                                Tooltip::text("Unstage all changes", cx)
 913                            } else {
 914                                Tooltip::text("Stage all changes", cx)
 915                            }
 916                        })
 917                        .on_click(cx.listener(move |git_panel, _, cx| match all_staged {
 918                            true => git_panel.unstage_all(&UnstageAll, cx),
 919                            false => git_panel.stage_all(&StageAll, cx),
 920                        })),
 921                    )
 922                    .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
 923            )
 924            .child(div().flex_grow())
 925            .child(
 926                h_flex()
 927                    .gap_2()
 928                    // TODO: Re-add once revert all is added
 929                    // .child(
 930                    //     IconButton::new("discard-changes", IconName::Undo)
 931                    //         .tooltip({
 932                    //             let focus_handle = focus_handle.clone();
 933                    //             move |cx| {
 934                    //                 Tooltip::for_action_in(
 935                    //                     "Discard all changes",
 936                    //                     &RevertAll,
 937                    //                     &focus_handle,
 938                    //                     cx,
 939                    //                 )
 940                    //             }
 941                    //         })
 942                    //         .icon_size(IconSize::Small)
 943                    //         .disabled(true),
 944                    // )
 945                    .child(if self.all_staged.unwrap_or(false) {
 946                        self.panel_button("unstage-all", "Unstage All")
 947                            .tooltip({
 948                                let focus_handle = focus_handle.clone();
 949                                move |cx| {
 950                                    Tooltip::for_action_in(
 951                                        "Unstage all changes",
 952                                        &UnstageAll,
 953                                        &focus_handle,
 954                                        cx,
 955                                    )
 956                                }
 957                            })
 958                            .key_binding(ui::KeyBinding::for_action_in(
 959                                &UnstageAll,
 960                                &focus_handle,
 961                                cx,
 962                            ))
 963                            .on_click(
 964                                cx.listener(move |this, _, cx| this.unstage_all(&UnstageAll, cx)),
 965                            )
 966                    } else {
 967                        self.panel_button("stage-all", "Stage All")
 968                            .tooltip({
 969                                let focus_handle = focus_handle.clone();
 970                                move |cx| {
 971                                    Tooltip::for_action_in(
 972                                        "Stage all changes",
 973                                        &StageAll,
 974                                        &focus_handle,
 975                                        cx,
 976                                    )
 977                                }
 978                            })
 979                            .key_binding(ui::KeyBinding::for_action_in(
 980                                &StageAll,
 981                                &focus_handle,
 982                                cx,
 983                            ))
 984                            .on_click(cx.listener(move |this, _, cx| this.stage_all(&StageAll, cx)))
 985                    }),
 986            )
 987    }
 988
 989    pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
 990        let editor = self.commit_editor.clone();
 991        let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
 992
 993        let focus_handle_1 = self.focus_handle(cx).clone();
 994        let focus_handle_2 = self.focus_handle(cx).clone();
 995
 996        let commit_staged_button = self
 997            .panel_button("commit-staged-changes", "Commit")
 998            .tooltip(move |cx| {
 999                let focus_handle = focus_handle_1.clone();
1000                Tooltip::for_action_in(
1001                    "Commit all staged changes",
1002                    &CommitChanges,
1003                    &focus_handle,
1004                    cx,
1005                )
1006            })
1007            .on_click(
1008                cx.listener(|this, _: &ClickEvent, cx| this.commit_changes(&CommitChanges, cx)),
1009            );
1010
1011        let commit_all_button = self
1012            .panel_button("commit-all-changes", "Commit All")
1013            .tooltip(move |cx| {
1014                let focus_handle = focus_handle_2.clone();
1015                Tooltip::for_action_in(
1016                    "Commit all changes, including unstaged changes",
1017                    &CommitAllChanges,
1018                    &focus_handle,
1019                    cx,
1020                )
1021            })
1022            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
1023                this.commit_all_changes(&CommitAllChanges, cx)
1024            }));
1025
1026        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
1027            v_flex()
1028                .id("commit-editor-container")
1029                .relative()
1030                .h_full()
1031                .py_2p5()
1032                .px_3()
1033                .bg(cx.theme().colors().editor_background)
1034                .on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
1035                .child(self.commit_editor.clone())
1036                .child(
1037                    h_flex()
1038                        .absolute()
1039                        .bottom_2p5()
1040                        .right_3()
1041                        .child(div().gap_1().flex_grow())
1042                        .child(if self.current_modifiers.alt {
1043                            commit_all_button
1044                        } else {
1045                            commit_staged_button
1046                        }),
1047                ),
1048        )
1049    }
1050
1051    fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
1052        h_flex()
1053            .h_full()
1054            .flex_1()
1055            .justify_center()
1056            .items_center()
1057            .child(
1058                v_flex()
1059                    .gap_3()
1060                    .child("No changes to commit")
1061                    .text_ui_sm(cx)
1062                    .mx_auto()
1063                    .text_color(Color::Placeholder.color(cx)),
1064            )
1065    }
1066
1067    fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
1068        let scroll_bar_style = self.show_scrollbar(cx);
1069        let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
1070
1071        if !self.should_show_scrollbar(cx)
1072            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
1073        {
1074            return None;
1075        }
1076
1077        Some(
1078            div()
1079                .id("git-panel-vertical-scroll")
1080                .occlude()
1081                .flex_none()
1082                .h_full()
1083                .cursor_default()
1084                .when(show_container, |this| this.pl_1().px_1p5())
1085                .when(!show_container, |this| {
1086                    this.absolute().right_1().top_1().bottom_1().w(px(12.))
1087                })
1088                .on_mouse_move(cx.listener(|_, _, cx| {
1089                    cx.notify();
1090                    cx.stop_propagation()
1091                }))
1092                .on_hover(|_, cx| {
1093                    cx.stop_propagation();
1094                })
1095                .on_any_mouse_down(|_, cx| {
1096                    cx.stop_propagation();
1097                })
1098                .on_mouse_up(
1099                    MouseButton::Left,
1100                    cx.listener(|this, _, cx| {
1101                        if !this.scrollbar_state.is_dragging()
1102                            && !this.focus_handle.contains_focused(cx)
1103                        {
1104                            this.hide_scrollbar(cx);
1105                            cx.notify();
1106                        }
1107
1108                        cx.stop_propagation();
1109                    }),
1110                )
1111                .on_scroll_wheel(cx.listener(|_, _, cx| {
1112                    cx.notify();
1113                }))
1114                .children(Scrollbar::vertical(
1115                    // percentage as f32..end_offset as f32,
1116                    self.scrollbar_state.clone(),
1117                )),
1118        )
1119    }
1120
1121    fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1122        let entry_count = self.visible_entries.len();
1123
1124        h_flex()
1125            .size_full()
1126            .overflow_hidden()
1127            .child(
1128                uniform_list(cx.view().clone(), "entries", entry_count, {
1129                    move |git_panel, range, cx| {
1130                        let mut items = Vec::with_capacity(range.end - range.start);
1131                        git_panel.for_each_visible_entry(range, cx, |ix, details, cx| {
1132                            items.push(git_panel.render_entry(ix, details, cx));
1133                        });
1134                        items
1135                    }
1136                })
1137                .size_full()
1138                .with_sizing_behavior(ListSizingBehavior::Infer)
1139                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
1140                // .with_width_from_item(self.max_width_item_index)
1141                .track_scroll(self.scroll_handle.clone()),
1142            )
1143            .children(self.render_scrollbar(cx))
1144    }
1145
1146    fn render_entry(
1147        &self,
1148        ix: usize,
1149        entry_details: GitListEntry,
1150        cx: &ViewContext<Self>,
1151    ) -> impl IntoElement {
1152        let repo_path = entry_details.repo_path.clone();
1153        let selected = self.selected_entry == Some(ix);
1154        let status_style = GitPanelSettings::get_global(cx).status_style;
1155        let status = entry_details.status;
1156
1157        let mut label_color = cx.theme().colors().text;
1158        if status_style == StatusStyle::LabelColor {
1159            label_color = if status.is_conflicted() {
1160                cx.theme().status().conflict
1161            } else if status.is_modified() {
1162                cx.theme().status().modified
1163            } else if status.is_deleted() {
1164                cx.theme().colors().text_disabled
1165            } else {
1166                cx.theme().status().created
1167            }
1168        }
1169
1170        let path_color = status
1171            .is_deleted()
1172            .then_some(cx.theme().colors().text_disabled)
1173            .unwrap_or(cx.theme().colors().text_muted);
1174
1175        let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into());
1176        let checkbox_id =
1177            ElementId::Name(format!("checkbox_{}", entry_details.display_name).into());
1178        let is_tree_view = false;
1179        let handle = cx.view().downgrade();
1180
1181        let end_slot = h_flex()
1182            .invisible()
1183            .when(selected, |this| this.visible())
1184            .when(!selected, |this| {
1185                this.group_hover("git-panel-entry", |this| this.visible())
1186            })
1187            .gap_1()
1188            .items_center()
1189            .child(
1190                IconButton::new("more", IconName::EllipsisVertical)
1191                    .icon_color(Color::Placeholder)
1192                    .icon_size(IconSize::Small),
1193            );
1194
1195        let mut entry = h_flex()
1196            .id(entry_id)
1197            .group("git-panel-entry")
1198            .h(px(28.))
1199            .w_full()
1200            .pr(px(4.))
1201            .items_center()
1202            .gap_2()
1203            .font_buffer(cx)
1204            .text_ui_sm(cx)
1205            .when(!selected, |this| {
1206                this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
1207            });
1208
1209        if is_tree_view {
1210            entry = entry.pl(px(8. + 12. * entry_details.depth as f32))
1211        } else {
1212            entry = entry.pl(px(8.))
1213        }
1214
1215        if selected {
1216            entry = entry.bg(cx.theme().status().info_background);
1217        }
1218
1219        entry = entry
1220            .child(
1221                Checkbox::new(
1222                    checkbox_id,
1223                    entry_details
1224                        .is_staged
1225                        .map_or(ToggleState::Indeterminate, ToggleState::from),
1226                )
1227                .fill()
1228                .elevation(ElevationIndex::Surface)
1229                .on_click({
1230                    let handle = handle.clone();
1231                    let repo_path = repo_path.clone();
1232                    move |toggle, cx| {
1233                        let Some(this) = handle.upgrade() else {
1234                            return;
1235                        };
1236                        this.update(cx, |this, cx| {
1237                            this.visible_entries[ix].is_staged = match *toggle {
1238                                ToggleState::Selected => Some(true),
1239                                ToggleState::Unselected => Some(false),
1240                                ToggleState::Indeterminate => None,
1241                            };
1242                            let repo_path = repo_path.clone();
1243                            let Some(git_state) = this.git_state(cx) else {
1244                                return;
1245                            };
1246                            git_state.update(cx, |git_state, _| match toggle {
1247                                ToggleState::Selected | ToggleState::Indeterminate => {
1248                                    git_state.stage_entries(vec![repo_path]);
1249                                }
1250                                ToggleState::Unselected => {
1251                                    git_state.unstage_entries(vec![repo_path])
1252                                }
1253                            })
1254                        });
1255                    }
1256                }),
1257            )
1258            .when(status_style == StatusStyle::Icon, |this| {
1259                this.child(git_status_icon(status))
1260            })
1261            .child(
1262                h_flex()
1263                    .text_color(label_color)
1264                    .when(status.is_deleted(), |this| this.line_through())
1265                    .when_some(repo_path.parent(), |this, parent| {
1266                        let parent_str = parent.to_string_lossy();
1267                        if !parent_str.is_empty() {
1268                            this.child(
1269                                div()
1270                                    .text_color(path_color)
1271                                    .child(format!("{}/", parent_str)),
1272                            )
1273                        } else {
1274                            this
1275                        }
1276                    })
1277                    .child(div().child(entry_details.display_name.clone())),
1278            )
1279            .child(div().flex_1())
1280            .child(end_slot)
1281            .on_click(move |_, cx| {
1282                // TODO: add `select_entry` method then do after that
1283                cx.dispatch_action(Box::new(OpenSelected));
1284
1285                handle
1286                    .update(cx, |git_panel, _| {
1287                        git_panel.selected_entry = Some(ix);
1288                    })
1289                    .ok();
1290            });
1291
1292        entry
1293    }
1294}
1295
1296impl Render for GitPanel {
1297    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1298        let project = self.project.read(cx);
1299
1300        v_flex()
1301            .id("git_panel")
1302            .key_context(self.dispatch_context(cx))
1303            .track_focus(&self.focus_handle)
1304            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
1305            .when(!project.is_read_only(cx), |this| {
1306                this.on_action(cx.listener(|this, &ToggleStaged, cx| {
1307                    this.toggle_staged_for_selected(&ToggleStaged, cx)
1308                }))
1309                .on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
1310                .on_action(cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)))
1311                .on_action(cx.listener(|this, &RevertAll, cx| this.discard_all(&RevertAll, cx)))
1312                .on_action(
1313                    cx.listener(|this, &CommitChanges, cx| this.commit_changes(&CommitChanges, cx)),
1314                )
1315                .on_action(cx.listener(|this, &CommitAllChanges, cx| {
1316                    this.commit_all_changes(&CommitAllChanges, cx)
1317                }))
1318            })
1319            .when(self.is_focused(cx), |this| {
1320                this.on_action(cx.listener(Self::select_first))
1321                    .on_action(cx.listener(Self::select_next))
1322                    .on_action(cx.listener(Self::select_prev))
1323                    .on_action(cx.listener(Self::select_last))
1324                    .on_action(cx.listener(Self::close_panel))
1325            })
1326            .on_action(cx.listener(Self::open_selected))
1327            .on_action(cx.listener(Self::focus_changes_list))
1328            .on_action(cx.listener(Self::focus_editor))
1329            .on_action(cx.listener(Self::toggle_staged_for_selected))
1330            // .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
1331            .on_hover(cx.listener(|this, hovered, cx| {
1332                if *hovered {
1333                    this.show_scrollbar = true;
1334                    this.hide_scrollbar_task.take();
1335                    cx.notify();
1336                } else if !this.focus_handle.contains_focused(cx) {
1337                    this.hide_scrollbar(cx);
1338                }
1339            }))
1340            .size_full()
1341            .overflow_hidden()
1342            .font_buffer(cx)
1343            .py_1()
1344            .bg(ElevationIndex::Surface.bg(cx))
1345            .child(self.render_panel_header(cx))
1346            .child(self.render_divider(cx))
1347            .child(if !self.no_entries(cx) {
1348                self.render_entries(cx).into_any_element()
1349            } else {
1350                self.render_empty_state(cx).into_any_element()
1351            })
1352            .child(self.render_divider(cx))
1353            .child(self.render_commit_editor(cx))
1354    }
1355}
1356
1357impl FocusableView for GitPanel {
1358    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1359        self.focus_handle.clone()
1360    }
1361}
1362
1363impl EventEmitter<Event> for GitPanel {}
1364
1365impl EventEmitter<PanelEvent> for GitPanel {}
1366
1367impl Panel for GitPanel {
1368    fn persistent_name() -> &'static str {
1369        "GitPanel"
1370    }
1371
1372    fn position(&self, cx: &WindowContext) -> DockPosition {
1373        GitPanelSettings::get_global(cx).dock
1374    }
1375
1376    fn position_is_valid(&self, position: DockPosition) -> bool {
1377        matches!(position, DockPosition::Left | DockPosition::Right)
1378    }
1379
1380    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1381        settings::update_settings_file::<GitPanelSettings>(
1382            self.fs.clone(),
1383            cx,
1384            move |settings, _| settings.dock = Some(position),
1385        );
1386    }
1387
1388    fn size(&self, cx: &WindowContext) -> Pixels {
1389        self.width
1390            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
1391    }
1392
1393    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1394        self.width = size;
1395        self.serialize(cx);
1396        cx.notify();
1397    }
1398
1399    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
1400        Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
1401    }
1402
1403    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1404        Some("Git Panel")
1405    }
1406
1407    fn toggle_action(&self) -> Box<dyn Action> {
1408        Box::new(ToggleFocus)
1409    }
1410
1411    fn activation_priority(&self) -> u32 {
1412        2
1413    }
1414}