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