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