git_panel.rs

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