project_panel.rs

   1pub mod file_associations;
   2mod project_panel_settings;
   3use settings::Settings;
   4
   5use db::kvp::KEY_VALUE_STORE;
   6use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
   7use file_associations::FileAssociations;
   8
   9use anyhow::{anyhow, Result};
  10use gpui::{
  11    actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
  12    ClipboardItem, Component, Div, Entity, EventEmitter, FocusHandle, FocusableKeyDispatch, Model,
  13    ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive,
  14    StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View,
  15    ViewContext, VisualContext as _, WeakView, WindowContext,
  16};
  17use menu::{Confirm, SelectNext, SelectPrev};
  18use project::{
  19    repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
  20    Worktree, WorktreeId,
  21};
  22use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
  23use serde::{Deserialize, Serialize};
  24use smallvec::SmallVec;
  25use std::{
  26    cmp::Ordering,
  27    collections::{hash_map, HashMap},
  28    ffi::OsStr,
  29    ops::Range,
  30    path::Path,
  31    sync::Arc,
  32};
  33use theme::ActiveTheme as _;
  34use ui::{h_stack, v_stack, Label};
  35use unicase::UniCase;
  36use util::{maybe, TryFutureExt};
  37use workspace::{
  38    dock::{DockPosition, PanelEvent},
  39    Workspace,
  40};
  41
  42const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
  43const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  44
  45pub struct ProjectPanel {
  46    project: Model<Project>,
  47    fs: Arc<dyn Fs>,
  48    list: UniformListScrollHandle,
  49    focus_handle: FocusHandle,
  50    visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
  51    last_worktree_root_id: Option<ProjectEntryId>,
  52    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
  53    selection: Option<Selection>,
  54    edit_state: Option<EditState>,
  55    filename_editor: View<Editor>,
  56    clipboard_entry: Option<ClipboardEntry>,
  57    dragged_entry_destination: Option<Arc<Path>>,
  58    workspace: WeakView<Workspace>,
  59    has_focus: bool,
  60    width: Option<f32>,
  61    pending_serialization: Task<Option<()>>,
  62}
  63
  64#[derive(Copy, Clone, Debug)]
  65struct Selection {
  66    worktree_id: WorktreeId,
  67    entry_id: ProjectEntryId,
  68}
  69
  70#[derive(Clone, Debug)]
  71struct EditState {
  72    worktree_id: WorktreeId,
  73    entry_id: ProjectEntryId,
  74    is_new_entry: bool,
  75    is_dir: bool,
  76    processing_filename: Option<String>,
  77}
  78
  79#[derive(Copy, Clone)]
  80pub enum ClipboardEntry {
  81    Copied {
  82        worktree_id: WorktreeId,
  83        entry_id: ProjectEntryId,
  84    },
  85    Cut {
  86        worktree_id: WorktreeId,
  87        entry_id: ProjectEntryId,
  88    },
  89}
  90
  91#[derive(Debug, PartialEq, Eq)]
  92pub struct EntryDetails {
  93    filename: String,
  94    icon: Option<Arc<str>>,
  95    path: Arc<Path>,
  96    depth: usize,
  97    kind: EntryKind,
  98    is_ignored: bool,
  99    is_expanded: bool,
 100    is_selected: bool,
 101    is_editing: bool,
 102    is_processing: bool,
 103    is_cut: bool,
 104    git_status: Option<GitFileStatus>,
 105}
 106
 107actions!(
 108    ExpandSelectedEntry,
 109    CollapseSelectedEntry,
 110    CollapseAllEntries,
 111    NewDirectory,
 112    NewFile,
 113    Copy,
 114    CopyPath,
 115    CopyRelativePath,
 116    RevealInFinder,
 117    OpenInTerminal,
 118    Cut,
 119    Paste,
 120    Delete,
 121    Rename,
 122    Open,
 123    ToggleFocus,
 124    NewSearchInDirectory,
 125);
 126
 127pub fn init_settings(cx: &mut AppContext) {
 128    ProjectPanelSettings::register(cx);
 129}
 130
 131pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
 132    init_settings(cx);
 133    file_associations::init(assets, cx);
 134}
 135
 136#[derive(Debug)]
 137pub enum Event {
 138    OpenedEntry {
 139        entry_id: ProjectEntryId,
 140        focus_opened_item: bool,
 141    },
 142    SplitEntry {
 143        entry_id: ProjectEntryId,
 144    },
 145    DockPositionChanged,
 146    Focus,
 147    NewSearchInDirectory {
 148        dir_entry: Entry,
 149    },
 150    ActivatePanel,
 151}
 152
 153#[derive(Serialize, Deserialize)]
 154struct SerializedProjectPanel {
 155    width: Option<f32>,
 156}
 157
 158impl ProjectPanel {
 159    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 160        let project = workspace.project().clone();
 161        let project_panel = cx.build_view(|cx: &mut ViewContext<Self>| {
 162            cx.observe(&project, |this, _, cx| {
 163                this.update_visible_entries(None, cx);
 164                cx.notify();
 165            })
 166            .detach();
 167            let focus_handle = cx.focus_handle();
 168
 169            cx.on_focus(&focus_handle, Self::focus_in).detach();
 170            cx.on_blur(&focus_handle, Self::focus_out).detach();
 171
 172            cx.subscribe(&project, |this, project, event, cx| match event {
 173                project::Event::ActiveEntryChanged(Some(entry_id)) => {
 174                    if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
 175                    {
 176                        this.expand_entry(worktree_id, *entry_id, cx);
 177                        this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
 178                        this.autoscroll(cx);
 179                        cx.notify();
 180                    }
 181                }
 182                project::Event::ActivateProjectPanel => {
 183                    cx.emit(Event::ActivatePanel);
 184                }
 185                project::Event::WorktreeRemoved(id) => {
 186                    this.expanded_dir_ids.remove(id);
 187                    this.update_visible_entries(None, cx);
 188                    cx.notify();
 189                }
 190                _ => {}
 191            })
 192            .detach();
 193
 194            let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
 195
 196            cx.subscribe(&filename_editor, |this, _, event, cx| match event {
 197                editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
 198                    this.autoscroll(cx);
 199                }
 200                _ => {}
 201            })
 202            .detach();
 203
 204            // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
 205            //     if !is_focused
 206            //         && this
 207            //             .edit_state
 208            //             .as_ref()
 209            //             .map_or(false, |state| state.processing_filename.is_none())
 210            //     {
 211            //         this.edit_state = None;
 212            //         this.update_visible_entries(None, cx);
 213            //     }
 214            // })
 215            // .detach();
 216
 217            // cx.observe_global::<FileAssociations, _>(|_, cx| {
 218            //     cx.notify();
 219            // })
 220            // .detach();
 221
 222            let view_id = cx.view().entity_id();
 223            let mut this = Self {
 224                project: project.clone(),
 225                fs: workspace.app_state().fs.clone(),
 226                list: UniformListScrollHandle::new(),
 227                focus_handle,
 228                visible_entries: Default::default(),
 229                last_worktree_root_id: Default::default(),
 230                expanded_dir_ids: Default::default(),
 231                selection: None,
 232                edit_state: None,
 233                filename_editor,
 234                clipboard_entry: None,
 235                // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 236                dragged_entry_destination: None,
 237                workspace: workspace.weak_handle(),
 238                has_focus: false,
 239                width: None,
 240                pending_serialization: Task::ready(None),
 241            };
 242            this.update_visible_entries(None, cx);
 243
 244            // Update the dock position when the setting changes.
 245            // todo!()
 246            // let mut old_dock_position = this.position(cx);
 247            // cx.observe_global::<SettingsStore, _>(move |this, cx| {
 248            //     let new_dock_position = this.position(cx);
 249            //     if new_dock_position != old_dock_position {
 250            //         old_dock_position = new_dock_position;
 251            //         cx.emit(Event::DockPositionChanged);
 252            //     }
 253            // })
 254            // .detach();
 255
 256            this
 257        });
 258
 259        cx.subscribe(&project_panel, {
 260            let project_panel = project_panel.downgrade();
 261            move |workspace, _, event, cx| match event {
 262                &Event::OpenedEntry {
 263                    entry_id,
 264                    focus_opened_item,
 265                } => {
 266                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 267                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 268                            workspace
 269                                .open_path(
 270                                    ProjectPath {
 271                                        worktree_id: worktree.read(cx).id(),
 272                                        path: entry.path.clone(),
 273                                    },
 274                                    None,
 275                                    focus_opened_item,
 276                                    cx,
 277                                )
 278                                .detach_and_log_err(cx);
 279                            if !focus_opened_item {
 280                                if let Some(project_panel) = project_panel.upgrade() {
 281                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 282                                    cx.focus(&focus_handle);
 283                                }
 284                            }
 285                        }
 286                    }
 287                }
 288                &Event::SplitEntry { entry_id } => {
 289                    // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 290                    //     if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 291                    //         workspace
 292                    //             .split_path(
 293                    //                 ProjectPath {
 294                    //                     worktree_id: worktree.read(cx).id(),
 295                    //                     path: entry.path.clone(),
 296                    //                 },
 297                    //                 cx,
 298                    //             )
 299                    //             .detach_and_log_err(cx);
 300                    //     }
 301                    // }
 302                }
 303                _ => {}
 304            }
 305        })
 306        .detach();
 307
 308        project_panel
 309    }
 310
 311    pub fn load(
 312        workspace: WeakView<Workspace>,
 313        cx: AsyncWindowContext,
 314    ) -> Task<Result<View<Self>>> {
 315        cx.spawn(|mut cx| async move {
 316            // let serialized_panel = if let Some(panel) = cx
 317            //     .background_executor()
 318            //     .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
 319            //     .await
 320            //     .log_err()
 321            //     .flatten()
 322            // {
 323            //     Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
 324            // } else {
 325            //     None
 326            // };
 327            workspace.update(&mut cx, |workspace, cx| {
 328                let panel = ProjectPanel::new(workspace, cx);
 329                // if let Some(serialized_panel) = serialized_panel {
 330                //     panel.update(cx, |panel, cx| {
 331                //         panel.width = serialized_panel.width;
 332                //         cx.notify();
 333                //     });
 334                // }
 335                panel
 336            })
 337        })
 338    }
 339
 340    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 341        let width = self.width;
 342        self.pending_serialization = cx.background_executor().spawn(
 343            async move {
 344                KEY_VALUE_STORE
 345                    .write_kvp(
 346                        PROJECT_PANEL_KEY.into(),
 347                        serde_json::to_string(&SerializedProjectPanel { width })?,
 348                    )
 349                    .await?;
 350                anyhow::Ok(())
 351            }
 352            .log_err(),
 353        );
 354    }
 355
 356    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 357        if !self.has_focus {
 358            self.has_focus = true;
 359            cx.emit(Event::Focus);
 360        }
 361    }
 362
 363    fn focus_out(&mut self, _: &mut ViewContext<Self>) {
 364        self.has_focus = false;
 365    }
 366
 367    fn deploy_context_menu(
 368        &mut self,
 369        position: Point<Pixels>,
 370        entry_id: ProjectEntryId,
 371        cx: &mut ViewContext<Self>,
 372    ) {
 373        // let project = self.project.read(cx);
 374
 375        // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
 376        //     id
 377        // } else {
 378        //     return;
 379        // };
 380
 381        // self.selection = Some(Selection {
 382        //     worktree_id,
 383        //     entry_id,
 384        // });
 385
 386        // let mut menu_entries = Vec::new();
 387        // if let Some((worktree, entry)) = self.selected_entry(cx) {
 388        //     let is_root = Some(entry) == worktree.root_entry();
 389        //     if !project.is_remote() {
 390        //         menu_entries.push(ContextMenuItem::action(
 391        //             "Add Folder to Project",
 392        //             workspace::AddFolderToProject,
 393        //         ));
 394        //         if is_root {
 395        //             let project = self.project.clone();
 396        //             menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
 397        //                 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
 398        //             }));
 399        //         }
 400        //     }
 401        //     menu_entries.push(ContextMenuItem::action("New File", NewFile));
 402        //     menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
 403        //     menu_entries.push(ContextMenuItem::Separator);
 404        //     menu_entries.push(ContextMenuItem::action("Cut", Cut));
 405        //     menu_entries.push(ContextMenuItem::action("Copy", Copy));
 406        //     if let Some(clipboard_entry) = self.clipboard_entry {
 407        //         if clipboard_entry.worktree_id() == worktree.id() {
 408        //             menu_entries.push(ContextMenuItem::action("Paste", Paste));
 409        //         }
 410        //     }
 411        //     menu_entries.push(ContextMenuItem::Separator);
 412        //     menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
 413        //     menu_entries.push(ContextMenuItem::action(
 414        //         "Copy Relative Path",
 415        //         CopyRelativePath,
 416        //     ));
 417
 418        //     if entry.is_dir() {
 419        //         menu_entries.push(ContextMenuItem::Separator);
 420        //     }
 421        //     menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
 422        //     if entry.is_dir() {
 423        //         menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
 424        //         menu_entries.push(ContextMenuItem::action(
 425        //             "Search Inside",
 426        //             NewSearchInDirectory,
 427        //         ));
 428        //     }
 429
 430        //     menu_entries.push(ContextMenuItem::Separator);
 431        //     menu_entries.push(ContextMenuItem::action("Rename", Rename));
 432        //     if !is_root {
 433        //         menu_entries.push(ContextMenuItem::action("Delete", Delete));
 434        //     }
 435        // }
 436
 437        // // self.context_menu.update(cx, |menu, cx| {
 438        // //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
 439        // // });
 440
 441        // cx.notify();
 442    }
 443
 444    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
 445        if let Some((worktree, entry)) = self.selected_entry(cx) {
 446            if entry.is_dir() {
 447                let worktree_id = worktree.id();
 448                let entry_id = entry.id;
 449                let expanded_dir_ids =
 450                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 451                        expanded_dir_ids
 452                    } else {
 453                        return;
 454                    };
 455
 456                match expanded_dir_ids.binary_search(&entry_id) {
 457                    Ok(_) => self.select_next(&SelectNext, cx),
 458                    Err(ix) => {
 459                        self.project.update(cx, |project, cx| {
 460                            project.expand_entry(worktree_id, entry_id, cx);
 461                        });
 462
 463                        expanded_dir_ids.insert(ix, entry_id);
 464                        self.update_visible_entries(None, cx);
 465                        cx.notify();
 466                    }
 467                }
 468            }
 469        }
 470    }
 471
 472    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
 473        if let Some((worktree, mut entry)) = self.selected_entry(cx) {
 474            let worktree_id = worktree.id();
 475            let expanded_dir_ids =
 476                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 477                    expanded_dir_ids
 478                } else {
 479                    return;
 480                };
 481
 482            loop {
 483                let entry_id = entry.id;
 484                match expanded_dir_ids.binary_search(&entry_id) {
 485                    Ok(ix) => {
 486                        expanded_dir_ids.remove(ix);
 487                        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 488                        cx.notify();
 489                        break;
 490                    }
 491                    Err(_) => {
 492                        if let Some(parent_entry) =
 493                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
 494                        {
 495                            entry = parent_entry;
 496                        } else {
 497                            break;
 498                        }
 499                    }
 500                }
 501            }
 502        }
 503    }
 504
 505    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
 506        self.expanded_dir_ids.clear();
 507        self.update_visible_entries(None, cx);
 508        cx.notify();
 509    }
 510
 511    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
 512        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
 513            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 514                self.project.update(cx, |project, cx| {
 515                    match expanded_dir_ids.binary_search(&entry_id) {
 516                        Ok(ix) => {
 517                            expanded_dir_ids.remove(ix);
 518                        }
 519                        Err(ix) => {
 520                            project.expand_entry(worktree_id, entry_id, cx);
 521                            expanded_dir_ids.insert(ix, entry_id);
 522                        }
 523                    }
 524                });
 525                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 526                cx.focus(&self.focus_handle);
 527                cx.notify();
 528            }
 529        }
 530    }
 531
 532    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 533        if let Some(selection) = self.selection {
 534            let (mut worktree_ix, mut entry_ix, _) =
 535                self.index_for_selection(selection).unwrap_or_default();
 536            if entry_ix > 0 {
 537                entry_ix -= 1;
 538            } else if worktree_ix > 0 {
 539                worktree_ix -= 1;
 540                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
 541            } else {
 542                return;
 543            }
 544
 545            let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix];
 546            self.selection = Some(Selection {
 547                worktree_id: *worktree_id,
 548                entry_id: worktree_entries[entry_ix].id,
 549            });
 550            self.autoscroll(cx);
 551            cx.notify();
 552        } else {
 553            self.select_first(cx);
 554        }
 555    }
 556
 557    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
 558        if let Some(task) = self.confirm_edit(cx) {
 559            task.detach_and_log_err(cx);
 560        }
 561    }
 562
 563    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
 564        if let Some((_, entry)) = self.selected_entry(cx) {
 565            if entry.is_file() {
 566                self.open_entry(entry.id, true, cx);
 567            }
 568        }
 569    }
 570
 571    fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
 572        let edit_state = self.edit_state.as_mut()?;
 573        cx.focus(&self.focus_handle);
 574
 575        let worktree_id = edit_state.worktree_id;
 576        let is_new_entry = edit_state.is_new_entry;
 577        let is_dir = edit_state.is_dir;
 578        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
 579        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
 580        let filename = self.filename_editor.read(cx).text(cx);
 581
 582        let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
 583        let edit_task;
 584        let edited_entry_id;
 585        if is_new_entry {
 586            self.selection = Some(Selection {
 587                worktree_id,
 588                entry_id: NEW_ENTRY_ID,
 589            });
 590            let new_path = entry.path.join(&filename.trim_start_matches("/"));
 591            if path_already_exists(new_path.as_path()) {
 592                return None;
 593            }
 594
 595            edited_entry_id = NEW_ENTRY_ID;
 596            edit_task = self.project.update(cx, |project, cx| {
 597                project.create_entry((worktree_id, &new_path), is_dir, cx)
 598            })?;
 599        } else {
 600            let new_path = if let Some(parent) = entry.path.clone().parent() {
 601                parent.join(&filename)
 602            } else {
 603                filename.clone().into()
 604            };
 605            if path_already_exists(new_path.as_path()) {
 606                return None;
 607            }
 608
 609            edited_entry_id = entry.id;
 610            edit_task = self.project.update(cx, |project, cx| {
 611                project.rename_entry(entry.id, new_path.as_path(), cx)
 612            })?;
 613        };
 614
 615        edit_state.processing_filename = Some(filename);
 616        cx.notify();
 617
 618        Some(cx.spawn(|this, mut cx| async move {
 619            let new_entry = edit_task.await;
 620            this.update(&mut cx, |this, cx| {
 621                this.edit_state.take();
 622                cx.notify();
 623            })?;
 624
 625            let new_entry = new_entry?;
 626            this.update(&mut cx, |this, cx| {
 627                if let Some(selection) = &mut this.selection {
 628                    if selection.entry_id == edited_entry_id {
 629                        selection.worktree_id = worktree_id;
 630                        selection.entry_id = new_entry.id;
 631                        this.expand_to_selection(cx);
 632                    }
 633                }
 634                this.update_visible_entries(None, cx);
 635                if is_new_entry && !is_dir {
 636                    this.open_entry(new_entry.id, true, cx);
 637                }
 638                cx.notify();
 639            })?;
 640            Ok(())
 641        }))
 642    }
 643
 644    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
 645        self.edit_state = None;
 646        self.update_visible_entries(None, cx);
 647        cx.focus(&self.focus_handle);
 648        cx.notify();
 649    }
 650
 651    fn open_entry(
 652        &mut self,
 653        entry_id: ProjectEntryId,
 654        focus_opened_item: bool,
 655        cx: &mut ViewContext<Self>,
 656    ) {
 657        cx.emit(Event::OpenedEntry {
 658            entry_id,
 659            focus_opened_item,
 660        });
 661    }
 662
 663    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
 664        cx.emit(Event::SplitEntry { entry_id });
 665    }
 666
 667    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
 668        self.add_entry(false, cx)
 669    }
 670
 671    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
 672        self.add_entry(true, cx)
 673    }
 674
 675    fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
 676        if let Some(Selection {
 677            worktree_id,
 678            entry_id,
 679        }) = self.selection
 680        {
 681            let directory_id;
 682            if let Some((worktree, expanded_dir_ids)) = self
 683                .project
 684                .read(cx)
 685                .worktree_for_id(worktree_id, cx)
 686                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
 687            {
 688                let worktree = worktree.read(cx);
 689                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
 690                    loop {
 691                        if entry.is_dir() {
 692                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
 693                                expanded_dir_ids.insert(ix, entry.id);
 694                            }
 695                            directory_id = entry.id;
 696                            break;
 697                        } else {
 698                            if let Some(parent_path) = entry.path.parent() {
 699                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
 700                                    entry = parent_entry;
 701                                    continue;
 702                                }
 703                            }
 704                            return;
 705                        }
 706                    }
 707                } else {
 708                    return;
 709                };
 710            } else {
 711                return;
 712            };
 713
 714            self.edit_state = Some(EditState {
 715                worktree_id,
 716                entry_id: directory_id,
 717                is_new_entry: true,
 718                is_dir,
 719                processing_filename: None,
 720            });
 721            self.filename_editor.update(cx, |editor, cx| {
 722                editor.clear(cx);
 723                editor.focus(cx);
 724            });
 725            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
 726            self.autoscroll(cx);
 727            cx.notify();
 728        }
 729    }
 730
 731    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
 732        if let Some(Selection {
 733            worktree_id,
 734            entry_id,
 735        }) = self.selection
 736        {
 737            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
 738                if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 739                    self.edit_state = Some(EditState {
 740                        worktree_id,
 741                        entry_id,
 742                        is_new_entry: false,
 743                        is_dir: entry.is_dir(),
 744                        processing_filename: None,
 745                    });
 746                    let file_name = entry
 747                        .path
 748                        .file_name()
 749                        .map(|s| s.to_string_lossy())
 750                        .unwrap_or_default()
 751                        .to_string();
 752                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
 753                    let selection_end =
 754                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
 755                    self.filename_editor.update(cx, |editor, cx| {
 756                        editor.set_text(file_name, cx);
 757                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 758                            s.select_ranges([0..selection_end])
 759                        });
 760                        editor.focus(cx);
 761                    });
 762                    self.update_visible_entries(None, cx);
 763                    self.autoscroll(cx);
 764                    cx.notify();
 765                }
 766            }
 767
 768            // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
 769            //     drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
 770            // })
 771        }
 772    }
 773
 774    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
 775        maybe!({
 776            let Selection { entry_id, .. } = self.selection?;
 777            let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
 778            let file_name = path.file_name()?;
 779
 780            let answer = cx.prompt(
 781                PromptLevel::Info,
 782                &format!("Delete {file_name:?}?"),
 783                &["Delete", "Cancel"],
 784            );
 785
 786            cx.spawn(|this, mut cx| async move {
 787                if answer.await != Ok(0) {
 788                    return Ok(());
 789                }
 790                this.update(&mut cx, |this, cx| {
 791                    this.project
 792                        .update(cx, |project, cx| project.delete_entry(entry_id, cx))
 793                        .ok_or_else(|| anyhow!("no such entry"))
 794                })??
 795                .await
 796            })
 797            .detach_and_log_err(cx);
 798            Some(())
 799        });
 800    }
 801
 802    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 803        if let Some(selection) = self.selection {
 804            let (mut worktree_ix, mut entry_ix, _) =
 805                self.index_for_selection(selection).unwrap_or_default();
 806            if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) {
 807                if entry_ix + 1 < worktree_entries.len() {
 808                    entry_ix += 1;
 809                } else {
 810                    worktree_ix += 1;
 811                    entry_ix = 0;
 812                }
 813            }
 814
 815            if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) {
 816                if let Some(entry) = worktree_entries.get(entry_ix) {
 817                    self.selection = Some(Selection {
 818                        worktree_id: *worktree_id,
 819                        entry_id: entry.id,
 820                    });
 821                    self.autoscroll(cx);
 822                    cx.notify();
 823                }
 824            }
 825        } else {
 826            self.select_first(cx);
 827        }
 828    }
 829
 830    fn select_first(&mut self, cx: &mut ViewContext<Self>) {
 831        let worktree = self
 832            .visible_entries
 833            .first()
 834            .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
 835        if let Some(worktree) = worktree {
 836            let worktree = worktree.read(cx);
 837            let worktree_id = worktree.id();
 838            if let Some(root_entry) = worktree.root_entry() {
 839                self.selection = Some(Selection {
 840                    worktree_id,
 841                    entry_id: root_entry.id,
 842                });
 843                self.autoscroll(cx);
 844                cx.notify();
 845            }
 846        }
 847    }
 848
 849    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
 850        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
 851            self.list.scroll_to_item(index);
 852            cx.notify();
 853        }
 854    }
 855
 856    fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
 857        if let Some((worktree, entry)) = self.selected_entry(cx) {
 858            self.clipboard_entry = Some(ClipboardEntry::Cut {
 859                worktree_id: worktree.id(),
 860                entry_id: entry.id,
 861            });
 862            cx.notify();
 863        }
 864    }
 865
 866    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
 867        if let Some((worktree, entry)) = self.selected_entry(cx) {
 868            self.clipboard_entry = Some(ClipboardEntry::Copied {
 869                worktree_id: worktree.id(),
 870                entry_id: entry.id,
 871            });
 872            cx.notify();
 873        }
 874    }
 875
 876    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
 877        maybe!({
 878            let (worktree, entry) = self.selected_entry(cx)?;
 879            let clipboard_entry = self.clipboard_entry?;
 880            if clipboard_entry.worktree_id() != worktree.id() {
 881                return None;
 882            }
 883
 884            let clipboard_entry_file_name = self
 885                .project
 886                .read(cx)
 887                .path_for_entry(clipboard_entry.entry_id(), cx)?
 888                .path
 889                .file_name()?
 890                .to_os_string();
 891
 892            let mut new_path = entry.path.to_path_buf();
 893            if entry.is_file() {
 894                new_path.pop();
 895            }
 896
 897            new_path.push(&clipboard_entry_file_name);
 898            let extension = new_path.extension().map(|e| e.to_os_string());
 899            let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
 900            let mut ix = 0;
 901            while worktree.entry_for_path(&new_path).is_some() {
 902                new_path.pop();
 903
 904                let mut new_file_name = file_name_without_extension.to_os_string();
 905                new_file_name.push(" copy");
 906                if ix > 0 {
 907                    new_file_name.push(format!(" {}", ix));
 908                }
 909                if let Some(extension) = extension.as_ref() {
 910                    new_file_name.push(".");
 911                    new_file_name.push(extension);
 912                }
 913
 914                new_path.push(new_file_name);
 915                ix += 1;
 916            }
 917
 918            if clipboard_entry.is_cut() {
 919                if let Some(task) = self.project.update(cx, |project, cx| {
 920                    project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
 921                }) {
 922                    task.detach_and_log_err(cx);
 923                }
 924            } else if let Some(task) = self.project.update(cx, |project, cx| {
 925                project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
 926            }) {
 927                task.detach_and_log_err(cx);
 928            }
 929
 930            Some(())
 931        });
 932    }
 933
 934    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
 935        if let Some((worktree, entry)) = self.selected_entry(cx) {
 936            cx.write_to_clipboard(ClipboardItem::new(
 937                worktree
 938                    .abs_path()
 939                    .join(&entry.path)
 940                    .to_string_lossy()
 941                    .to_string(),
 942            ));
 943        }
 944    }
 945
 946    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
 947        if let Some((_, entry)) = self.selected_entry(cx) {
 948            cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string()));
 949        }
 950    }
 951
 952    fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
 953        if let Some((worktree, entry)) = self.selected_entry(cx) {
 954            cx.reveal_path(&worktree.abs_path().join(&entry.path));
 955        }
 956    }
 957
 958    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
 959        todo!()
 960        // if let Some((worktree, entry)) = self.selected_entry(cx) {
 961        //     let window = cx.window();
 962        //     let view_id = cx.view_id();
 963        //     let path = worktree.abs_path().join(&entry.path);
 964
 965        //     cx.app_context()
 966        //         .spawn(|mut cx| async move {
 967        //             window.dispatch_action(
 968        //                 view_id,
 969        //                 &workspace::OpenTerminal {
 970        //                     working_directory: path,
 971        //                 },
 972        //                 &mut cx,
 973        //             );
 974        //         })
 975        //         .detach();
 976        // }
 977    }
 978
 979    pub fn new_search_in_directory(
 980        &mut self,
 981        _: &NewSearchInDirectory,
 982        cx: &mut ViewContext<Self>,
 983    ) {
 984        if let Some((_, entry)) = self.selected_entry(cx) {
 985            if entry.is_dir() {
 986                cx.emit(Event::NewSearchInDirectory {
 987                    dir_entry: entry.clone(),
 988                });
 989            }
 990        }
 991    }
 992
 993    fn move_entry(
 994        &mut self,
 995        entry_to_move: ProjectEntryId,
 996        destination: ProjectEntryId,
 997        destination_is_file: bool,
 998        cx: &mut ViewContext<Self>,
 999    ) {
1000        let destination_worktree = self.project.update(cx, |project, cx| {
1001            let entry_path = project.path_for_entry(entry_to_move, cx)?;
1002            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
1003
1004            let mut destination_path = destination_entry_path.as_ref();
1005            if destination_is_file {
1006                destination_path = destination_path.parent()?;
1007            }
1008
1009            let mut new_path = destination_path.to_path_buf();
1010            new_path.push(entry_path.path.file_name()?);
1011            if new_path != entry_path.path.as_ref() {
1012                let task = project.rename_entry(entry_to_move, new_path, cx)?;
1013                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
1014            }
1015
1016            Some(project.worktree_id_for_entry(destination, cx)?)
1017        });
1018
1019        if let Some(destination_worktree) = destination_worktree {
1020            self.expand_entry(destination_worktree, destination, cx);
1021        }
1022    }
1023
1024    fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
1025        let mut entry_index = 0;
1026        let mut visible_entries_index = 0;
1027        for (worktree_index, (worktree_id, worktree_entries)) in
1028            self.visible_entries.iter().enumerate()
1029        {
1030            if *worktree_id == selection.worktree_id {
1031                for entry in worktree_entries {
1032                    if entry.id == selection.entry_id {
1033                        return Some((worktree_index, entry_index, visible_entries_index));
1034                    } else {
1035                        visible_entries_index += 1;
1036                        entry_index += 1;
1037                    }
1038                }
1039                break;
1040            } else {
1041                visible_entries_index += worktree_entries.len();
1042            }
1043        }
1044        None
1045    }
1046
1047    pub fn selected_entry<'a>(
1048        &self,
1049        cx: &'a AppContext,
1050    ) -> Option<(&'a Worktree, &'a project::Entry)> {
1051        let (worktree, entry) = self.selected_entry_handle(cx)?;
1052        Some((worktree.read(cx), entry))
1053    }
1054
1055    fn selected_entry_handle<'a>(
1056        &self,
1057        cx: &'a AppContext,
1058    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
1059        let selection = self.selection?;
1060        let project = self.project.read(cx);
1061        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
1062        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
1063        Some((worktree, entry))
1064    }
1065
1066    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
1067        let (worktree, entry) = self.selected_entry(cx)?;
1068        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
1069
1070        for path in entry.path.ancestors() {
1071            let Some(entry) = worktree.entry_for_path(path) else {
1072                continue;
1073            };
1074            if entry.is_dir() {
1075                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
1076                    expanded_dir_ids.insert(idx, entry.id);
1077                }
1078            }
1079        }
1080
1081        Some(())
1082    }
1083
1084    fn update_visible_entries(
1085        &mut self,
1086        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
1087        cx: &mut ViewContext<Self>,
1088    ) {
1089        let project = self.project.read(cx);
1090        self.last_worktree_root_id = project
1091            .visible_worktrees(cx)
1092            .rev()
1093            .next()
1094            .and_then(|worktree| worktree.read(cx).root_entry())
1095            .map(|entry| entry.id);
1096
1097        self.visible_entries.clear();
1098        for worktree in project.visible_worktrees(cx) {
1099            let snapshot = worktree.read(cx).snapshot();
1100            let worktree_id = snapshot.id();
1101
1102            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
1103                hash_map::Entry::Occupied(e) => e.into_mut(),
1104                hash_map::Entry::Vacant(e) => {
1105                    // The first time a worktree's root entry becomes available,
1106                    // mark that root entry as expanded.
1107                    if let Some(entry) = snapshot.root_entry() {
1108                        e.insert(vec![entry.id]).as_slice()
1109                    } else {
1110                        &[]
1111                    }
1112                }
1113            };
1114
1115            let mut new_entry_parent_id = None;
1116            let mut new_entry_kind = EntryKind::Dir;
1117            if let Some(edit_state) = &self.edit_state {
1118                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
1119                    new_entry_parent_id = Some(edit_state.entry_id);
1120                    new_entry_kind = if edit_state.is_dir {
1121                        EntryKind::Dir
1122                    } else {
1123                        EntryKind::File(Default::default())
1124                    };
1125                }
1126            }
1127
1128            let mut visible_worktree_entries = Vec::new();
1129            let mut entry_iter = snapshot.entries(true);
1130
1131            while let Some(entry) = entry_iter.entry() {
1132                visible_worktree_entries.push(entry.clone());
1133                if Some(entry.id) == new_entry_parent_id {
1134                    visible_worktree_entries.push(Entry {
1135                        id: NEW_ENTRY_ID,
1136                        kind: new_entry_kind,
1137                        path: entry.path.join("\0").into(),
1138                        inode: 0,
1139                        mtime: entry.mtime,
1140                        is_symlink: false,
1141                        is_ignored: false,
1142                        is_external: false,
1143                        git_status: entry.git_status,
1144                    });
1145                }
1146                if expanded_dir_ids.binary_search(&entry.id).is_err()
1147                    && entry_iter.advance_to_sibling()
1148                {
1149                    continue;
1150                }
1151                entry_iter.advance();
1152            }
1153
1154            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
1155
1156            visible_worktree_entries.sort_by(|entry_a, entry_b| {
1157                let mut components_a = entry_a.path.components().peekable();
1158                let mut components_b = entry_b.path.components().peekable();
1159                loop {
1160                    match (components_a.next(), components_b.next()) {
1161                        (Some(component_a), Some(component_b)) => {
1162                            let a_is_file = components_a.peek().is_none() && entry_a.is_file();
1163                            let b_is_file = components_b.peek().is_none() && entry_b.is_file();
1164                            let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1165                                let name_a =
1166                                    UniCase::new(component_a.as_os_str().to_string_lossy());
1167                                let name_b =
1168                                    UniCase::new(component_b.as_os_str().to_string_lossy());
1169                                name_a.cmp(&name_b)
1170                            });
1171                            if !ordering.is_eq() {
1172                                return ordering;
1173                            }
1174                        }
1175                        (Some(_), None) => break Ordering::Greater,
1176                        (None, Some(_)) => break Ordering::Less,
1177                        (None, None) => break Ordering::Equal,
1178                    }
1179                }
1180            });
1181            self.visible_entries
1182                .push((worktree_id, visible_worktree_entries));
1183        }
1184
1185        if let Some((worktree_id, entry_id)) = new_selected_entry {
1186            self.selection = Some(Selection {
1187                worktree_id,
1188                entry_id,
1189            });
1190        }
1191    }
1192
1193    fn expand_entry(
1194        &mut self,
1195        worktree_id: WorktreeId,
1196        entry_id: ProjectEntryId,
1197        cx: &mut ViewContext<Self>,
1198    ) {
1199        self.project.update(cx, |project, cx| {
1200            if let Some((worktree, expanded_dir_ids)) = project
1201                .worktree_for_id(worktree_id, cx)
1202                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1203            {
1204                project.expand_entry(worktree_id, entry_id, cx);
1205                let worktree = worktree.read(cx);
1206
1207                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1208                    loop {
1209                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1210                            expanded_dir_ids.insert(ix, entry.id);
1211                        }
1212
1213                        if let Some(parent_entry) =
1214                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1215                        {
1216                            entry = parent_entry;
1217                        } else {
1218                            break;
1219                        }
1220                    }
1221                }
1222            }
1223        });
1224    }
1225
1226    fn for_each_visible_entry(
1227        &self,
1228        range: Range<usize>,
1229        cx: &mut ViewContext<ProjectPanel>,
1230        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
1231    ) {
1232        let mut ix = 0;
1233        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
1234            if ix >= range.end {
1235                return;
1236            }
1237
1238            if ix + visible_worktree_entries.len() <= range.start {
1239                ix += visible_worktree_entries.len();
1240                continue;
1241            }
1242
1243            let end_ix = range.end.min(ix + visible_worktree_entries.len());
1244            let (git_status_setting, show_file_icons, show_folder_icons) = {
1245                let settings = ProjectPanelSettings::get_global(cx);
1246                (
1247                    settings.git_status,
1248                    settings.file_icons,
1249                    settings.folder_icons,
1250                )
1251            };
1252            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1253                let snapshot = worktree.read(cx).snapshot();
1254                let root_name = OsStr::new(snapshot.root_name());
1255                let expanded_entry_ids = self
1256                    .expanded_dir_ids
1257                    .get(&snapshot.id())
1258                    .map(Vec::as_slice)
1259                    .unwrap_or(&[]);
1260
1261                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
1262                for entry in visible_worktree_entries[entry_range].iter() {
1263                    let status = git_status_setting.then(|| entry.git_status).flatten();
1264                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
1265                    let icon = match entry.kind {
1266                        EntryKind::File(_) => {
1267                            if show_file_icons {
1268                                Some(FileAssociations::get_icon(&entry.path, cx))
1269                            } else {
1270                                None
1271                            }
1272                        }
1273                        _ => {
1274                            if show_folder_icons {
1275                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
1276                            } else {
1277                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
1278                            }
1279                        }
1280                    };
1281
1282                    let mut details = EntryDetails {
1283                        filename: entry
1284                            .path
1285                            .file_name()
1286                            .unwrap_or(root_name)
1287                            .to_string_lossy()
1288                            .to_string(),
1289                        icon,
1290                        path: entry.path.clone(),
1291                        depth: entry.path.components().count(),
1292                        kind: entry.kind,
1293                        is_ignored: entry.is_ignored,
1294                        is_expanded,
1295                        is_selected: self.selection.map_or(false, |e| {
1296                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
1297                        }),
1298                        is_editing: false,
1299                        is_processing: false,
1300                        is_cut: self
1301                            .clipboard_entry
1302                            .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
1303                        git_status: status,
1304                    };
1305
1306                    if let Some(edit_state) = &self.edit_state {
1307                        let is_edited_entry = if edit_state.is_new_entry {
1308                            entry.id == NEW_ENTRY_ID
1309                        } else {
1310                            entry.id == edit_state.entry_id
1311                        };
1312
1313                        if is_edited_entry {
1314                            if let Some(processing_filename) = &edit_state.processing_filename {
1315                                details.is_processing = true;
1316                                details.filename.clear();
1317                                details.filename.push_str(processing_filename);
1318                            } else {
1319                                if edit_state.is_new_entry {
1320                                    details.filename.clear();
1321                                }
1322                                details.is_editing = true;
1323                            }
1324                        }
1325                    }
1326
1327                    callback(entry.id, details, cx);
1328                }
1329            }
1330            ix = end_ix;
1331        }
1332    }
1333
1334    fn render_entry_visual_element(
1335        details: &EntryDetails,
1336        editor: Option<&View<Editor>>,
1337        padding: Pixels,
1338        cx: &mut ViewContext<Self>,
1339    ) -> Div<Self> {
1340        let show_editor = details.is_editing && !details.is_processing;
1341
1342        let theme = cx.theme();
1343        let filename_text_color = details
1344            .git_status
1345            .as_ref()
1346            .map(|status| match status {
1347                GitFileStatus::Added => theme.status().created,
1348                GitFileStatus::Modified => theme.status().modified,
1349                GitFileStatus::Conflict => theme.status().conflict,
1350            })
1351            .unwrap_or(theme.status().info);
1352
1353        h_stack()
1354            .child(if let Some(icon) = &details.icon {
1355                div().child(
1356                    // todo!() Marshall: Can we use our `IconElement` component here?
1357                    svg()
1358                        .size(rems(0.9375))
1359                        .flex_none()
1360                        .path(icon.to_string())
1361                        .text_color(cx.theme().colors().icon),
1362                )
1363            } else {
1364                div()
1365            })
1366            .child(
1367                if let (Some(editor), true) = (editor, show_editor) {
1368                    div().w_full().child(editor.clone())
1369                } else {
1370                    div().child(Label::new(details.filename.clone()))
1371                }
1372                .ml_1(),
1373            )
1374            .pl(padding)
1375    }
1376
1377    fn render_entry(
1378        &self,
1379        entry_id: ProjectEntryId,
1380        details: EntryDetails,
1381        // dragged_entry_destination: &mut Option<Arc<Path>>,
1382        cx: &mut ViewContext<Self>,
1383    ) -> Div<Self, StatefulInteractivity<Self>> {
1384        let kind = details.kind;
1385        let settings = ProjectPanelSettings::get_global(cx);
1386        const INDENT_SIZE: Pixels = px(16.0);
1387        let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
1388        let show_editor = details.is_editing && !details.is_processing;
1389        let is_selected = self
1390            .selection
1391            .map_or(false, |selection| selection.entry_id == entry_id);
1392
1393        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
1394            .id(entry_id.to_proto() as usize)
1395            .w_full()
1396            .cursor_pointer()
1397            .when(is_selected, |this| {
1398                this.bg(cx.theme().colors().element_selected)
1399            })
1400            .hover(|style| style.bg(cx.theme().colors().element_hover))
1401            .on_click(move |this, event, cx| {
1402                if !show_editor {
1403                    if kind.is_dir() {
1404                        this.toggle_expanded(entry_id, cx);
1405                    } else {
1406                        if event.down.modifiers.command {
1407                            this.split_entry(entry_id, cx);
1408                        } else {
1409                            this.open_entry(entry_id, event.up.click_count > 1, cx);
1410                        }
1411                    }
1412                }
1413            })
1414        // .on_down(MouseButton::Right, move |event, this, cx| {
1415        //     this.deploy_context_menu(event.position, entry_id, cx);
1416        // })
1417        // .on_up(MouseButton::Left, move |_, this, cx| {
1418        // if let Some((_, dragged_entry)) = cx
1419        //     .global::<DragAndDrop<Workspace>>()
1420        //     .currently_dragged::<ProjectEntryId>(cx.window())
1421        // {
1422        //     this.move_entry(
1423        //         *dragged_entry,
1424        //         entry_id,
1425        //         matches!(details.kind, EntryKind::File(_)),
1426        //         cx,
1427        //     );
1428        // }
1429        // })
1430    }
1431}
1432
1433impl Render for ProjectPanel {
1434    type Element = Div<Self, StatefulInteractivity<Self>, FocusableKeyDispatch<Self>>;
1435
1436    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
1437        let theme = cx.theme();
1438        let last_worktree_root_id = self.last_worktree_root_id;
1439
1440        let has_worktree = self.visible_entries.len() != 0;
1441
1442        if has_worktree {
1443            div()
1444                .id("project-panel")
1445                .size_full()
1446                .context("ProjectPanel")
1447                .on_action(Self::select_next)
1448                .on_action(Self::select_prev)
1449                .on_action(Self::expand_selected_entry)
1450                .on_action(Self::collapse_selected_entry)
1451                .on_action(Self::collapse_all_entries)
1452                .on_action(Self::new_file)
1453                .on_action(Self::new_directory)
1454                .on_action(Self::rename)
1455                .on_action(Self::delete)
1456                .on_action(Self::confirm)
1457                .on_action(Self::open_file)
1458                .on_action(Self::cancel)
1459                .on_action(Self::cut)
1460                .on_action(Self::copy)
1461                .on_action(Self::copy_path)
1462                .on_action(Self::copy_relative_path)
1463                .on_action(Self::paste)
1464                .on_action(Self::reveal_in_finder)
1465                .on_action(Self::open_in_terminal)
1466                .on_action(Self::new_search_in_directory)
1467                .track_focus(&self.focus_handle)
1468                .child(
1469                    uniform_list(
1470                        "entries",
1471                        self.visible_entries
1472                            .iter()
1473                            .map(|(_, worktree_entries)| worktree_entries.len())
1474                            .sum(),
1475                        |this: &mut Self, range, cx| {
1476                            let mut items = SmallVec::new();
1477                            this.for_each_visible_entry(range, cx, |id, details, cx| {
1478                                items.push(this.render_entry(
1479                                    id, details, // &mut dragged_entry_destination,
1480                                    cx,
1481                                ));
1482                            });
1483                            items
1484                        },
1485                    )
1486                    .size_full()
1487                    .track_scroll(self.list.clone()),
1488                )
1489        } else {
1490            v_stack()
1491                .id("empty-project_panel")
1492                .track_focus(&self.focus_handle)
1493        }
1494    }
1495}
1496
1497impl EventEmitter<Event> for ProjectPanel {}
1498
1499impl EventEmitter<PanelEvent> for ProjectPanel {}
1500
1501impl workspace::dock::Panel for ProjectPanel {
1502    fn position(&self, cx: &WindowContext) -> DockPosition {
1503        match ProjectPanelSettings::get_global(cx).dock {
1504            ProjectPanelDockPosition::Left => DockPosition::Left,
1505            ProjectPanelDockPosition::Right => DockPosition::Right,
1506        }
1507    }
1508
1509    fn position_is_valid(&self, position: DockPosition) -> bool {
1510        matches!(position, DockPosition::Left | DockPosition::Right)
1511    }
1512
1513    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1514        settings::update_settings_file::<ProjectPanelSettings>(
1515            self.fs.clone(),
1516            cx,
1517            move |settings| {
1518                let dock = match position {
1519                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
1520                    DockPosition::Right => ProjectPanelDockPosition::Right,
1521                };
1522                settings.dock = Some(dock);
1523            },
1524        );
1525    }
1526
1527    fn size(&self, cx: &WindowContext) -> f32 {
1528        self.width
1529            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
1530    }
1531
1532    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
1533        self.width = size;
1534        self.serialize(cx);
1535        cx.notify();
1536    }
1537
1538    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
1539        Some("icons/project.svg")
1540    }
1541
1542    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
1543        ("Project Panel".into(), Some(Box::new(ToggleFocus)))
1544    }
1545
1546    // fn should_change_position_on_event(event: &Self::Event) -> bool {
1547    //     matches!(event, Event::DockPositionChanged)
1548    // }
1549
1550    fn has_focus(&self, _: &WindowContext) -> bool {
1551        self.has_focus
1552    }
1553
1554    fn persistent_name(&self) -> &'static str {
1555        "Project Panel"
1556    }
1557
1558    fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
1559        self.focus_handle.clone()
1560    }
1561
1562    // fn is_focus_event(event: &Self::Event) -> bool {
1563    //     matches!(event, Event::Focus)
1564    // }
1565}
1566
1567impl ClipboardEntry {
1568    fn is_cut(&self) -> bool {
1569        matches!(self, Self::Cut { .. })
1570    }
1571
1572    fn entry_id(&self) -> ProjectEntryId {
1573        match self {
1574            ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
1575                *entry_id
1576            }
1577        }
1578    }
1579
1580    fn worktree_id(&self) -> WorktreeId {
1581        match self {
1582            ClipboardEntry::Copied { worktree_id, .. }
1583            | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
1584        }
1585    }
1586}
1587
1588// todo!()
1589// #[cfg(test)]
1590// mod tests {
1591//     use super::*;
1592//     use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle};
1593//     use pretty_assertions::assert_eq;
1594//     use project::FakeFs;
1595//     use serde_json::json;
1596//     use settings::SettingsStore;
1597//     use std::{
1598//         collections::HashSet,
1599//         path::{Path, PathBuf},
1600//         sync::atomic::{self, AtomicUsize},
1601//     };
1602//     use workspace::{pane, AppState};
1603
1604//     #[gpui::test]
1605//     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
1606//         init_test(cx);
1607
1608//         let fs = FakeFs::new(cx.executor().clone());
1609//         fs.insert_tree(
1610//             "/root1",
1611//             json!({
1612//                 ".dockerignore": "",
1613//                 ".git": {
1614//                     "HEAD": "",
1615//                 },
1616//                 "a": {
1617//                     "0": { "q": "", "r": "", "s": "" },
1618//                     "1": { "t": "", "u": "" },
1619//                     "2": { "v": "", "w": "", "x": "", "y": "" },
1620//                 },
1621//                 "b": {
1622//                     "3": { "Q": "" },
1623//                     "4": { "R": "", "S": "", "T": "", "U": "" },
1624//                 },
1625//                 "C": {
1626//                     "5": {},
1627//                     "6": { "V": "", "W": "" },
1628//                     "7": { "X": "" },
1629//                     "8": { "Y": {}, "Z": "" }
1630//                 }
1631//             }),
1632//         )
1633//         .await;
1634//         fs.insert_tree(
1635//             "/root2",
1636//             json!({
1637//                 "d": {
1638//                     "9": ""
1639//                 },
1640//                 "e": {}
1641//             }),
1642//         )
1643//         .await;
1644
1645//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1646//         let workspace = cx
1647//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
1648//             .root(cx);
1649//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1650//         assert_eq!(
1651//             visible_entries_as_strings(&panel, 0..50, cx),
1652//             &[
1653//                 "v root1",
1654//                 "    > .git",
1655//                 "    > a",
1656//                 "    > b",
1657//                 "    > C",
1658//                 "      .dockerignore",
1659//                 "v root2",
1660//                 "    > d",
1661//                 "    > e",
1662//             ]
1663//         );
1664
1665//         toggle_expand_dir(&panel, "root1/b", cx);
1666//         assert_eq!(
1667//             visible_entries_as_strings(&panel, 0..50, cx),
1668//             &[
1669//                 "v root1",
1670//                 "    > .git",
1671//                 "    > a",
1672//                 "    v b  <== selected",
1673//                 "        > 3",
1674//                 "        > 4",
1675//                 "    > C",
1676//                 "      .dockerignore",
1677//                 "v root2",
1678//                 "    > d",
1679//                 "    > e",
1680//             ]
1681//         );
1682
1683//         assert_eq!(
1684//             visible_entries_as_strings(&panel, 6..9, cx),
1685//             &[
1686//                 //
1687//                 "    > C",
1688//                 "      .dockerignore",
1689//                 "v root2",
1690//             ]
1691//         );
1692//     }
1693
1694//     #[gpui::test(iterations = 30)]
1695//     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
1696//         init_test(cx);
1697
1698//         let fs = FakeFs::new(cx.background());
1699//         fs.insert_tree(
1700//             "/root1",
1701//             json!({
1702//                 ".dockerignore": "",
1703//                 ".git": {
1704//                     "HEAD": "",
1705//                 },
1706//                 "a": {
1707//                     "0": { "q": "", "r": "", "s": "" },
1708//                     "1": { "t": "", "u": "" },
1709//                     "2": { "v": "", "w": "", "x": "", "y": "" },
1710//                 },
1711//                 "b": {
1712//                     "3": { "Q": "" },
1713//                     "4": { "R": "", "S": "", "T": "", "U": "" },
1714//                 },
1715//                 "C": {
1716//                     "5": {},
1717//                     "6": { "V": "", "W": "" },
1718//                     "7": { "X": "" },
1719//                     "8": { "Y": {}, "Z": "" }
1720//                 }
1721//             }),
1722//         )
1723//         .await;
1724//         fs.insert_tree(
1725//             "/root2",
1726//             json!({
1727//                 "d": {
1728//                     "9": ""
1729//                 },
1730//                 "e": {}
1731//             }),
1732//         )
1733//         .await;
1734
1735//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1736//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1737//         let workspace = window.root(cx);
1738//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1739
1740//         select_path(&panel, "root1", cx);
1741//         assert_eq!(
1742//             visible_entries_as_strings(&panel, 0..10, cx),
1743//             &[
1744//                 "v root1  <== selected",
1745//                 "    > .git",
1746//                 "    > a",
1747//                 "    > b",
1748//                 "    > C",
1749//                 "      .dockerignore",
1750//                 "v root2",
1751//                 "    > d",
1752//                 "    > e",
1753//             ]
1754//         );
1755
1756//         // Add a file with the root folder selected. The filename editor is placed
1757//         // before the first file in the root folder.
1758//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1759//         window.read_with(cx, |cx| {
1760//             let panel = panel.read(cx);
1761//             assert!(panel.filename_editor.is_focused(cx));
1762//         });
1763//         assert_eq!(
1764//             visible_entries_as_strings(&panel, 0..10, cx),
1765//             &[
1766//                 "v root1",
1767//                 "    > .git",
1768//                 "    > a",
1769//                 "    > b",
1770//                 "    > C",
1771//                 "      [EDITOR: '']  <== selected",
1772//                 "      .dockerignore",
1773//                 "v root2",
1774//                 "    > d",
1775//                 "    > e",
1776//             ]
1777//         );
1778
1779//         let confirm = panel.update(cx, |panel, cx| {
1780//             panel
1781//                 .filename_editor
1782//                 .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
1783//             panel.confirm(&Confirm, cx).unwrap()
1784//         });
1785//         assert_eq!(
1786//             visible_entries_as_strings(&panel, 0..10, cx),
1787//             &[
1788//                 "v root1",
1789//                 "    > .git",
1790//                 "    > a",
1791//                 "    > b",
1792//                 "    > C",
1793//                 "      [PROCESSING: 'the-new-filename']  <== selected",
1794//                 "      .dockerignore",
1795//                 "v root2",
1796//                 "    > d",
1797//                 "    > e",
1798//             ]
1799//         );
1800
1801//         confirm.await.unwrap();
1802//         assert_eq!(
1803//             visible_entries_as_strings(&panel, 0..10, cx),
1804//             &[
1805//                 "v root1",
1806//                 "    > .git",
1807//                 "    > a",
1808//                 "    > b",
1809//                 "    > C",
1810//                 "      .dockerignore",
1811//                 "      the-new-filename  <== selected",
1812//                 "v root2",
1813//                 "    > d",
1814//                 "    > e",
1815//             ]
1816//         );
1817
1818//         select_path(&panel, "root1/b", cx);
1819//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1820//         assert_eq!(
1821//             visible_entries_as_strings(&panel, 0..10, cx),
1822//             &[
1823//                 "v root1",
1824//                 "    > .git",
1825//                 "    > a",
1826//                 "    v b",
1827//                 "        > 3",
1828//                 "        > 4",
1829//                 "          [EDITOR: '']  <== selected",
1830//                 "    > C",
1831//                 "      .dockerignore",
1832//                 "      the-new-filename",
1833//             ]
1834//         );
1835
1836//         panel
1837//             .update(cx, |panel, cx| {
1838//                 panel
1839//                     .filename_editor
1840//                     .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
1841//                 panel.confirm(&Confirm, cx).unwrap()
1842//             })
1843//             .await
1844//             .unwrap();
1845//         assert_eq!(
1846//             visible_entries_as_strings(&panel, 0..10, cx),
1847//             &[
1848//                 "v root1",
1849//                 "    > .git",
1850//                 "    > a",
1851//                 "    v b",
1852//                 "        > 3",
1853//                 "        > 4",
1854//                 "          another-filename.txt  <== selected",
1855//                 "    > C",
1856//                 "      .dockerignore",
1857//                 "      the-new-filename",
1858//             ]
1859//         );
1860
1861//         select_path(&panel, "root1/b/another-filename.txt", cx);
1862//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
1863//         assert_eq!(
1864//             visible_entries_as_strings(&panel, 0..10, cx),
1865//             &[
1866//                 "v root1",
1867//                 "    > .git",
1868//                 "    > a",
1869//                 "    v b",
1870//                 "        > 3",
1871//                 "        > 4",
1872//                 "          [EDITOR: 'another-filename.txt']  <== selected",
1873//                 "    > C",
1874//                 "      .dockerignore",
1875//                 "      the-new-filename",
1876//             ]
1877//         );
1878
1879//         let confirm = panel.update(cx, |panel, cx| {
1880//             panel.filename_editor.update(cx, |editor, cx| {
1881//                 let file_name_selections = editor.selections.all::<usize>(cx);
1882//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
1883//                 let file_name_selection = &file_name_selections[0];
1884//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
1885//                 assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
1886
1887//                 editor.set_text("a-different-filename.tar.gz", cx)
1888//             });
1889//             panel.confirm(&Confirm, cx).unwrap()
1890//         });
1891//         assert_eq!(
1892//             visible_entries_as_strings(&panel, 0..10, cx),
1893//             &[
1894//                 "v root1",
1895//                 "    > .git",
1896//                 "    > a",
1897//                 "    v b",
1898//                 "        > 3",
1899//                 "        > 4",
1900//                 "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
1901//                 "    > C",
1902//                 "      .dockerignore",
1903//                 "      the-new-filename",
1904//             ]
1905//         );
1906
1907//         confirm.await.unwrap();
1908//         assert_eq!(
1909//             visible_entries_as_strings(&panel, 0..10, cx),
1910//             &[
1911//                 "v root1",
1912//                 "    > .git",
1913//                 "    > a",
1914//                 "    v b",
1915//                 "        > 3",
1916//                 "        > 4",
1917//                 "          a-different-filename.tar.gz  <== selected",
1918//                 "    > C",
1919//                 "      .dockerignore",
1920//                 "      the-new-filename",
1921//             ]
1922//         );
1923
1924//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
1925//         assert_eq!(
1926//             visible_entries_as_strings(&panel, 0..10, cx),
1927//             &[
1928//                 "v root1",
1929//                 "    > .git",
1930//                 "    > a",
1931//                 "    v b",
1932//                 "        > 3",
1933//                 "        > 4",
1934//                 "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
1935//                 "    > C",
1936//                 "      .dockerignore",
1937//                 "      the-new-filename",
1938//             ]
1939//         );
1940
1941//         panel.update(cx, |panel, cx| {
1942//             panel.filename_editor.update(cx, |editor, cx| {
1943//                 let file_name_selections = editor.selections.all::<usize>(cx);
1944//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
1945//                 let file_name_selection = &file_name_selections[0];
1946//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
1947//                 assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot");
1948
1949//             });
1950//             panel.cancel(&Cancel, cx)
1951//         });
1952
1953//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
1954//         assert_eq!(
1955//             visible_entries_as_strings(&panel, 0..10, cx),
1956//             &[
1957//                 "v root1",
1958//                 "    > .git",
1959//                 "    > a",
1960//                 "    v b",
1961//                 "        > [EDITOR: '']  <== selected",
1962//                 "        > 3",
1963//                 "        > 4",
1964//                 "          a-different-filename.tar.gz",
1965//                 "    > C",
1966//                 "      .dockerignore",
1967//             ]
1968//         );
1969
1970//         let confirm = panel.update(cx, |panel, cx| {
1971//             panel
1972//                 .filename_editor
1973//                 .update(cx, |editor, cx| editor.set_text("new-dir", cx));
1974//             panel.confirm(&Confirm, cx).unwrap()
1975//         });
1976//         panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
1977//         assert_eq!(
1978//             visible_entries_as_strings(&panel, 0..10, cx),
1979//             &[
1980//                 "v root1",
1981//                 "    > .git",
1982//                 "    > a",
1983//                 "    v b",
1984//                 "        > [PROCESSING: 'new-dir']",
1985//                 "        > 3  <== selected",
1986//                 "        > 4",
1987//                 "          a-different-filename.tar.gz",
1988//                 "    > C",
1989//                 "      .dockerignore",
1990//             ]
1991//         );
1992
1993//         confirm.await.unwrap();
1994//         assert_eq!(
1995//             visible_entries_as_strings(&panel, 0..10, cx),
1996//             &[
1997//                 "v root1",
1998//                 "    > .git",
1999//                 "    > a",
2000//                 "    v b",
2001//                 "        > 3  <== selected",
2002//                 "        > 4",
2003//                 "        > new-dir",
2004//                 "          a-different-filename.tar.gz",
2005//                 "    > C",
2006//                 "      .dockerignore",
2007//             ]
2008//         );
2009
2010//         panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
2011//         assert_eq!(
2012//             visible_entries_as_strings(&panel, 0..10, cx),
2013//             &[
2014//                 "v root1",
2015//                 "    > .git",
2016//                 "    > a",
2017//                 "    v b",
2018//                 "        > [EDITOR: '3']  <== selected",
2019//                 "        > 4",
2020//                 "        > new-dir",
2021//                 "          a-different-filename.tar.gz",
2022//                 "    > C",
2023//                 "      .dockerignore",
2024//             ]
2025//         );
2026
2027//         // Dismiss the rename editor when it loses focus.
2028//         workspace.update(cx, |_, cx| cx.focus_self());
2029//         assert_eq!(
2030//             visible_entries_as_strings(&panel, 0..10, cx),
2031//             &[
2032//                 "v root1",
2033//                 "    > .git",
2034//                 "    > a",
2035//                 "    v b",
2036//                 "        > 3  <== selected",
2037//                 "        > 4",
2038//                 "        > new-dir",
2039//                 "          a-different-filename.tar.gz",
2040//                 "    > C",
2041//                 "      .dockerignore",
2042//             ]
2043//         );
2044//     }
2045
2046//     #[gpui::test(iterations = 30)]
2047//     async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
2048//         init_test(cx);
2049
2050//         let fs = FakeFs::new(cx.background());
2051//         fs.insert_tree(
2052//             "/root1",
2053//             json!({
2054//                 ".dockerignore": "",
2055//                 ".git": {
2056//                     "HEAD": "",
2057//                 },
2058//                 "a": {
2059//                     "0": { "q": "", "r": "", "s": "" },
2060//                     "1": { "t": "", "u": "" },
2061//                     "2": { "v": "", "w": "", "x": "", "y": "" },
2062//                 },
2063//                 "b": {
2064//                     "3": { "Q": "" },
2065//                     "4": { "R": "", "S": "", "T": "", "U": "" },
2066//                 },
2067//                 "C": {
2068//                     "5": {},
2069//                     "6": { "V": "", "W": "" },
2070//                     "7": { "X": "" },
2071//                     "8": { "Y": {}, "Z": "" }
2072//                 }
2073//             }),
2074//         )
2075//         .await;
2076//         fs.insert_tree(
2077//             "/root2",
2078//             json!({
2079//                 "d": {
2080//                     "9": ""
2081//                 },
2082//                 "e": {}
2083//             }),
2084//         )
2085//         .await;
2086
2087//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
2088//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2089//         let workspace = window.root(cx);
2090//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2091
2092//         select_path(&panel, "root1", cx);
2093//         assert_eq!(
2094//             visible_entries_as_strings(&panel, 0..10, cx),
2095//             &[
2096//                 "v root1  <== selected",
2097//                 "    > .git",
2098//                 "    > a",
2099//                 "    > b",
2100//                 "    > C",
2101//                 "      .dockerignore",
2102//                 "v root2",
2103//                 "    > d",
2104//                 "    > e",
2105//             ]
2106//         );
2107
2108//         // Add a file with the root folder selected. The filename editor is placed
2109//         // before the first file in the root folder.
2110//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
2111//         window.read_with(cx, |cx| {
2112//             let panel = panel.read(cx);
2113//             assert!(panel.filename_editor.is_focused(cx));
2114//         });
2115//         assert_eq!(
2116//             visible_entries_as_strings(&panel, 0..10, cx),
2117//             &[
2118//                 "v root1",
2119//                 "    > .git",
2120//                 "    > a",
2121//                 "    > b",
2122//                 "    > C",
2123//                 "      [EDITOR: '']  <== selected",
2124//                 "      .dockerignore",
2125//                 "v root2",
2126//                 "    > d",
2127//                 "    > e",
2128//             ]
2129//         );
2130
2131//         let confirm = panel.update(cx, |panel, cx| {
2132//             panel.filename_editor.update(cx, |editor, cx| {
2133//                 editor.set_text("/bdir1/dir2/the-new-filename", cx)
2134//             });
2135//             panel.confirm(&Confirm, cx).unwrap()
2136//         });
2137
2138//         assert_eq!(
2139//             visible_entries_as_strings(&panel, 0..10, cx),
2140//             &[
2141//                 "v root1",
2142//                 "    > .git",
2143//                 "    > a",
2144//                 "    > b",
2145//                 "    > C",
2146//                 "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
2147//                 "      .dockerignore",
2148//                 "v root2",
2149//                 "    > d",
2150//                 "    > e",
2151//             ]
2152//         );
2153
2154//         confirm.await.unwrap();
2155//         assert_eq!(
2156//             visible_entries_as_strings(&panel, 0..13, cx),
2157//             &[
2158//                 "v root1",
2159//                 "    > .git",
2160//                 "    > a",
2161//                 "    > b",
2162//                 "    v bdir1",
2163//                 "        v dir2",
2164//                 "              the-new-filename  <== selected",
2165//                 "    > C",
2166//                 "      .dockerignore",
2167//                 "v root2",
2168//                 "    > d",
2169//                 "    > e",
2170//             ]
2171//         );
2172//     }
2173
2174//     #[gpui::test]
2175//     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
2176//         init_test(cx);
2177
2178//         let fs = FakeFs::new(cx.background());
2179//         fs.insert_tree(
2180//             "/root1",
2181//             json!({
2182//                 "one.two.txt": "",
2183//                 "one.txt": ""
2184//             }),
2185//         )
2186//         .await;
2187
2188//         let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2189//         let workspace = cx
2190//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2191//             .root(cx);
2192//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2193
2194//         panel.update(cx, |panel, cx| {
2195//             panel.select_next(&Default::default(), cx);
2196//             panel.select_next(&Default::default(), cx);
2197//         });
2198
2199//         assert_eq!(
2200//             visible_entries_as_strings(&panel, 0..50, cx),
2201//             &[
2202//                 //
2203//                 "v root1",
2204//                 "      one.two.txt  <== selected",
2205//                 "      one.txt",
2206//             ]
2207//         );
2208
2209//         // Regression test - file name is created correctly when
2210//         // the copied file's name contains multiple dots.
2211//         panel.update(cx, |panel, cx| {
2212//             panel.copy(&Default::default(), cx);
2213//             panel.paste(&Default::default(), cx);
2214//         });
2215//         cx.foreground().run_until_parked();
2216
2217//         assert_eq!(
2218//             visible_entries_as_strings(&panel, 0..50, cx),
2219//             &[
2220//                 //
2221//                 "v root1",
2222//                 "      one.two copy.txt",
2223//                 "      one.two.txt  <== selected",
2224//                 "      one.txt",
2225//             ]
2226//         );
2227
2228//         panel.update(cx, |panel, cx| {
2229//             panel.paste(&Default::default(), cx);
2230//         });
2231//         cx.foreground().run_until_parked();
2232
2233//         assert_eq!(
2234//             visible_entries_as_strings(&panel, 0..50, cx),
2235//             &[
2236//                 //
2237//                 "v root1",
2238//                 "      one.two copy 1.txt",
2239//                 "      one.two copy.txt",
2240//                 "      one.two.txt  <== selected",
2241//                 "      one.txt",
2242//             ]
2243//         );
2244//     }
2245
2246//     #[gpui::test]
2247//     async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
2248//         init_test_with_editor(cx);
2249
2250//         let fs = FakeFs::new(cx.background());
2251//         fs.insert_tree(
2252//             "/src",
2253//             json!({
2254//                 "test": {
2255//                     "first.rs": "// First Rust file",
2256//                     "second.rs": "// Second Rust file",
2257//                     "third.rs": "// Third Rust file",
2258//                 }
2259//             }),
2260//         )
2261//         .await;
2262
2263//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2264//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2265//         let workspace = window.root(cx);
2266//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2267
2268//         toggle_expand_dir(&panel, "src/test", cx);
2269//         select_path(&panel, "src/test/first.rs", cx);
2270//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
2271//         cx.foreground().run_until_parked();
2272//         assert_eq!(
2273//             visible_entries_as_strings(&panel, 0..10, cx),
2274//             &[
2275//                 "v src",
2276//                 "    v test",
2277//                 "          first.rs  <== selected",
2278//                 "          second.rs",
2279//                 "          third.rs"
2280//             ]
2281//         );
2282//         ensure_single_file_is_opened(window, "test/first.rs", cx);
2283
2284//         submit_deletion(window.into(), &panel, cx);
2285//         assert_eq!(
2286//             visible_entries_as_strings(&panel, 0..10, cx),
2287//             &[
2288//                 "v src",
2289//                 "    v test",
2290//                 "          second.rs",
2291//                 "          third.rs"
2292//             ],
2293//             "Project panel should have no deleted file, no other file is selected in it"
2294//         );
2295//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
2296
2297//         select_path(&panel, "src/test/second.rs", cx);
2298//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
2299//         cx.foreground().run_until_parked();
2300//         assert_eq!(
2301//             visible_entries_as_strings(&panel, 0..10, cx),
2302//             &[
2303//                 "v src",
2304//                 "    v test",
2305//                 "          second.rs  <== selected",
2306//                 "          third.rs"
2307//             ]
2308//         );
2309//         ensure_single_file_is_opened(window, "test/second.rs", cx);
2310
2311//         window.update(cx, |cx| {
2312//             let active_items = workspace
2313//                 .read(cx)
2314//                 .panes()
2315//                 .iter()
2316//                 .filter_map(|pane| pane.read(cx).active_item())
2317//                 .collect::<Vec<_>>();
2318//             assert_eq!(active_items.len(), 1);
2319//             let open_editor = active_items
2320//                 .into_iter()
2321//                 .next()
2322//                 .unwrap()
2323//                 .downcast::<Editor>()
2324//                 .expect("Open item should be an editor");
2325//             open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
2326//         });
2327//         submit_deletion(window.into(), &panel, cx);
2328//         assert_eq!(
2329//             visible_entries_as_strings(&panel, 0..10, cx),
2330//             &["v src", "    v test", "          third.rs"],
2331//             "Project panel should have no deleted file, with one last file remaining"
2332//         );
2333//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
2334//     }
2335
2336//     #[gpui::test]
2337//     async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2338//         init_test_with_editor(cx);
2339
2340//         let fs = FakeFs::new(cx.background());
2341//         fs.insert_tree(
2342//             "/src",
2343//             json!({
2344//                 "test": {
2345//                     "first.rs": "// First Rust file",
2346//                     "second.rs": "// Second Rust file",
2347//                     "third.rs": "// Third Rust file",
2348//                 }
2349//             }),
2350//         )
2351//         .await;
2352
2353//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2354//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2355//         let workspace = window.root(cx);
2356//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2357
2358//         select_path(&panel, "src/", cx);
2359//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2360//         cx.foreground().run_until_parked();
2361//         assert_eq!(
2362//             visible_entries_as_strings(&panel, 0..10, cx),
2363//             &["v src  <== selected", "    > test"]
2364//         );
2365//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
2366//         window.read_with(cx, |cx| {
2367//             let panel = panel.read(cx);
2368//             assert!(panel.filename_editor.is_focused(cx));
2369//         });
2370//         assert_eq!(
2371//             visible_entries_as_strings(&panel, 0..10, cx),
2372//             &["v src", "    > [EDITOR: '']  <== selected", "    > test"]
2373//         );
2374//         panel.update(cx, |panel, cx| {
2375//             panel
2376//                 .filename_editor
2377//                 .update(cx, |editor, cx| editor.set_text("test", cx));
2378//             assert!(
2379//                 panel.confirm(&Confirm, cx).is_none(),
2380//                 "Should not allow to confirm on conflicting new directory name"
2381//             )
2382//         });
2383//         assert_eq!(
2384//             visible_entries_as_strings(&panel, 0..10, cx),
2385//             &["v src", "    > test"],
2386//             "File list should be unchanged after failed folder create confirmation"
2387//         );
2388
2389//         select_path(&panel, "src/test/", cx);
2390//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2391//         cx.foreground().run_until_parked();
2392//         assert_eq!(
2393//             visible_entries_as_strings(&panel, 0..10, cx),
2394//             &["v src", "    > test  <== selected"]
2395//         );
2396//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
2397//         window.read_with(cx, |cx| {
2398//             let panel = panel.read(cx);
2399//             assert!(panel.filename_editor.is_focused(cx));
2400//         });
2401//         assert_eq!(
2402//             visible_entries_as_strings(&panel, 0..10, cx),
2403//             &[
2404//                 "v src",
2405//                 "    v test",
2406//                 "          [EDITOR: '']  <== selected",
2407//                 "          first.rs",
2408//                 "          second.rs",
2409//                 "          third.rs"
2410//             ]
2411//         );
2412//         panel.update(cx, |panel, cx| {
2413//             panel
2414//                 .filename_editor
2415//                 .update(cx, |editor, cx| editor.set_text("first.rs", cx));
2416//             assert!(
2417//                 panel.confirm(&Confirm, cx).is_none(),
2418//                 "Should not allow to confirm on conflicting new file name"
2419//             )
2420//         });
2421//         assert_eq!(
2422//             visible_entries_as_strings(&panel, 0..10, cx),
2423//             &[
2424//                 "v src",
2425//                 "    v test",
2426//                 "          first.rs",
2427//                 "          second.rs",
2428//                 "          third.rs"
2429//             ],
2430//             "File list should be unchanged after failed file create confirmation"
2431//         );
2432
2433//         select_path(&panel, "src/test/first.rs", cx);
2434//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2435//         cx.foreground().run_until_parked();
2436//         assert_eq!(
2437//             visible_entries_as_strings(&panel, 0..10, cx),
2438//             &[
2439//                 "v src",
2440//                 "    v test",
2441//                 "          first.rs  <== selected",
2442//                 "          second.rs",
2443//                 "          third.rs"
2444//             ],
2445//         );
2446//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
2447//         window.read_with(cx, |cx| {
2448//             let panel = panel.read(cx);
2449//             assert!(panel.filename_editor.is_focused(cx));
2450//         });
2451//         assert_eq!(
2452//             visible_entries_as_strings(&panel, 0..10, cx),
2453//             &[
2454//                 "v src",
2455//                 "    v test",
2456//                 "          [EDITOR: 'first.rs']  <== selected",
2457//                 "          second.rs",
2458//                 "          third.rs"
2459//             ]
2460//         );
2461//         panel.update(cx, |panel, cx| {
2462//             panel
2463//                 .filename_editor
2464//                 .update(cx, |editor, cx| editor.set_text("second.rs", cx));
2465//             assert!(
2466//                 panel.confirm(&Confirm, cx).is_none(),
2467//                 "Should not allow to confirm on conflicting file rename"
2468//             )
2469//         });
2470//         assert_eq!(
2471//             visible_entries_as_strings(&panel, 0..10, cx),
2472//             &[
2473//                 "v src",
2474//                 "    v test",
2475//                 "          first.rs  <== selected",
2476//                 "          second.rs",
2477//                 "          third.rs"
2478//             ],
2479//             "File list should be unchanged after failed rename confirmation"
2480//         );
2481//     }
2482
2483//     #[gpui::test]
2484//     async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
2485//         init_test_with_editor(cx);
2486
2487//         let fs = FakeFs::new(cx.background());
2488//         fs.insert_tree(
2489//             "/src",
2490//             json!({
2491//                 "test": {
2492//                     "first.rs": "// First Rust file",
2493//                     "second.rs": "// Second Rust file",
2494//                     "third.rs": "// Third Rust file",
2495//                 }
2496//             }),
2497//         )
2498//         .await;
2499
2500//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2501//         let workspace = cx
2502//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2503//             .root(cx);
2504//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2505
2506//         let new_search_events_count = Arc::new(AtomicUsize::new(0));
2507//         let _subscription = panel.update(cx, |_, cx| {
2508//             let subcription_count = Arc::clone(&new_search_events_count);
2509//             cx.subscribe(&cx.handle(), move |_, _, event, _| {
2510//                 if matches!(event, Event::NewSearchInDirectory { .. }) {
2511//                     subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
2512//                 }
2513//             })
2514//         });
2515
2516//         toggle_expand_dir(&panel, "src/test", cx);
2517//         select_path(&panel, "src/test/first.rs", cx);
2518//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2519//         cx.foreground().run_until_parked();
2520//         assert_eq!(
2521//             visible_entries_as_strings(&panel, 0..10, cx),
2522//             &[
2523//                 "v src",
2524//                 "    v test",
2525//                 "          first.rs  <== selected",
2526//                 "          second.rs",
2527//                 "          third.rs"
2528//             ]
2529//         );
2530//         panel.update(cx, |panel, cx| {
2531//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
2532//         });
2533//         assert_eq!(
2534//             new_search_events_count.load(atomic::Ordering::SeqCst),
2535//             0,
2536//             "Should not trigger new search in directory when called on a file"
2537//         );
2538
2539//         select_path(&panel, "src/test", cx);
2540//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2541//         cx.foreground().run_until_parked();
2542//         assert_eq!(
2543//             visible_entries_as_strings(&panel, 0..10, cx),
2544//             &[
2545//                 "v src",
2546//                 "    v test  <== selected",
2547//                 "          first.rs",
2548//                 "          second.rs",
2549//                 "          third.rs"
2550//             ]
2551//         );
2552//         panel.update(cx, |panel, cx| {
2553//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
2554//         });
2555//         assert_eq!(
2556//             new_search_events_count.load(atomic::Ordering::SeqCst),
2557//             1,
2558//             "Should trigger new search in directory when called on a directory"
2559//         );
2560//     }
2561
2562//     #[gpui::test]
2563//     async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2564//         init_test_with_editor(cx);
2565
2566//         let fs = FakeFs::new(cx.background());
2567//         fs.insert_tree(
2568//             "/project_root",
2569//             json!({
2570//                 "dir_1": {
2571//                     "nested_dir": {
2572//                         "file_a.py": "# File contents",
2573//                         "file_b.py": "# File contents",
2574//                         "file_c.py": "# File contents",
2575//                     },
2576//                     "file_1.py": "# File contents",
2577//                     "file_2.py": "# File contents",
2578//                     "file_3.py": "# File contents",
2579//                 },
2580//                 "dir_2": {
2581//                     "file_1.py": "# File contents",
2582//                     "file_2.py": "# File contents",
2583//                     "file_3.py": "# File contents",
2584//                 }
2585//             }),
2586//         )
2587//         .await;
2588
2589//         let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2590//         let workspace = cx
2591//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2592//             .root(cx);
2593//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2594
2595//         panel.update(cx, |panel, cx| {
2596//             panel.collapse_all_entries(&CollapseAllEntries, cx)
2597//         });
2598//         cx.foreground().run_until_parked();
2599//         assert_eq!(
2600//             visible_entries_as_strings(&panel, 0..10, cx),
2601//             &["v project_root", "    > dir_1", "    > dir_2",]
2602//         );
2603
2604//         // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2605//         toggle_expand_dir(&panel, "project_root/dir_1", cx);
2606//         cx.foreground().run_until_parked();
2607//         assert_eq!(
2608//             visible_entries_as_strings(&panel, 0..10, cx),
2609//             &[
2610//                 "v project_root",
2611//                 "    v dir_1  <== selected",
2612//                 "        > nested_dir",
2613//                 "          file_1.py",
2614//                 "          file_2.py",
2615//                 "          file_3.py",
2616//                 "    > dir_2",
2617//             ]
2618//         );
2619//     }
2620
2621//     #[gpui::test]
2622//     async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2623//         init_test(cx);
2624
2625//         let fs = FakeFs::new(cx.background());
2626//         fs.as_fake().insert_tree("/root", json!({})).await;
2627//         let project = Project::test(fs, ["/root".as_ref()], cx).await;
2628//         let workspace = cx
2629//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2630//             .root(cx);
2631//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2632
2633//         // Make a new buffer with no backing file
2634//         workspace.update(cx, |workspace, cx| {
2635//             Editor::new_file(workspace, &Default::default(), cx)
2636//         });
2637
2638//         // "Save as"" the buffer, creating a new backing file for it
2639//         let task = workspace.update(cx, |workspace, cx| {
2640//             workspace.save_active_item(workspace::SaveIntent::Save, cx)
2641//         });
2642
2643//         cx.foreground().run_until_parked();
2644//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
2645//         task.await.unwrap();
2646
2647//         // Rename the file
2648//         select_path(&panel, "root/new", cx);
2649//         assert_eq!(
2650//             visible_entries_as_strings(&panel, 0..10, cx),
2651//             &["v root", "      new  <== selected"]
2652//         );
2653//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
2654//         panel.update(cx, |panel, cx| {
2655//             panel
2656//                 .filename_editor
2657//                 .update(cx, |editor, cx| editor.set_text("newer", cx));
2658//         });
2659//         panel
2660//             .update(cx, |panel, cx| panel.confirm(&Confirm, cx))
2661//             .unwrap()
2662//             .await
2663//             .unwrap();
2664
2665//         cx.foreground().run_until_parked();
2666//         assert_eq!(
2667//             visible_entries_as_strings(&panel, 0..10, cx),
2668//             &["v root", "      newer  <== selected"]
2669//         );
2670
2671//         workspace
2672//             .update(cx, |workspace, cx| {
2673//                 workspace.save_active_item(workspace::SaveIntent::Save, cx)
2674//             })
2675//             .await
2676//             .unwrap();
2677
2678//         cx.foreground().run_until_parked();
2679//         // assert that saving the file doesn't restore "new"
2680//         assert_eq!(
2681//             visible_entries_as_strings(&panel, 0..10, cx),
2682//             &["v root", "      newer  <== selected"]
2683//         );
2684//     }
2685
2686//     fn toggle_expand_dir(
2687//         panel: &View<ProjectPanel>,
2688//         path: impl AsRef<Path>,
2689//         cx: &mut TestAppContext,
2690//     ) {
2691//         let path = path.as_ref();
2692//         panel.update(cx, |panel, cx| {
2693//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
2694//                 let worktree = worktree.read(cx);
2695//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2696//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2697//                     panel.toggle_expanded(entry_id, cx);
2698//                     return;
2699//                 }
2700//             }
2701//             panic!("no worktree for path {:?}", path);
2702//         });
2703//     }
2704
2705//     fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut TestAppContext) {
2706//         let path = path.as_ref();
2707//         panel.update(cx, |panel, cx| {
2708//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
2709//                 let worktree = worktree.read(cx);
2710//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2711//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2712//                     panel.selection = Some(Selection {
2713//                         worktree_id: worktree.id(),
2714//                         entry_id,
2715//                     });
2716//                     return;
2717//                 }
2718//             }
2719//             panic!("no worktree for path {:?}", path);
2720//         });
2721//     }
2722
2723//     fn visible_entries_as_strings(
2724//         panel: &View<ProjectPanel>,
2725//         range: Range<usize>,
2726//         cx: &mut TestAppContext,
2727//     ) -> Vec<String> {
2728//         let mut result = Vec::new();
2729//         let mut project_entries = HashSet::new();
2730//         let mut has_editor = false;
2731
2732//         panel.update(cx, |panel, cx| {
2733//             panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
2734//                 if details.is_editing {
2735//                     assert!(!has_editor, "duplicate editor entry");
2736//                     has_editor = true;
2737//                 } else {
2738//                     assert!(
2739//                         project_entries.insert(project_entry),
2740//                         "duplicate project entry {:?} {:?}",
2741//                         project_entry,
2742//                         details
2743//                     );
2744//                 }
2745
2746//                 let indent = "    ".repeat(details.depth);
2747//                 let icon = if details.kind.is_dir() {
2748//                     if details.is_expanded {
2749//                         "v "
2750//                     } else {
2751//                         "> "
2752//                     }
2753//                 } else {
2754//                     "  "
2755//                 };
2756//                 let name = if details.is_editing {
2757//                     format!("[EDITOR: '{}']", details.filename)
2758//                 } else if details.is_processing {
2759//                     format!("[PROCESSING: '{}']", details.filename)
2760//                 } else {
2761//                     details.filename.clone()
2762//                 };
2763//                 let selected = if details.is_selected {
2764//                     "  <== selected"
2765//                 } else {
2766//                     ""
2767//                 };
2768//                 result.push(format!("{indent}{icon}{name}{selected}"));
2769//             });
2770//         });
2771
2772//         result
2773//     }
2774
2775//     fn init_test(cx: &mut TestAppContext) {
2776//         cx.foreground().forbid_parking();
2777//         cx.update(|cx| {
2778//             cx.set_global(SettingsStore::test(cx));
2779//             init_settings(cx);
2780//             theme::init(cx);
2781//             language::init(cx);
2782//             editor::init_settings(cx);
2783//             crate::init((), cx);
2784//             workspace::init_settings(cx);
2785//             client::init_settings(cx);
2786//             Project::init_settings(cx);
2787//         });
2788//     }
2789
2790//     fn init_test_with_editor(cx: &mut TestAppContext) {
2791//         cx.foreground().forbid_parking();
2792//         cx.update(|cx| {
2793//             let app_state = AppState::test(cx);
2794//             theme::init(cx);
2795//             init_settings(cx);
2796//             language::init(cx);
2797//             editor::init(cx);
2798//             pane::init(cx);
2799//             crate::init((), cx);
2800//             workspace::init(app_state.clone(), cx);
2801//             Project::init_settings(cx);
2802//         });
2803//     }
2804
2805//     fn ensure_single_file_is_opened(
2806//         window: WindowHandle<Workspace>,
2807//         expected_path: &str,
2808//         cx: &mut TestAppContext,
2809//     ) {
2810//         window.update_root(cx, |workspace, cx| {
2811//             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
2812//             assert_eq!(worktrees.len(), 1);
2813//             let worktree_id = WorktreeId::from_usize(worktrees[0].id());
2814
2815//             let open_project_paths = workspace
2816//                 .panes()
2817//                 .iter()
2818//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2819//                 .collect::<Vec<_>>();
2820//             assert_eq!(
2821//                 open_project_paths,
2822//                 vec![ProjectPath {
2823//                     worktree_id,
2824//                     path: Arc::from(Path::new(expected_path))
2825//                 }],
2826//                 "Should have opened file, selected in project panel"
2827//             );
2828//         });
2829//     }
2830
2831//     fn submit_deletion(
2832//         window: AnyWindowHandle,
2833//         panel: &View<ProjectPanel>,
2834//         cx: &mut TestAppContext,
2835//     ) {
2836//         assert!(
2837//             !window.has_pending_prompt(cx),
2838//             "Should have no prompts before the deletion"
2839//         );
2840//         panel.update(cx, |panel, cx| {
2841//             panel
2842//                 .delete(&Delete, cx)
2843//                 .expect("Deletion start")
2844//                 .detach_and_log_err(cx);
2845//         });
2846//         assert!(
2847//             window.has_pending_prompt(cx),
2848//             "Should have a prompt after the deletion"
2849//         );
2850//         window.simulate_prompt_answer(0, cx);
2851//         assert!(
2852//             !window.has_pending_prompt(cx),
2853//             "Should have no prompts after prompt was replied to"
2854//         );
2855//         cx.foreground().run_until_parked();
2856//     }
2857
2858//     fn ensure_no_open_items_and_panes(
2859//         window: AnyWindowHandle,
2860//         workspace: &View<Workspace>,
2861//         cx: &mut TestAppContext,
2862//     ) {
2863//         assert!(
2864//             !window.has_pending_prompt(cx),
2865//             "Should have no prompts after deletion operation closes the file"
2866//         );
2867//         window.read_with(cx, |cx| {
2868//             let open_project_paths = workspace
2869//                 .read(cx)
2870//                 .panes()
2871//                 .iter()
2872//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2873//                 .collect::<Vec<_>>();
2874//             assert!(
2875//                 open_project_paths.is_empty(),
2876//                 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
2877//             );
2878//         });
2879//     }
2880// }