git_panel.rs

   1use crate::{git_status_icon, settings::GitPanelSettings};
   2use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
   3use anyhow::Result;
   4use db::kvp::KEY_VALUE_STORE;
   5use git::{
   6    diff::DiffHunk,
   7    repository::{GitFileStatus, RepoPath},
   8};
   9use gpui::*;
  10use gpui::{
  11    actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
  12    CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
  13    ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
  14    MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
  15};
  16use menu::{SelectNext, SelectPrev};
  17use project::{EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
  18use serde::{Deserialize, Serialize};
  19use settings::Settings as _;
  20use std::{
  21    cell::OnceCell,
  22    collections::HashSet,
  23    ffi::OsStr,
  24    ops::{Deref, Range},
  25    path::PathBuf,
  26    rc::Rc,
  27    sync::Arc,
  28    time::Duration,
  29    usize,
  30};
  31use ui::{
  32    prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
  33    ScrollbarState, Tooltip,
  34};
  35use util::{ResultExt, TryFutureExt};
  36use workspace::{
  37    dock::{DockPosition, Panel, PanelEvent},
  38    Workspace,
  39};
  40use worktree::StatusEntry;
  41
  42actions!(git_panel, [ToggleFocus]);
  43
  44const GIT_PANEL_KEY: &str = "GitPanel";
  45
  46const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  47
  48pub fn init(cx: &mut AppContext) {
  49    cx.observe_new_views(
  50        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
  51            workspace.register_action(|workspace, _: &ToggleFocus, cx| {
  52                workspace.toggle_panel_focus::<GitPanel>(cx);
  53            });
  54        },
  55    )
  56    .detach();
  57}
  58
  59#[derive(Debug)]
  60pub enum Event {
  61    Focus,
  62}
  63
  64pub struct GitStatusEntry {}
  65
  66#[derive(Debug, PartialEq, Eq, Clone)]
  67struct EntryDetails {
  68    filename: String,
  69    display_name: String,
  70    path: RepoPath,
  71    kind: EntryKind,
  72    depth: usize,
  73    is_expanded: bool,
  74    status: Option<GitFileStatus>,
  75    hunks: Rc<OnceCell<Vec<DiffHunk>>>,
  76    index: usize,
  77}
  78
  79impl EntryDetails {
  80    pub fn is_dir(&self) -> bool {
  81        self.kind.is_dir()
  82    }
  83}
  84
  85#[derive(Serialize, Deserialize)]
  86struct SerializedGitPanel {
  87    width: Option<Pixels>,
  88}
  89
  90pub struct GitPanel {
  91    // workspace: WeakView<Workspace>,
  92    current_modifiers: Modifiers,
  93    focus_handle: FocusHandle,
  94    fs: Arc<dyn Fs>,
  95    hide_scrollbar_task: Option<Task<()>>,
  96    pending_serialization: Task<Option<()>>,
  97    project: Model<Project>,
  98    scroll_handle: UniformListScrollHandle,
  99    scrollbar_state: ScrollbarState,
 100    selected_item: Option<usize>,
 101    show_scrollbar: bool,
 102    // TODO Reintroduce expanded directories, once we're deriving directories from paths
 103    // expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
 104
 105    // The entries that are currently shown in the panel, aka
 106    // not hidden by folding or such
 107    visible_entries: Vec<WorktreeEntries>,
 108    width: Option<Pixels>,
 109    // git_diff_editor: Option<View<Editor>>,
 110    // git_diff_editor_updates: Task<()>,
 111    reveal_in_editor: Task<()>,
 112}
 113
 114#[derive(Debug, Clone)]
 115struct WorktreeEntries {
 116    worktree_id: WorktreeId,
 117    // TODO support multiple repositories per worktree
 118    // work_directory: worktree::WorkDirectory,
 119    visible_entries: Vec<GitPanelEntry>,
 120    paths: Rc<OnceCell<HashSet<RepoPath>>>,
 121}
 122
 123#[derive(Debug, Clone)]
 124struct GitPanelEntry {
 125    entry: worktree::StatusEntry,
 126    hunks: Rc<OnceCell<Vec<DiffHunk>>>,
 127}
 128
 129impl Deref for GitPanelEntry {
 130    type Target = worktree::StatusEntry;
 131
 132    fn deref(&self) -> &Self::Target {
 133        &self.entry
 134    }
 135}
 136
 137impl WorktreeEntries {
 138    fn paths(&self) -> &HashSet<RepoPath> {
 139        self.paths.get_or_init(|| {
 140            self.visible_entries
 141                .iter()
 142                .map(|e| (e.entry.repo_path.clone()))
 143                .collect()
 144        })
 145    }
 146}
 147
 148impl GitPanel {
 149    pub fn load(
 150        workspace: WeakView<Workspace>,
 151        cx: AsyncWindowContext,
 152    ) -> Task<Result<View<Self>>> {
 153        cx.spawn(|mut cx| async move { workspace.update(&mut cx, Self::new) })
 154    }
 155
 156    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 157        let fs = workspace.app_state().fs.clone();
 158        // let weak_workspace = workspace.weak_handle();
 159        let project = workspace.project().clone();
 160
 161        let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
 162            let focus_handle = cx.focus_handle();
 163            cx.on_focus(&focus_handle, Self::focus_in).detach();
 164            cx.on_focus_out(&focus_handle, |this, _, cx| {
 165                this.hide_scrollbar(cx);
 166            })
 167            .detach();
 168            cx.subscribe(&project, |this, _, event, cx| match event {
 169                project::Event::WorktreeRemoved(_id) => {
 170                    // this.expanded_dir_ids.remove(id);
 171                    this.update_visible_entries(None, None, cx);
 172                    cx.notify();
 173                }
 174                project::Event::WorktreeOrderChanged => {
 175                    this.update_visible_entries(None, None, cx);
 176                    cx.notify();
 177                }
 178                project::Event::WorktreeUpdatedEntries(id, _)
 179                | project::Event::WorktreeAdded(id)
 180                | project::Event::WorktreeUpdatedGitRepositories(id) => {
 181                    this.update_visible_entries(Some(*id), None, cx);
 182                    cx.notify();
 183                }
 184                project::Event::Closed => {
 185                    // this.git_diff_editor_updates = Task::ready(());
 186                    this.reveal_in_editor = Task::ready(());
 187                    // this.expanded_dir_ids.clear();
 188                    this.visible_entries.clear();
 189                    // this.git_diff_editor = None;
 190                }
 191                _ => {}
 192            })
 193            .detach();
 194
 195            let scroll_handle = UniformListScrollHandle::new();
 196
 197            let mut git_panel = Self {
 198                // workspace: weak_workspace,
 199                focus_handle: cx.focus_handle(),
 200                fs,
 201                pending_serialization: Task::ready(None),
 202                visible_entries: Vec::new(),
 203                current_modifiers: cx.modifiers(),
 204                // expanded_dir_ids: Default::default(),
 205                width: Some(px(360.)),
 206                scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
 207                scroll_handle,
 208                selected_item: None,
 209                show_scrollbar: !Self::should_autohide_scrollbar(cx),
 210                hide_scrollbar_task: None,
 211                // git_diff_editor: Some(diff_display_editor(cx)),
 212                // git_diff_editor_updates: Task::ready(()),
 213                reveal_in_editor: Task::ready(()),
 214                project,
 215            };
 216            git_panel.update_visible_entries(None, None, cx);
 217            git_panel
 218        });
 219
 220        git_panel
 221    }
 222
 223    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 224        let width = self.width;
 225        self.pending_serialization = cx.background_executor().spawn(
 226            async move {
 227                KEY_VALUE_STORE
 228                    .write_kvp(
 229                        GIT_PANEL_KEY.into(),
 230                        serde_json::to_string(&SerializedGitPanel { width })?,
 231                    )
 232                    .await?;
 233                anyhow::Ok(())
 234            }
 235            .log_err(),
 236        );
 237    }
 238
 239    fn dispatch_context(&self) -> KeyContext {
 240        let mut dispatch_context = KeyContext::new_with_defaults();
 241        dispatch_context.add("GitPanel");
 242        dispatch_context.add("menu");
 243
 244        dispatch_context
 245    }
 246
 247    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 248        if !self.focus_handle.contains_focused(cx) {
 249            cx.emit(Event::Focus);
 250        }
 251    }
 252
 253    fn should_show_scrollbar(_cx: &AppContext) -> bool {
 254        // TODO: plug into settings
 255        true
 256    }
 257
 258    fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
 259        // TODO: plug into settings
 260        true
 261    }
 262
 263    fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
 264        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 265        if !Self::should_autohide_scrollbar(cx) {
 266            return;
 267        }
 268        self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
 269            cx.background_executor()
 270                .timer(SCROLLBAR_SHOW_INTERVAL)
 271                .await;
 272            panel
 273                .update(&mut cx, |panel, cx| {
 274                    panel.show_scrollbar = false;
 275                    cx.notify();
 276                })
 277                .log_err();
 278        }))
 279    }
 280
 281    fn handle_modifiers_changed(
 282        &mut self,
 283        event: &ModifiersChangedEvent,
 284        cx: &mut ViewContext<Self>,
 285    ) {
 286        self.current_modifiers = event.modifiers;
 287        cx.notify();
 288    }
 289
 290    fn calculate_depth_and_difference(
 291        entry: &StatusEntry,
 292        visible_worktree_entries: &HashSet<RepoPath>,
 293    ) -> (usize, usize) {
 294        let (depth, difference) = entry
 295            .repo_path
 296            .ancestors()
 297            .skip(1) // Skip the entry itself
 298            .find_map(|ancestor| {
 299                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
 300                    let entry_path_components_count = entry.repo_path.components().count();
 301                    let parent_path_components_count = parent_entry.components().count();
 302                    let difference = entry_path_components_count - parent_path_components_count;
 303                    let depth = parent_entry
 304                        .ancestors()
 305                        .skip(1)
 306                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
 307                        .count();
 308                    Some((depth + 1, difference))
 309                } else {
 310                    None
 311                }
 312            })
 313            .unwrap_or((0, 0));
 314
 315        (depth, difference)
 316    }
 317
 318    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 319        let item_count = self
 320            .visible_entries
 321            .iter()
 322            .map(|worktree_entries| worktree_entries.visible_entries.len())
 323            .sum::<usize>();
 324        if item_count == 0 {
 325            return;
 326        }
 327        let selection = match self.selected_item {
 328            Some(i) => {
 329                if i < item_count - 1 {
 330                    self.selected_item = Some(i + 1);
 331                    i + 1
 332                } else {
 333                    self.selected_item = Some(0);
 334                    0
 335                }
 336            }
 337            None => {
 338                self.selected_item = Some(0);
 339                0
 340            }
 341        };
 342        self.scroll_handle
 343            .scroll_to_item(selection, ScrollStrategy::Center);
 344
 345        let mut hunks = None;
 346        self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
 347            hunks = Some(entry.hunks.clone());
 348        });
 349        if let Some(hunks) = hunks {
 350            self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
 351        }
 352
 353        cx.notify();
 354    }
 355
 356    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 357        let item_count = self
 358            .visible_entries
 359            .iter()
 360            .map(|worktree_entries| worktree_entries.visible_entries.len())
 361            .sum::<usize>();
 362        if item_count == 0 {
 363            return;
 364        }
 365        let selection = match self.selected_item {
 366            Some(i) => {
 367                if i > 0 {
 368                    self.selected_item = Some(i - 1);
 369                    i - 1
 370                } else {
 371                    self.selected_item = Some(item_count - 1);
 372                    item_count - 1
 373                }
 374            }
 375            None => {
 376                self.selected_item = Some(0);
 377                0
 378            }
 379        };
 380        self.scroll_handle
 381            .scroll_to_item(selection, ScrollStrategy::Center);
 382
 383        let mut hunks = None;
 384        self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
 385            hunks = Some(entry.hunks.clone());
 386        });
 387        if let Some(hunks) = hunks {
 388            self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
 389        }
 390
 391        cx.notify();
 392    }
 393}
 394
 395impl GitPanel {
 396    fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
 397        // TODO: Implement stage all
 398        println!("Stage all triggered");
 399    }
 400
 401    fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
 402        // TODO: Implement unstage all
 403        println!("Unstage all triggered");
 404    }
 405
 406    fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
 407        // TODO: Implement discard all
 408        println!("Discard all triggered");
 409    }
 410
 411    /// Commit all staged changes
 412    fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
 413        // TODO: Implement commit all staged
 414        println!("Commit staged changes triggered");
 415    }
 416
 417    /// Commit all changes, regardless of whether they are staged or not
 418    fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
 419        // TODO: Implement commit all changes
 420        println!("Commit all changes triggered");
 421    }
 422
 423    fn all_staged(&self) -> bool {
 424        // TODO: Implement all_staged
 425        true
 426    }
 427
 428    fn no_entries(&self) -> bool {
 429        self.visible_entries.is_empty()
 430    }
 431
 432    fn entry_count(&self) -> usize {
 433        self.visible_entries
 434            .iter()
 435            .map(|worktree_entries| worktree_entries.visible_entries.len())
 436            .sum()
 437    }
 438
 439    fn for_each_visible_entry(
 440        &self,
 441        range: Range<usize>,
 442        cx: &mut ViewContext<Self>,
 443        mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext<Self>),
 444    ) {
 445        let mut ix = 0;
 446        for worktree_entries in &self.visible_entries {
 447            if ix >= range.end {
 448                return;
 449            }
 450
 451            if ix + worktree_entries.visible_entries.len() <= range.start {
 452                ix += worktree_entries.visible_entries.len();
 453                continue;
 454            }
 455
 456            let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
 457            // let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
 458            if let Some(worktree) = self
 459                .project
 460                .read(cx)
 461                .worktree_for_id(worktree_entries.worktree_id, cx)
 462            {
 463                let snapshot = worktree.read(cx).snapshot();
 464                let root_name = OsStr::new(snapshot.root_name());
 465                // let expanded_entry_ids = self
 466                //     .expanded_dir_ids
 467                //     .get(&snapshot.id())
 468                //     .map(Vec::as_slice)
 469                //     .unwrap_or(&[]);
 470
 471                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
 472                let entries = worktree_entries.paths();
 473
 474                let index_start = entry_range.start;
 475                for (i, entry) in worktree_entries.visible_entries[entry_range]
 476                    .iter()
 477                    .enumerate()
 478                {
 479                    let index = index_start + i;
 480                    let status = entry.status;
 481                    let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
 482
 483                    let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
 484
 485                    let filename = match difference {
 486                        diff if diff > 1 => entry
 487                            .repo_path
 488                            .iter()
 489                            .skip(entry.repo_path.components().count() - diff)
 490                            .collect::<PathBuf>()
 491                            .to_str()
 492                            .unwrap_or_default()
 493                            .to_string(),
 494                        _ => entry
 495                            .repo_path
 496                            .file_name()
 497                            .map(|name| name.to_string_lossy().into_owned())
 498                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
 499                    };
 500
 501                    let details = EntryDetails {
 502                        filename,
 503                        display_name: entry.repo_path.to_string_lossy().into_owned(),
 504                        // TODO get it from StatusEntry?
 505                        kind: EntryKind::File,
 506                        is_expanded,
 507                        path: entry.repo_path.clone(),
 508                        status: Some(status),
 509                        hunks: entry.hunks.clone(),
 510                        depth,
 511                        index,
 512                    };
 513                    callback(ix, details, cx);
 514                }
 515            }
 516            ix = end_ix;
 517        }
 518    }
 519
 520    // TODO: Update expanded directory state
 521    // TODO: Updates happen in the main loop, could be long for large workspaces
 522    #[track_caller]
 523    fn update_visible_entries(
 524        &mut self,
 525        for_worktree: Option<WorktreeId>,
 526        _new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
 527        cx: &mut ViewContext<Self>,
 528    ) {
 529        let project = self.project.read(cx);
 530        let mut old_entries_removed = false;
 531        let mut after_update = Vec::new();
 532        self.visible_entries
 533            .retain(|worktree_entries| match for_worktree {
 534                Some(for_worktree) => {
 535                    if worktree_entries.worktree_id == for_worktree {
 536                        old_entries_removed = true;
 537                        false
 538                    } else if old_entries_removed {
 539                        after_update.push(worktree_entries.clone());
 540                        false
 541                    } else {
 542                        true
 543                    }
 544                }
 545                None => false,
 546            });
 547        for worktree in project.visible_worktrees(cx) {
 548            let snapshot = worktree.read(cx).snapshot();
 549            let worktree_id = snapshot.id();
 550
 551            if for_worktree.is_some() && for_worktree != Some(worktree_id) {
 552                continue;
 553            }
 554
 555            let mut visible_worktree_entries = Vec::new();
 556            // Only use the first repository for now
 557            let repositories = snapshot.repositories().take(1);
 558            // let mut work_directory = None;
 559            for repository in repositories {
 560                visible_worktree_entries.extend(repository.status());
 561                // work_directory = Some(worktree::WorkDirectory::clone(repository));
 562            }
 563
 564            // TODO use the GitTraversal
 565            // let mut visible_worktree_entries = snapshot
 566            //     .entries(false, 0)
 567            //     .filter(|entry| !entry.is_external)
 568            //     .filter(|entry| entry.git_status.is_some())
 569            //     .cloned()
 570            //     .collect::<Vec<_>>();
 571            // snapshot.propagate_git_statuses(&mut visible_worktree_entries);
 572            // project::sort_worktree_entries(&mut visible_worktree_entries);
 573
 574            if !visible_worktree_entries.is_empty() {
 575                self.visible_entries.push(WorktreeEntries {
 576                    worktree_id,
 577                    // work_directory: work_directory.unwrap(),
 578                    visible_entries: visible_worktree_entries
 579                        .into_iter()
 580                        .map(|entry| GitPanelEntry {
 581                            entry,
 582                            hunks: Rc::default(),
 583                        })
 584                        .collect(),
 585                    paths: Rc::default(),
 586                });
 587            }
 588        }
 589        self.visible_entries.extend(after_update);
 590
 591        // TODO re-implement this
 592        // if let Some((worktree_id, entry_id)) = new_selected_entry {
 593        //     self.selected_item = self.visible_entries.iter().enumerate().find_map(
 594        //         |(worktree_index, worktree_entries)| {
 595        //             if worktree_entries.worktree_id == worktree_id {
 596        //                 worktree_entries
 597        //                     .visible_entries
 598        //                     .iter()
 599        //                     .position(|entry| entry.id == entry_id)
 600        //                     .map(|entry_index| {
 601        //                         worktree_index * worktree_entries.visible_entries.len()
 602        //                             + entry_index
 603        //                     })
 604        //             } else {
 605        //                 None
 606        //             }
 607        //         },
 608        //     );
 609        // }
 610
 611        // let project = self.project.downgrade();
 612        // self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
 613        //     cx.background_executor()
 614        //         .timer(UPDATE_DEBOUNCE)
 615        //         .await;
 616        //     let Some(project_buffers) = git_panel
 617        //         .update(&mut cx, |git_panel, cx| {
 618        //             futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
 619        //                 |worktree_entries| {
 620        //                     worktree_entries
 621        //                         .visible_entries
 622        //                         .iter()
 623        //                         .filter_map(|entry| {
 624        //                             let git_status = entry.status;
 625        //                             let entry_hunks = entry.hunks.clone();
 626        //                             let (entry_path, unstaged_changes_task) =
 627        //                                 project.update(cx, |project, cx| {
 628        //                                     let entry_path = ProjectPath {
 629        //                                         worktree_id: worktree_entries.worktree_id,
 630        //                                         path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
 631        //                                     };
 632        //                                     let open_task =
 633        //                                         project.open_path(entry_path.clone(), cx);
 634        //                                     let unstaged_changes_task =
 635        //                                         cx.spawn(|project, mut cx| async move {
 636        //                                             let (_, opened_model) = open_task
 637        //                                                 .await
 638        //                                                 .context("opening buffer")?;
 639        //                                             let buffer = opened_model
 640        //                                                 .downcast::<Buffer>()
 641        //                                                 .map_err(|_| {
 642        //                                                     anyhow::anyhow!(
 643        //                                                         "accessing buffer for entry"
 644        //                                                     )
 645        //                                                 })?;
 646        //                                             // TODO added files have noop changes and those are not expanded properly in the multi buffer
 647        //                                             let unstaged_changes = project
 648        //                                                 .update(&mut cx, |project, cx| {
 649        //                                                     project.open_unstaged_changes(
 650        //                                                         buffer.clone(),
 651        //                                                         cx,
 652        //                                                     )
 653        //                                                 })?
 654        //                                                 .await
 655        //                                                 .context("opening unstaged changes")?;
 656
 657        //                                             let hunks = cx.update(|cx| {
 658        //                                                 entry_hunks
 659        //                                                     .get_or_init(|| {
 660        //                                                         match git_status {
 661        //                                                             GitFileStatus::Added => {
 662        //                                                                 let buffer_snapshot = buffer.read(cx).snapshot();
 663        //                                                                 let entire_buffer_range =
 664        //                                                                     buffer_snapshot.anchor_after(0)
 665        //                                                                         ..buffer_snapshot
 666        //                                                                             .anchor_before(
 667        //                                                                                 buffer_snapshot.len(),
 668        //                                                                             );
 669        //                                                                 let entire_buffer_point_range =
 670        //                                                                     entire_buffer_range
 671        //                                                                         .clone()
 672        //                                                                         .to_point(&buffer_snapshot);
 673
 674        //                                                                 vec![DiffHunk {
 675        //                                                                     row_range: entire_buffer_point_range
 676        //                                                                         .start
 677        //                                                                         .row
 678        //                                                                         ..entire_buffer_point_range
 679        //                                                                             .end
 680        //                                                                             .row,
 681        //                                                                     buffer_range: entire_buffer_range,
 682        //                                                                     diff_base_byte_range: 0..0,
 683        //                                                                 }]
 684        //                                                             }
 685        //                                                             GitFileStatus::Modified => {
 686        //                                                                     let buffer_snapshot =
 687        //                                                                         buffer.read(cx).snapshot();
 688        //                                                                     unstaged_changes.read(cx)
 689        //                                                                         .diff_to_buffer
 690        //                                                                         .hunks_in_row_range(
 691        //                                                                             0..BufferRow::MAX,
 692        //                                                                             &buffer_snapshot,
 693        //                                                                         )
 694        //                                                                         .collect()
 695        //                                                             }
 696        //                                                             // TODO support these
 697        //                                                             GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
 698        //                                                         }
 699        //                                                     }).clone()
 700        //                                             })?;
 701
 702        //                                             anyhow::Ok((buffer, unstaged_changes, hunks))
 703        //                                         });
 704        //                                     Some((entry_path, unstaged_changes_task))
 705        //                                 }).ok()??;
 706        //                             Some((entry_path, unstaged_changes_task))
 707        //                         })
 708        //                         .map(|(entry_path, open_task)| async move {
 709        //                             (entry_path, open_task.await)
 710        //                         })
 711        //                         .collect::<Vec<_>>()
 712        //                 },
 713        //             ))
 714        //         })
 715        //         .ok()
 716        //     else {
 717        //         return;
 718        //     };
 719
 720        //     let project_buffers = project_buffers.await;
 721        //     if project_buffers.is_empty() {
 722        //         return;
 723        //     }
 724        //     let mut change_sets = Vec::with_capacity(project_buffers.len());
 725        //     if let Some(buffer_update_task) = git_panel
 726        //         .update(&mut cx, |git_panel, cx| {
 727        //             let editor = git_panel.git_diff_editor.clone()?;
 728        //             let multi_buffer = editor.read(cx).buffer().clone();
 729        //             let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
 730        //             for (buffer_path, open_result) in project_buffers {
 731        //                 if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
 732        //                     .with_context(|| format!("opening buffer {buffer_path:?}"))
 733        //                     .log_err()
 734        //                 {
 735        //                     change_sets.push(unstaged_changes);
 736        //                     buffers_with_ranges.push((
 737        //                         buffer,
 738        //                         diff_hunks
 739        //                             .into_iter()
 740        //                             .map(|hunk| hunk.buffer_range)
 741        //                             .collect(),
 742        //                     ));
 743        //                 }
 744        //             }
 745
 746        //             Some(multi_buffer.update(cx, |multi_buffer, cx| {
 747        //                 multi_buffer.clear(cx);
 748        //                 multi_buffer.push_multiple_excerpts_with_context_lines(
 749        //                     buffers_with_ranges,
 750        //                     DEFAULT_MULTIBUFFER_CONTEXT,
 751        //                     cx,
 752        //                 )
 753        //             }))
 754        //         })
 755        //         .ok().flatten()
 756        //     {
 757        //         buffer_update_task.await;
 758        //         git_panel
 759        //             .update(&mut cx, |git_panel, cx| {
 760        //                 if let Some(diff_editor) = git_panel.git_diff_editor.as_ref() {
 761        //                     diff_editor.update(cx, |editor, cx| {
 762        //                         for change_set in change_sets {
 763        //                             editor.add_change_set(change_set, cx);
 764        //                         }
 765        //                     });
 766        //                 }
 767        //             })
 768        //             .ok();
 769        //     }
 770        // });
 771
 772        cx.notify();
 773    }
 774}
 775
 776impl GitPanel {
 777    pub fn panel_button(
 778        &self,
 779        id: impl Into<SharedString>,
 780        label: impl Into<SharedString>,
 781    ) -> Button {
 782        let id = id.into().clone();
 783        let label = label.into().clone();
 784
 785        Button::new(id, label)
 786            .label_size(LabelSize::Small)
 787            .layer(ElevationIndex::ElevatedSurface)
 788            .size(ButtonSize::Compact)
 789            .style(ButtonStyle::Filled)
 790    }
 791
 792    pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 793        h_flex()
 794            .items_center()
 795            .h(px(8.))
 796            .child(Divider::horizontal_dashed().color(DividerColor::Border))
 797    }
 798
 799    pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 800        let focus_handle = self.focus_handle(cx).clone();
 801
 802        let changes_string = format!("{} changes", self.entry_count());
 803
 804        h_flex()
 805            .h(px(32.))
 806            .items_center()
 807            .px_3()
 808            .bg(ElevationIndex::Surface.bg(cx))
 809            .child(
 810                h_flex()
 811                    .gap_2()
 812                    .child(Checkbox::new("all-changes", true.into()).disabled(true))
 813                    .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
 814            )
 815            .child(div().flex_grow())
 816            .child(
 817                h_flex()
 818                    .gap_2()
 819                    .child(
 820                        IconButton::new("discard-changes", IconName::Undo)
 821                            .tooltip(move |cx| {
 822                                let focus_handle = focus_handle.clone();
 823
 824                                Tooltip::for_action_in(
 825                                    "Discard all changes",
 826                                    &DiscardAll,
 827                                    &focus_handle,
 828                                    cx,
 829                                )
 830                            })
 831                            .icon_size(IconSize::Small)
 832                            .disabled(true),
 833                    )
 834                    .child(if self.all_staged() {
 835                        self.panel_button("unstage-all", "Unstage All").on_click(
 836                            cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
 837                        )
 838                    } else {
 839                        self.panel_button("stage-all", "Stage All").on_click(
 840                            cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
 841                        )
 842                    }),
 843            )
 844    }
 845
 846    pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
 847        let focus_handle_1 = self.focus_handle(cx).clone();
 848        let focus_handle_2 = self.focus_handle(cx).clone();
 849
 850        let commit_staged_button = self
 851            .panel_button("commit-staged-changes", "Commit")
 852            .tooltip(move |cx| {
 853                let focus_handle = focus_handle_1.clone();
 854                Tooltip::for_action_in(
 855                    "Commit all staged changes",
 856                    &CommitStagedChanges,
 857                    &focus_handle,
 858                    cx,
 859                )
 860            })
 861            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 862                this.commit_staged_changes(&CommitStagedChanges, cx)
 863            }));
 864
 865        let commit_all_button = self
 866            .panel_button("commit-all-changes", "Commit All")
 867            .tooltip(move |cx| {
 868                let focus_handle = focus_handle_2.clone();
 869                Tooltip::for_action_in(
 870                    "Commit all changes, including unstaged changes",
 871                    &CommitAllChanges,
 872                    &focus_handle,
 873                    cx,
 874                )
 875            })
 876            .on_click(cx.listener(|this, _: &ClickEvent, cx| {
 877                this.commit_all_changes(&CommitAllChanges, cx)
 878            }));
 879
 880        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
 881            v_flex()
 882                .h_full()
 883                .py_2p5()
 884                .px_3()
 885                .bg(cx.theme().colors().editor_background)
 886                .font_buffer(cx)
 887                .text_ui_sm(cx)
 888                .text_color(cx.theme().colors().text_muted)
 889                .child("Add a message")
 890                .gap_1()
 891                .child(div().flex_grow())
 892                .child(h_flex().child(div().gap_1().flex_grow()).child(
 893                    if self.current_modifiers.alt {
 894                        commit_all_button
 895                    } else {
 896                        commit_staged_button
 897                    },
 898                ))
 899                .cursor(CursorStyle::OperationNotAllowed)
 900                .opacity(0.5),
 901        )
 902    }
 903
 904    fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
 905        h_flex()
 906            .h_full()
 907            .flex_1()
 908            .justify_center()
 909            .items_center()
 910            .child(
 911                v_flex()
 912                    .gap_3()
 913                    .child("No changes to commit")
 914                    .text_ui_sm(cx)
 915                    .mx_auto()
 916                    .text_color(Color::Placeholder.color(cx)),
 917            )
 918    }
 919
 920    fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
 921        if !Self::should_show_scrollbar(cx)
 922            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
 923        {
 924            return None;
 925        }
 926        Some(
 927            div()
 928                .occlude()
 929                .id("project-panel-vertical-scroll")
 930                .on_mouse_move(cx.listener(|_, _, cx| {
 931                    cx.notify();
 932                    cx.stop_propagation()
 933                }))
 934                .on_hover(|_, cx| {
 935                    cx.stop_propagation();
 936                })
 937                .on_any_mouse_down(|_, cx| {
 938                    cx.stop_propagation();
 939                })
 940                .on_mouse_up(
 941                    MouseButton::Left,
 942                    cx.listener(|this, _, cx| {
 943                        if !this.scrollbar_state.is_dragging()
 944                            && !this.focus_handle.contains_focused(cx)
 945                        {
 946                            this.hide_scrollbar(cx);
 947                            cx.notify();
 948                        }
 949
 950                        cx.stop_propagation();
 951                    }),
 952                )
 953                .on_scroll_wheel(cx.listener(|_, _, cx| {
 954                    cx.notify();
 955                }))
 956                .h_full()
 957                .absolute()
 958                .right_1()
 959                .top_1()
 960                .bottom_1()
 961                .w(px(12.))
 962                .cursor_default()
 963                .children(Scrollbar::vertical(
 964                    // percentage as f32..end_offset as f32,
 965                    self.scrollbar_state.clone(),
 966                )),
 967        )
 968    }
 969
 970    fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 971        let item_count = self
 972            .visible_entries
 973            .iter()
 974            .map(|worktree_entries| worktree_entries.visible_entries.len())
 975            .sum();
 976        let selected_entry = self.selected_item;
 977        h_flex()
 978            .size_full()
 979            .overflow_hidden()
 980            .child(
 981                uniform_list(cx.view().clone(), "entries", item_count, {
 982                    move |git_panel, range, cx| {
 983                        let mut items = Vec::with_capacity(range.end - range.start);
 984                        git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
 985                            items.push(git_panel.render_entry(
 986                                id,
 987                                Some(details.index) == selected_entry,
 988                                details,
 989                                cx,
 990                            ));
 991                        });
 992                        items
 993                    }
 994                })
 995                .size_full()
 996                .with_sizing_behavior(ListSizingBehavior::Infer)
 997                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
 998                // .with_width_from_item(self.max_width_item_index)
 999                .track_scroll(self.scroll_handle.clone()),
