git_panel.rs

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