git_panel.rs

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