git_panel.rs

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