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 mut ends_with_co_authors = false;
 753        let existing_co_authors = existing_text
 754            .lines()
 755            .filter_map(|line| {
 756                let line = line.trim();
 757                if line.starts_with(CO_AUTHOR_PREFIX) {
 758                    ends_with_co_authors = true;
 759                    Some(line)
 760                } else {
 761                    ends_with_co_authors = false;
 762                    None
 763                }
 764            })
 765            .collect::<HashSet<_>>();
 766
 767        let new_co_authors = room
 768            .read(cx)
 769            .remote_participants()
 770            .values()
 771            .filter(|participant| participant.can_write())
 772            .map(|participant| participant.user.clone())
 773            .filter_map(|user| {
 774                let email = user.email.as_deref()?;
 775                let name = user.name.as_deref().unwrap_or(&user.github_login);
 776                Some(format!("{CO_AUTHOR_PREFIX}{name} <{email}>"))
 777            })
 778            .filter(|co_author| {
 779                !existing_co_authors.contains(co_author.to_ascii_lowercase().as_str())
 780            })
 781            .collect::<Vec<_>>();
 782        if new_co_authors.is_empty() {
 783            return;
 784        }
 785
 786        self.commit_editor.update(cx, |editor, cx| {
 787            let editor_end = editor.buffer().read(cx).read(cx).len();
 788            let mut edit = String::new();
 789            if !ends_with_co_authors {
 790                edit.push('\n');
 791            }
 792            for co_author in new_co_authors {
 793                edit.push('\n');
 794                edit.push_str(&co_author);
 795            }
 796
 797            editor.edit(Some((editor_end..editor_end, edit)), cx);
 798            editor.move_to_end(&MoveToEnd, cx);
 799            editor.focus(cx);
 800        });
 801    }
 802
 803    fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
 804        self.git_state(cx)
 805            .map_or(true, |git_state| git_state.read(cx).entry_count() == 0)
 806    }
 807
 808    fn for_each_visible_entry(
 809        &self,
 810        range: Range<usize>,
 811        cx: &mut ViewContext<Self>,
 812        mut callback: impl FnMut(usize, GitListEntry, &mut ViewContext<Self>),
 813    ) {
 814        let visible_entries = &self.visible_entries;
 815
 816        for (ix, entry) in visible_entries
 817            .iter()
 818            .enumerate()
 819            .skip(range.start)
 820            .take(range.end - range.start)
 821        {
 822            let status = entry.status;
 823            let filename = entry
 824                .repo_path
 825                .file_name()
 826                .map(|name| name.to_string_lossy().into_owned())
 827                .unwrap_or_else(|| entry.repo_path.to_string_lossy().into_owned());
 828
 829            let details = GitListEntry {
 830                repo_path: entry.repo_path.clone(),
 831                status,
 832                depth: 0,
 833                display_name: filename,
 834                is_staged: entry.is_staged,
 835            };
 836
 837            callback(ix, details, cx);
 838        }
 839    }
 840
 841    fn schedule_update(&mut self) {
 842        self.rebuild_requested.store(true, Ordering::Relaxed);
 843    }
 844
 845    #[track_caller]
 846    fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
 847        self.visible_entries.clear();
 848
 849        let Some((_, repo, _)) = self.active_repository(cx) else {
 850            // Just clear entries if no repository is active.
 851            cx.notify();
 852            return;
 853        };
 854
 855        // First pass - collect all paths
 856        let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
 857
 858        // Second pass - create entries with proper depth calculation
 859        let mut all_staged = None;
 860        for (ix, entry) in repo.status().enumerate() {
 861            let (depth, difference) =
 862                Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
 863            let is_staged = entry.status.is_staged();
 864            all_staged = if ix == 0 {
 865                is_staged
 866            } else {
 867                match (all_staged, is_staged) {
 868                    (None, _) | (_, None) => None,
 869                    (Some(a), Some(b)) => (a == b).then_some(a),
 870                }
 871            };
 872
 873            let display_name = if difference > 1 {
 874                // Show partial path for deeply nested files
 875                entry
 876                    .repo_path
 877                    .as_ref()
 878                    .iter()
 879                    .skip(entry.repo_path.components().count() - difference)
 880                    .collect::<PathBuf>()
 881                    .to_string_lossy()
 882                    .into_owned()
 883            } else {
 884                // Just show filename
 885                entry
 886                    .repo_path
 887                    .file_name()
 888                    .map(|name| name.to_string_lossy().into_owned())
 889                    .unwrap_or_default()
 890            };
 891
 892            let entry = GitListEntry {
 893                depth,
 894                display_name,
 895                repo_path: entry.repo_path,
 896                status: entry.status,
 897                is_staged,
 898            };
 899
 900            self.visible_entries.push(entry);
 901        }
 902        self.all_staged = all_staged;
 903
 904        // Sort entries by path to maintain consistent order
 905        self.visible_entries
 906            .sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
 907
 908        self.select_first_entry_if_none(cx);
 909
 910        cx.notify();
 911    }
 912
 913    fn on_buffer_event(
 914        &mut self,
 915        _buffer: Model<Buffer>,
 916        event: &language::BufferEvent,
 917        cx: &mut ViewContext<Self>,
 918    ) {
 919        if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
 920            let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx));
 921
 922            let Some(git_state) = self.git_state(cx) else {
 923                return;
 924            };
 925            git_state.update(cx, |git_state, _| {
 926                git_state.commit_message = commit_message.into();
 927            });
 928
 929            cx.notify();
 930        }
 931    }
 932
 933    fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
 934        let Some(workspace) = self.weak_workspace.upgrade() else {
 935            return;
 936        };
 937        let notif_id = NotificationId::Named(id.into());
 938        let message = e.to_string();
 939        workspace.update(cx, |workspace, cx| {
 940            let toast = Toast::new(notif_id, message).on_click("Open Zed Log", |cx| {
 941                cx.dispatch_action(workspace::OpenLog.boxed_clone());
 942            });
 943            workspace.show_toast(toast, cx);
 944        });
 945    }
 946}
 947
 948// GitPanel –– Render
 949impl GitPanel {
 950    pub fn panel_button(
 951        &self,
 952        id: impl Into<SharedString>,
 953        label: impl Into<SharedString>,
 954    ) -> Button {
 955        let id = id.into().clone();
 956        let label = label.into().clone();
 957
 958        Button::new(id, label)
 959            .label_size(LabelSize::Small)
 960            .layer(ElevationIndex::ElevatedSurface)
 961            .size(ButtonSize::Compact)
 962            .style(ButtonStyle::Filled)
 963    }
 964
 965    pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 966        h_flex()
 967            .items_center()
 968            .h(px(8.))
 969            .child(Divider::horizontal_dashed().color(DividerColor::Border))
 970    }
 971
 972    pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 973        let focus_handle = self.focus_handle(cx).clone();
 974        let entry_count = self
 975            .git_state(cx)
 976            .map_or(0, |git_state| git_state.read(cx).entry_count());
 977
 978        let changes_string = match entry_count {
 979            0 => "No changes".to_string(),
 980            1 => "1 change".to_string(),
 981            n => format!("{} changes", n),
 982        };
 983
 984        // for our use case treat None as false
 985        let all_staged = self.all_staged.unwrap_or(false);
 986
 987        h_flex()
 988            .h(px(32.))
 989            .items_center()
 990            .px_2()
 991            .bg(ElevationIndex::Surface.bg(cx))
 992            .child(
 993                h_flex()
 994                    .gap_2()
 995                    .child(
 996                        Checkbox::new(
 997                            "all-changes",
 998                            if self.no_entries(cx) {
 999                                ToggleState::Selected
1000                            } else {
1001                                self.all_staged
1002                                    .map_or(ToggleState::Indeterminate, ToggleState::from)
1003                            },
1004                        )
1005                        .fill()
1006                        .elevation(ElevationIndex::Surface)
1007                        .tooltip(move |cx| {
1008                            if all_staged {
1009                                Tooltip::text("Unstage all changes", cx)
1010                            } else {
1011                                Tooltip::text("Stage all changes", cx)
1012                            }
1013                        })
1014                        .on_click(cx.listener(move |git_panel, _, cx| match all_staged {
1015                            true => git_panel.unstage_all(&UnstageAll, cx),
1016                            false => git_panel.stage_all(&StageAll, cx),
1017                        })),
1018                    )
1019                    .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
1020            )
1021            .child(div().flex_grow())
1022            .child(
1023                h_flex()
1024                    .gap_2()
1025                    // TODO: Re-add once revert all is added
1026                    // .child(
1027                    //     IconButton::new("discard-changes", IconName::Undo)
1028                    //         .tooltip({
1029                    //             let focus_handle = focus_handle.clone();
1030                    //             move |cx| {
1031                    //                 Tooltip::for_action_in(
1032                    //                     "Discard all changes",
1033                    //                     &RevertAll,
1034                    //                     &focus_handle,
1035                    //                     cx,
1036                    //                 )
1037                    //             }
1038                    //         })
1039                    //         .icon_size(IconSize::Small)
1040                    //         .disabled(true),
1041                    // )
1042                    .child(if self.all_staged.unwrap_or(false) {
1043                        self.panel_button("unstage-all", "Unstage All")
1044                            .tooltip({
1045                                let focus_handle = focus_handle.clone();
1046                                move |cx| {
1047                                    Tooltip::for_action_in(
1048                                        "Unstage all changes",
1049                                        &UnstageAll,
1050                                        &focus_handle,
1051                                        cx,
1052                                    )
1053                                }
1054                            })
1055                            .key_binding(ui::KeyBinding::for_action_in(
1056                                &UnstageAll,
1057                                &focus_handle,
1058                                cx,
1059                            ))
1060                            .on_click(
1061                                cx.listener(move |this, _, cx| this.unstage_all(&UnstageAll, cx)),
1062                            )
1063                    } else {
1064                        self.panel_button("stage-all", "Stage All")
1065                            .tooltip({
1066                                let focus_handle = focus_handle.clone();
1067                                move |cx| {
1068                                    Tooltip::for_action_in(
1069                                        "Stage all changes",
1070                                        &StageAll,
1071                                        &focus_handle,
1072                                        cx,
1073                                    )
1074                                }
1075                            })
1076                            .key_binding(ui::KeyBinding::for_action_in(
1077                                &StageAll,
1078                                &focus_handle,
1079                                cx,
1080                            ))
1081                            .on_click(cx.listener(move |this, _, cx| this.stage_all(&StageAll, cx)))
1082                    }),
1083            )
1084    }
1085
1086    pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
1087        let editor = self.commit_editor.clone();
1088        let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
1089        let (can_commit, can_commit_all) = self.git_state(cx).map_or((false, false), |git_state| {
1090            let git_state = git_state.read(cx);
1091            (git_state.can_commit(false), git_state.can_commit(true))
1092        });
1093
1094        let focus_handle_1 = self.focus_handle(cx).clone();
1095        let focus_handle_2 = self.focus_handle(cx).clone();
1096
1097        let commit_staged_button = self
1098            .panel_button("commit-staged-changes", "Commit")
1099            .tooltip(move |cx| {
1100                let focus_handle = focus_handle_1.clone();
1101                Tooltip::for_action_in(
1102                    "Commit all staged changes",
1103                    &CommitChanges,
1104                    &focus_handle,
1105                    cx,
1106                )
1107            })
1108            .disabled(!can_commit)
1109            .on_click(
1110                cx.listener(|this, _: &ClickEvent, cx| this.commit_changes(&CommitChanges, cx)),
1111            );
1112
1113        let commit_all_button = self
1114            .panel_button("commit-all-changes", "Commit All")
1115            .tooltip(move |cx| {
1116                let focus_handle = focus_handle_2.clone();
1117                Tooltip::for_action_in(
1118                    "Commit all changes, including unstaged changes",
1119                    &CommitAllChanges,
1120                    &focus_handle,
1121                    cx,
1122                )
1123            })
1124            .disabled(!can_commit_all)
1125            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
1126                this.commit_all_changes(&CommitAllChanges, cx)
1127            }));
1128
1129        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
1130            v_flex()
1131                .id("commit-editor-container")
1132                .relative()
1133                .h_full()
1134                .py_2p5()
1135                .px_3()
1136                .bg(cx.theme().colors().editor_background)
1137                .on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
1138                .child(self.commit_editor.clone())
1139                .child(
1140                    h_flex()
1141                        .absolute()
1142                        .bottom_2p5()
1143                        .right_3()
1144                        .child(div().gap_1().flex_grow())
1145                        .child(if self.current_modifiers.alt {
1146                            commit_all_button
1147                        } else {
1148                            commit_staged_button
1149                        }),
1150                ),
1151        )
1152    }
1153
1154    fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
1155        h_flex()
1156            .h_full()
1157            .flex_1()
1158            .justify_center()
1159            .items_center()
1160            .child(
1161                v_flex()
1162                    .gap_3()
1163                    .child("No changes to commit")
1164                    .text_ui_sm(cx)
1165                    .mx_auto()
1166                    .text_color(Color::Placeholder.color(cx)),
1167            )
1168    }
1169
1170    fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
1171        let scroll_bar_style = self.show_scrollbar(cx);
1172        let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
1173
1174        if !self.should_show_scrollbar(cx)
1175            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
1176        {
1177            return None;
1178        }
1179
1180        Some(
1181            div()
1182                .id("git-panel-vertical-scroll")
1183                .occlude()
1184                .flex_none()
1185                .h_full()
1186                .cursor_default()
1187                .when(show_container, |this| this.pl_1().px_1p5())
1188                .when(!show_container, |this| {
1189                    this.absolute().right_1().top_1().bottom_1().w(px(12.))
1190                })
1191                .on_mouse_move(cx.listener(|_, _, cx| {
1192                    cx.notify();
1193                    cx.stop_propagation()
1194                }))
1195                .on_hover(|_, cx| {
1196                    cx.stop_propagation();
1197                })
1198                .on_any_mouse_down(|_, cx| {
1199                    cx.stop_propagation();
1200                })
1201                .on_mouse_up(
1202                    MouseButton::Left,
1203                    cx.listener(|this, _, cx| {
1204                        if !this.scrollbar_state.is_dragging()
1205                            && !this.focus_handle.contains_focused(cx)
1206                        {
1207                            this.hide_scrollbar(cx);
1208                            cx.notify();
1209                        }
1210
1211                        cx.stop_propagation();
1212                    }),
1213                )
1214                .on_scroll_wheel(cx.listener(|_, _, cx| {
1215                    cx.notify();
1216                }))
1217                .children(Scrollbar::vertical(
1218                    // percentage as f32..end_offset as f32,
1219                    self.scrollbar_state.clone(),
1220                )),
1221        )
1222    }
1223
1224    fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1225        let entry_count = self.visible_entries.len();
1226
1227        h_flex()
1228            .size_full()
1229            .overflow_hidden()
1230            .child(
1231                uniform_list(cx.view().clone(), "entries", entry_count, {
1232                    move |git_panel, range, cx| {
1233                        let mut items = Vec::with_capacity(range.end - range.start);
1234                        git_panel.for_each_visible_entry(range, cx, |ix, details, cx| {
1235                            items.push(git_panel.render_entry(ix, details, cx));
1236                        });
1237                        items
1238                    }
1239                })
1240                .size_full()
1241                .with_sizing_behavior(ListSizingBehavior::Infer)
1242                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
1243                // .with_width_from_item(self.max_width_item_index)
1244                .track_scroll(self.scroll_handle.clone()),
1245            )
1246            .children(self.render_scrollbar(cx))
1247    }
1248
1249    fn render_entry(
1250        &self,
1251        ix: usize,
1252        entry_details: GitListEntry,
1253        cx: &ViewContext<Self>,
1254    ) -> impl IntoElement {
1255        let repo_path = entry_details.repo_path.clone();
1256        let selected = self.selected_entry == Some(ix);
1257        let status_style = GitPanelSettings::get_global(cx).status_style;
1258        let status = entry_details.status;
1259
1260        let mut label_color = cx.theme().colors().text;
1261        if status_style == StatusStyle::LabelColor {
1262            label_color = if status.is_conflicted() {
1263                cx.theme().status().conflict
1264            } else if status.is_modified() {
1265                cx.theme().status().modified
1266            } else if status.is_deleted() {
1267                cx.theme().colors().text_disabled
1268            } else {
1269                cx.theme().status().created
1270            }
1271        }
1272
1273        let path_color = status
1274            .is_deleted()
1275            .then_some(cx.theme().colors().text_disabled)
1276            .unwrap_or(cx.theme().colors().text_muted);
1277
1278        let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into());
1279        let checkbox_id =
1280            ElementId::Name(format!("checkbox_{}", entry_details.display_name).into());
1281        let is_tree_view = false;
1282        let handle = cx.view().downgrade();
1283
1284        let end_slot = h_flex()
1285            .invisible()
1286            .when(selected, |this| this.visible())
1287            .when(!selected, |this| {
1288                this.group_hover("git-panel-entry", |this| this.visible())
1289            })
1290            .gap_1()
1291            .items_center()
1292            .child(
1293                IconButton::new("more", IconName::EllipsisVertical)
1294                    .icon_color(Color::Placeholder)
1295                    .icon_size(IconSize::Small),
1296            );
1297
1298        let mut entry = h_flex()
1299            .id(entry_id)
1300            .group("git-panel-entry")
1301            .h(px(28.))
1302            .w_full()
1303            .pr(px(4.))
1304            .items_center()
1305            .gap_2()
1306            .font_buffer(cx)
1307            .text_ui_sm(cx)
1308            .when(!selected, |this| {
1309                this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
1310            });
1311
1312        if is_tree_view {
1313            entry = entry.pl(px(8. + 12. * entry_details.depth as f32))
1314        } else {
1315            entry = entry.pl(px(8.))
1316        }
1317
1318        if selected {
1319            entry = entry.bg(cx.theme().status().info_background);
1320        }
1321
1322        entry = entry
1323            .child(
1324                Checkbox::new(
1325                    checkbox_id,
1326                    entry_details
1327                        .is_staged
1328                        .map_or(ToggleState::Indeterminate, ToggleState::from),
1329                )
1330                .fill()
1331                .elevation(ElevationIndex::Surface)
1332                .on_click({
1333                    let handle = handle.clone();
1334                    let repo_path = repo_path.clone();
1335                    move |toggle, cx| {
1336                        let Some(this) = handle.upgrade() else {
1337                            return;
1338                        };
1339                        this.update(cx, |this, cx| {
1340                            this.visible_entries[ix].is_staged = match *toggle {
1341                                ToggleState::Selected => Some(true),
1342                                ToggleState::Unselected => Some(false),
1343                                ToggleState::Indeterminate => None,
1344                            };
1345                            let repo_path = repo_path.clone();
1346                            let Some(git_state) = this.git_state(cx) else {
1347                                return;
1348                            };
1349                            let result = git_state.update(cx, |git_state, _| match toggle {
1350                                ToggleState::Selected | ToggleState::Indeterminate => git_state
1351                                    .stage_entries(vec![repo_path], this.err_sender.clone()),
1352                                ToggleState::Unselected => git_state
1353                                    .unstage_entries(vec![repo_path], this.err_sender.clone()),
1354                            });
1355                            if let Err(e) = result {
1356                                this.show_err_toast("toggle staged error", e, cx);
1357                            }
1358                        });
1359                    }
1360                }),
1361            )
1362            .when(status_style == StatusStyle::Icon, |this| {
1363                this.child(git_status_icon(status))
1364            })
1365            .child(
1366                h_flex()
1367                    .text_color(label_color)
1368                    .when(status.is_deleted(), |this| this.line_through())
1369                    .when_some(repo_path.parent(), |this, parent| {
1370                        let parent_str = parent.to_string_lossy();
1371                        if !parent_str.is_empty() {
1372                            this.child(
1373                                div()
1374                                    .text_color(path_color)
1375                                    .child(format!("{}/", parent_str)),
1376                            )
1377                        } else {
1378                            this
1379                        }
1380                    })
1381                    .child(div().child(entry_details.display_name.clone())),
1382            )
1383            .child(div().flex_1())
1384            .child(end_slot)
1385            .on_click(move |_, cx| {
1386                // TODO: add `select_entry` method then do after that
1387                cx.dispatch_action(Box::new(OpenSelected));
1388
1389                handle
1390                    .update(cx, |git_panel, _| {
1391                        git_panel.selected_entry = Some(ix);
1392                    })
1393                    .ok();
1394            });
1395
1396        entry
1397    }
1398}
1399
1400impl Render for GitPanel {
1401    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1402        let project = self.project.read(cx);
1403        let has_co_authors = self
1404            .workspace
1405            .upgrade()
1406            .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned())
1407            .map(|room| {
1408                let room = room.read(cx);
1409                room.local_participant().can_write()
1410                    && room
1411                        .remote_participants()
1412                        .values()
1413                        .any(|remote_participant| remote_participant.can_write())
1414            })
1415            .unwrap_or(false);
1416
1417        v_flex()
1418            .id("git_panel")
1419            .key_context(self.dispatch_context(cx))
1420            .track_focus(&self.focus_handle)
1421            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
1422            .when(!project.is_read_only(cx), |this| {
1423                this.on_action(cx.listener(|this, &ToggleStaged, cx| {
1424                    this.toggle_staged_for_selected(&ToggleStaged, cx)
1425                }))
1426                .on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
1427                .on_action(cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)))
1428                .on_action(cx.listener(|this, &RevertAll, cx| this.discard_all(&RevertAll, cx)))
1429                .on_action(
1430                    cx.listener(|this, &CommitChanges, cx| this.commit_changes(&CommitChanges, cx)),
1431                )
1432                .on_action(cx.listener(|this, &CommitAllChanges, cx| {
1433                    this.commit_all_changes(&CommitAllChanges, cx)
1434                }))
1435            })
1436            .when(self.is_focused(cx), |this| {
1437                this.on_action(cx.listener(Self::select_first))
1438                    .on_action(cx.listener(Self::select_next))
1439                    .on_action(cx.listener(Self::select_prev))
1440                    .on_action(cx.listener(Self::select_last))
1441                    .on_action(cx.listener(Self::close_panel))
1442            })
1443            .on_action(cx.listener(Self::open_selected))
1444            .on_action(cx.listener(Self::focus_changes_list))
1445            .on_action(cx.listener(Self::focus_editor))
1446            .on_action(cx.listener(Self::toggle_staged_for_selected))
1447            .when(has_co_authors, |git_panel| {
1448                git_panel.on_action(cx.listener(Self::fill_co_authors))
1449            })
1450            // .on_action(cx.listener(|this, &OpenSelected, cx| this.open_selected(&OpenSelected, cx)))
1451            .on_hover(cx.listener(|this, hovered, cx| {
1452                if *hovered {
1453                    this.show_scrollbar = true;
1454                    this.hide_scrollbar_task.take();
1455                    cx.notify();
1456                } else if !this.focus_handle.contains_focused(cx) {
1457                    this.hide_scrollbar(cx);
1458                }
1459            }))
1460            .size_full()
1461            .overflow_hidden()
1462            .font_buffer(cx)
1463            .py_1()
1464            .bg(ElevationIndex::Surface.bg(cx))
1465            .child(self.render_panel_header(cx))
1466            .child(self.render_divider(cx))
1467            .child(if !self.no_entries(cx) {
1468                self.render_entries(cx).into_any_element()
1469            } else {
1470                self.render_empty_state(cx).into_any_element()
1471            })
1472            .child(self.render_divider(cx))
1473            .child(self.render_commit_editor(cx))
1474    }
1475}
1476
1477impl FocusableView for GitPanel {
1478    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1479        self.focus_handle.clone()
1480    }
1481}
1482
1483impl EventEmitter<Event> for GitPanel {}
1484
1485impl EventEmitter<PanelEvent> for GitPanel {}
1486
1487impl Panel for GitPanel {
1488    fn persistent_name() -> &'static str {
1489        "GitPanel"
1490    }
1491
1492    fn position(&self, cx: &WindowContext) -> DockPosition {
1493        GitPanelSettings::get_global(cx).dock
1494    }
1495
1496    fn position_is_valid(&self, position: DockPosition) -> bool {
1497        matches!(position, DockPosition::Left | DockPosition::Right)
1498    }
1499
1500    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1501        settings::update_settings_file::<GitPanelSettings>(
1502            self.fs.clone(),
1503            cx,
1504            move |settings, _| settings.dock = Some(position),
1505        );
1506    }
1507
1508    fn size(&self, cx: &WindowContext) -> Pixels {
1509        self.width
1510            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
1511    }
1512
1513    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1514        self.width = size;
1515        self.serialize(cx);
1516        cx.notify();
1517    }
1518
1519    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
1520        Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
1521    }
1522
1523    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1524        Some("Git Panel")
1525    }
1526
1527    fn toggle_action(&self) -> Box<dyn Action> {
1528        Box::new(ToggleFocus)
1529    }
1530
1531    fn activation_priority(&self) -> u32 {
1532        2
1533    }
1534}