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