1000            )
1001            .children(self.render_scrollbar(cx))
1002    }
1003
1004    fn render_entry(
1005        &self,
1006        ix: usize,
1007        selected: bool,
1008        details: EntryDetails,
1009        cx: &ViewContext<Self>,
1010    ) -> impl IntoElement {
1011        let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
1012        let is_staged = ToggleState::Selected;
1013        let handle = cx.view().downgrade();
1014
1015        h_flex()
1016            .id(("git-panel-entry", ix))
1017            .h(px(28.))
1018            .w_full()
1019            .pl(px(12. + 12. * details.depth as f32))
1020            .pr(px(4.))
1021            .items_center()
1022            .gap_2()
1023            .font_buffer(cx)
1024            .text_ui_sm(cx)
1025            .when(!details.is_dir(), |this| {
1026                this.child(Checkbox::new(checkbox_id, is_staged))
1027            })
1028            .when_some(details.status, |this, status| {
1029                this.child(git_status_icon(status))
1030            })
1031            .child(
1032                ListItem::new(details.path.0.clone())
1033                    .toggle_state(selected)
1034                    .child(h_flex().gap_1p5().child(details.display_name.clone()))
1035                    .on_click(move |e, cx| {
1036                        handle
1037                            .update(cx, |git_panel, cx| {
1038                                git_panel.selected_item = Some(details.index);
1039                                let change_focus = e.down.click_count > 1;
1040                                git_panel.reveal_entry_in_git_editor(
1041                                    details.hunks.clone(),
1042                                    change_focus,
1043                                    None,
1044                                    cx,
1045                                );
1046                            })
1047                            .ok();
1048                    }),
1049            )
1050    }
1051
1052    fn reveal_entry_in_git_editor(
1053        &mut self,
1054        _hunks: Rc<OnceCell<Vec<DiffHunk>>>,
1055        _change_focus: bool,
1056        _debounce: Option<Duration>,
1057        _cx: &mut ViewContext<Self>,
1058    ) {
1059        // let workspace = self.workspace.clone();
1060        // let Some(diff_editor) = self.git_diff_editor.clone() else {
1061        //     return;
1062        // };
1063        // self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
1064        //     if let Some(debounce) = debounce {
1065        //         cx.background_executor().timer(debounce).await;
1066        //     }
1067
1068        //     let Some(editor) = workspace
1069        //         .update(&mut cx, |workspace, cx| {
1070        //             let git_diff_editor = workspace
1071        //                 .items_of_type::<Editor>(cx)
1072        //                 .find(|editor| &diff_editor == editor);
1073        //             match git_diff_editor {
1074        //                 Some(existing_editor) => {
1075        //                     workspace.activate_item(&existing_editor, true, change_focus, cx);
1076        //                     existing_editor
1077        //                 }
1078        //                 None => {
1079        //                     workspace.active_pane().update(cx, |pane, cx| {
1080        //                         pane.add_item(
1081        //                          `   diff_editor.boxed_clone(),
1082        //                             true,
1083        //                             change_focus,
1084        //                             None,
1085        //                             cx,
1086        //                         )
1087        //                     });
1088        //                     diff_editor.clone()
1089        //                 }
1090        //             }
1091        //         })
1092        //         .ok()
1093        //     else {
1094        //         return;
1095        //     };
1096
1097        //     if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
1098        //         let hunk_buffer_range = &first_hunk.buffer_range;
1099        //         if let Some(buffer_id) = hunk_buffer_range
1100        //             .start
1101        //             .buffer_id
1102        //             .or_else(|| first_hunk.buffer_range.end.buffer_id)
1103        //         {
1104        //             editor
1105        //                 .update(&mut cx, |editor, cx| {
1106        //                     let multi_buffer = editor.buffer().read(cx);
1107        //                     let buffer = multi_buffer.buffer(buffer_id)?;
1108        //                     let buffer_snapshot = buffer.read(cx).snapshot();
1109        //                     let (excerpt_id, _) = multi_buffer
1110        //                         .excerpts_for_buffer(&buffer, cx)
1111        //                         .into_iter()
1112        //                         .find(|(_, excerpt)| {
1113        //                             hunk_buffer_range
1114        //                                 .start
1115        //                                 .cmp(&excerpt.context.start, &buffer_snapshot)
1116        //                                 .is_ge()
1117        //                                 && hunk_buffer_range
1118        //                                     .end
1119        //                                     .cmp(&excerpt.context.end, &buffer_snapshot)
1120        //                                     .is_le()
1121        //                         })?;
1122        //                     let multi_buffer_hunk_start = multi_buffer
1123        //                         .snapshot(cx)
1124        //                         .anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
1125        //                     editor.change_selections(
1126        //                         Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
1127        //                         cx,
1128        //                         |s| {
1129        //                             s.select_ranges(Some(
1130        //                                 multi_buffer_hunk_start..multi_buffer_hunk_start,
1131        //                             ))
1132        //                         },
1133        //                     );
1134        //                     cx.notify();
1135        //                     Some(())
1136        //                 })
1137        //                 .ok()
1138        //                 .flatten();
1139        //         }
1140        //     }
1141        // });
1142    }
1143}
1144
1145impl Render for GitPanel {
1146    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1147        let project = self.project.read(cx);
1148
1149        v_flex()
1150            .id("git_panel")
1151            .key_context(self.dispatch_context())
1152            .track_focus(&self.focus_handle)
1153            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
1154            .when(!project.is_read_only(cx), |this| {
1155                this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
1156                    .on_action(
1157                        cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
1158                    )
1159                    .on_action(
1160                        cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
1161                    )
1162                    .on_action(cx.listener(|this, &CommitStagedChanges, cx| {
1163                        this.commit_staged_changes(&CommitStagedChanges, cx)
1164                    }))
1165                    .on_action(cx.listener(|this, &CommitAllChanges, cx| {
1166                        this.commit_all_changes(&CommitAllChanges, cx)
1167                    }))
1168            })
1169            .on_action(cx.listener(Self::select_next))
1170            .on_action(cx.listener(Self::select_prev))
1171            .on_hover(cx.listener(|this, hovered, cx| {
1172                if *hovered {
1173                    this.show_scrollbar = true;
1174                    this.hide_scrollbar_task.take();
1175                    cx.notify();
1176                } else if !this.focus_handle.contains_focused(cx) {
1177                    this.hide_scrollbar(cx);
1178                }
1179            }))
1180            .size_full()
1181            .overflow_hidden()
1182            .font_buffer(cx)
1183            .py_1()
1184            .bg(ElevationIndex::Surface.bg(cx))
1185            .child(self.render_panel_header(cx))
1186            .child(self.render_divider(cx))
1187            .child(if !self.no_entries() {
1188                self.render_entries(cx).into_any_element()
1189            } else {
1190                self.render_empty_state(cx).into_any_element()
1191            })
1192            .child(self.render_divider(cx))
1193            .child(self.render_commit_editor(cx))
1194    }
1195}
1196
1197impl FocusableView for GitPanel {
1198    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
1199        self.focus_handle.clone()
1200    }
1201}
1202
1203impl EventEmitter<Event> for GitPanel {}
1204
1205impl EventEmitter<PanelEvent> for GitPanel {}
1206
1207impl Panel for GitPanel {
1208    fn persistent_name() -> &'static str {
1209        "GitPanel"
1210    }
1211
1212    fn position(&self, cx: &WindowContext) -> DockPosition {
1213        GitPanelSettings::get_global(cx).dock
1214    }
1215
1216    fn position_is_valid(&self, position: DockPosition) -> bool {
1217        matches!(position, DockPosition::Left | DockPosition::Right)
1218    }
1219
1220    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1221        settings::update_settings_file::<GitPanelSettings>(
1222            self.fs.clone(),
1223            cx,
1224            move |settings, _| settings.dock = Some(position),
1225        );
1226    }
1227
1228    fn size(&self, cx: &WindowContext) -> Pixels {
1229        self.width
1230            .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
1231    }
1232
1233    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1234        self.width = size;
1235        self.serialize(cx);
1236        cx.notify();
1237    }
1238
1239    fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
1240        Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
1241    }
1242
1243    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1244        Some("Git Panel")
1245    }
1246
1247    fn toggle_action(&self) -> Box<dyn Action> {
1248        Box::new(ToggleFocus)
1249    }
1250
1251    fn activation_priority(&self) -> u32 {
1252        2
1253    }
1254}
1255
1256// fn diff_display_editor(cx: &mut WindowContext) -> View<Editor> {
1257//     cx.new_view(|cx| {
1258//         let multi_buffer = cx.new_model(|_| {
1259//             MultiBuffer::new(language::Capability::ReadWrite).with_title("Project diff".to_string())
1260//         });
1261//         let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
1262//         editor.set_expand_all_diff_hunks();
1263//         editor
1264//     })
1265// }