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, EventEmitter, FocusHandle, FocusableKeyDispatch, Model,
  13    MouseButton, 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 mut this = Self {
 223                project: project.clone(),
 224                fs: workspace.app_state().fs.clone(),
 225                list: UniformListScrollHandle::new(),
 226                focus_handle,
 227                visible_entries: Default::default(),
 228                last_worktree_root_id: Default::default(),
 229                expanded_dir_ids: Default::default(),
 230                selection: None,
 231                edit_state: None,
 232                filename_editor,
 233                clipboard_entry: None,
 234                // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 235                _dragged_entry_destination: None,
 236                _workspace: workspace.weak_handle(),
 237                has_focus: false,
 238                width: None,
 239                pending_serialization: Task::ready(None),
 240            };
 241            this.update_visible_entries(None, cx);
 242
 243            // Update the dock position when the setting changes.
 244            // todo!()
 245            // let mut old_dock_position = this.position(cx);
 246            // cx.observe_global::<SettingsStore, _>(move |this, cx| {
 247            //     let new_dock_position = this.position(cx);
 248            //     if new_dock_position != old_dock_position {
 249            //         old_dock_position = new_dock_position;
 250            //         cx.emit(Event::DockPositionChanged);
 251            //     }
 252            // })
 253            // .detach();
 254
 255            this
 256        });
 257
 258        cx.subscribe(&project_panel, {
 259            let project_panel = project_panel.downgrade();
 260            move |workspace, _, event, cx| match event {
 261                &Event::OpenedEntry {
 262                    entry_id,
 263                    focus_opened_item,
 264                } => {
 265                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 266                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 267                            workspace
 268                                .open_path(
 269                                    ProjectPath {
 270                                        worktree_id: worktree.read(cx).id(),
 271                                        path: entry.path.clone(),
 272                                    },
 273                                    None,
 274                                    focus_opened_item,
 275                                    cx,
 276                                )
 277                                .detach_and_log_err(cx);
 278                            if !focus_opened_item {
 279                                if let Some(project_panel) = project_panel.upgrade() {
 280                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
 281                                    cx.focus(&focus_handle);
 282                                }
 283                            }
 284                        }
 285                    }
 286                }
 287                &Event::SplitEntry { entry_id } => {
 288                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 289                        if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) {
 290                            // workspace
 291                            //     .split_path(
 292                            //         ProjectPath {
 293                            //             worktree_id: worktree.read(cx).id(),
 294                            //             path: entry.path.clone(),
 295                            //         },
 296                            //         cx,
 297                            //     )
 298                            //     .detach_and_log_err(cx);
 299                        }
 300                    }
 301                }
 302                _ => {}
 303            }
 304        })
 305        .detach();
 306
 307        project_panel
 308    }
 309
 310    pub fn load(
 311        workspace: WeakView<Workspace>,
 312        cx: AsyncWindowContext,
 313    ) -> Task<Result<View<Self>>> {
 314        cx.spawn(|mut cx| async move {
 315            // let serialized_panel = if let Some(panel) = cx
 316            //     .background_executor()
 317            //     .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
 318            //     .await
 319            //     .log_err()
 320            //     .flatten()
 321            // {
 322            //     Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
 323            // } else {
 324            //     None
 325            // };
 326            workspace.update(&mut cx, |workspace, cx| {
 327                let panel = ProjectPanel::new(workspace, cx);
 328                // if let Some(serialized_panel) = serialized_panel {
 329                //     panel.update(cx, |panel, cx| {
 330                //         panel.width = serialized_panel.width;
 331                //         cx.notify();
 332                //     });
 333                // }
 334                panel
 335            })
 336        })
 337    }
 338
 339    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 340        let width = self.width;
 341        self.pending_serialization = cx.background_executor().spawn(
 342            async move {
 343                KEY_VALUE_STORE
 344                    .write_kvp(
 345                        PROJECT_PANEL_KEY.into(),
 346                        serde_json::to_string(&SerializedProjectPanel { width })?,
 347                    )
 348                    .await?;
 349                anyhow::Ok(())
 350            }
 351            .log_err(),
 352        );
 353    }
 354
 355    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 356        if !self.has_focus {
 357            self.has_focus = true;
 358            cx.emit(Event::Focus);
 359        }
 360    }
 361
 362    fn focus_out(&mut self, _: &mut ViewContext<Self>) {
 363        self.has_focus = false;
 364    }
 365
 366    fn deploy_context_menu(
 367        &mut self,
 368        _position: Point<Pixels>,
 369        _entry_id: ProjectEntryId,
 370        _cx: &mut ViewContext<Self>,
 371    ) {
 372        todo!()
 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    // todo!()
 994    // fn move_entry(
 995    //     &mut self,
 996    //     entry_to_move: ProjectEntryId,
 997    //     destination: ProjectEntryId,
 998    //     destination_is_file: bool,
 999    //     cx: &mut ViewContext<Self>,
1000    // ) {
1001    //     let destination_worktree = self.project.update(cx, |project, cx| {
1002    //         let entry_path = project.path_for_entry(entry_to_move, cx)?;
1003    //         let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
1004
1005    //         let mut destination_path = destination_entry_path.as_ref();
1006    //         if destination_is_file {
1007    //             destination_path = destination_path.parent()?;
1008    //         }
1009
1010    //         let mut new_path = destination_path.to_path_buf();
1011    //         new_path.push(entry_path.path.file_name()?);
1012    //         if new_path != entry_path.path.as_ref() {
1013    //             let task = project.rename_entry(entry_to_move, new_path, cx)?;
1014    //             cx.foreground_executor().spawn(task).detach_and_log_err(cx);
1015    //         }
1016
1017    //         Some(project.worktree_id_for_entry(destination, cx)?)
1018    //     });
1019
1020    //     if let Some(destination_worktree) = destination_worktree {
1021    //         self.expand_entry(destination_worktree, destination, cx);
1022    //     }
1023    // }
1024
1025    fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
1026        let mut entry_index = 0;
1027        let mut visible_entries_index = 0;
1028        for (worktree_index, (worktree_id, worktree_entries)) in
1029            self.visible_entries.iter().enumerate()
1030        {
1031            if *worktree_id == selection.worktree_id {
1032                for entry in worktree_entries {
1033                    if entry.id == selection.entry_id {
1034                        return Some((worktree_index, entry_index, visible_entries_index));
1035                    } else {
1036                        visible_entries_index += 1;
1037                        entry_index += 1;
1038                    }
1039                }
1040                break;
1041            } else {
1042                visible_entries_index += worktree_entries.len();
1043            }
1044        }
1045        None
1046    }
1047
1048    pub fn selected_entry<'a>(
1049        &self,
1050        cx: &'a AppContext,
1051    ) -> Option<(&'a Worktree, &'a project::Entry)> {
1052        let (worktree, entry) = self.selected_entry_handle(cx)?;
1053        Some((worktree.read(cx), entry))
1054    }
1055
1056    fn selected_entry_handle<'a>(
1057        &self,
1058        cx: &'a AppContext,
1059    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
1060        let selection = self.selection?;
1061        let project = self.project.read(cx);
1062        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
1063        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
1064        Some((worktree, entry))
1065    }
1066
1067    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
1068        let (worktree, entry) = self.selected_entry(cx)?;
1069        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
1070
1071        for path in entry.path.ancestors() {
1072            let Some(entry) = worktree.entry_for_path(path) else {
1073                continue;
1074            };
1075            if entry.is_dir() {
1076                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
1077                    expanded_dir_ids.insert(idx, entry.id);
1078                }
1079            }
1080        }
1081
1082        Some(())
1083    }
1084
1085    fn update_visible_entries(
1086        &mut self,
1087        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
1088        cx: &mut ViewContext<Self>,
1089    ) {
1090        let project = self.project.read(cx);
1091        self.last_worktree_root_id = project
1092            .visible_worktrees(cx)
1093            .rev()
1094            .next()
1095            .and_then(|worktree| worktree.read(cx).root_entry())
1096            .map(|entry| entry.id);
1097
1098        self.visible_entries.clear();
1099        for worktree in project.visible_worktrees(cx) {
1100            let snapshot = worktree.read(cx).snapshot();
1101            let worktree_id = snapshot.id();
1102
1103            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
1104                hash_map::Entry::Occupied(e) => e.into_mut(),
1105                hash_map::Entry::Vacant(e) => {
1106                    // The first time a worktree's root entry becomes available,
1107                    // mark that root entry as expanded.
1108                    if let Some(entry) = snapshot.root_entry() {
1109                        e.insert(vec![entry.id]).as_slice()
1110                    } else {
1111                        &[]
1112                    }
1113                }
1114            };
1115
1116            let mut new_entry_parent_id = None;
1117            let mut new_entry_kind = EntryKind::Dir;
1118            if let Some(edit_state) = &self.edit_state {
1119                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
1120                    new_entry_parent_id = Some(edit_state.entry_id);
1121                    new_entry_kind = if edit_state.is_dir {
1122                        EntryKind::Dir
1123                    } else {
1124                        EntryKind::File(Default::default())
1125                    };
1126                }
1127            }
1128
1129            let mut visible_worktree_entries = Vec::new();
1130            let mut entry_iter = snapshot.entries(true);
1131
1132            while let Some(entry) = entry_iter.entry() {
1133                visible_worktree_entries.push(entry.clone());
1134                if Some(entry.id) == new_entry_parent_id {
1135                    visible_worktree_entries.push(Entry {
1136                        id: NEW_ENTRY_ID,
1137                        kind: new_entry_kind,
1138                        path: entry.path.join("\0").into(),
1139                        inode: 0,
1140                        mtime: entry.mtime,
1141                        is_symlink: false,
1142                        is_ignored: false,
1143                        is_external: false,
1144                        git_status: entry.git_status,
1145                    });
1146                }
1147                if expanded_dir_ids.binary_search(&entry.id).is_err()
1148                    && entry_iter.advance_to_sibling()
1149                {
1150                    continue;
1151                }
1152                entry_iter.advance();
1153            }
1154
1155            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
1156
1157            visible_worktree_entries.sort_by(|entry_a, entry_b| {
1158                let mut components_a = entry_a.path.components().peekable();
1159                let mut components_b = entry_b.path.components().peekable();
1160                loop {
1161                    match (components_a.next(), components_b.next()) {
1162                        (Some(component_a), Some(component_b)) => {
1163                            let a_is_file = components_a.peek().is_none() && entry_a.is_file();
1164                            let b_is_file = components_b.peek().is_none() && entry_b.is_file();
1165                            let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1166                                let name_a =
1167                                    UniCase::new(component_a.as_os_str().to_string_lossy());
1168                                let name_b =
1169                                    UniCase::new(component_b.as_os_str().to_string_lossy());
1170                                name_a.cmp(&name_b)
1171                            });
1172                            if !ordering.is_eq() {
1173                                return ordering;
1174                            }
1175                        }
1176                        (Some(_), None) => break Ordering::Greater,
1177                        (None, Some(_)) => break Ordering::Less,
1178                        (None, None) => break Ordering::Equal,
1179                    }
1180                }
1181            });
1182            self.visible_entries
1183                .push((worktree_id, visible_worktree_entries));
1184        }
1185
1186        if let Some((worktree_id, entry_id)) = new_selected_entry {
1187            self.selection = Some(Selection {
1188                worktree_id,
1189                entry_id,
1190            });
1191        }
1192    }
1193
1194    fn expand_entry(
1195        &mut self,
1196        worktree_id: WorktreeId,
1197        entry_id: ProjectEntryId,
1198        cx: &mut ViewContext<Self>,
1199    ) {
1200        self.project.update(cx, |project, cx| {
1201            if let Some((worktree, expanded_dir_ids)) = project
1202                .worktree_for_id(worktree_id, cx)
1203                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1204            {
1205                project.expand_entry(worktree_id, entry_id, cx);
1206                let worktree = worktree.read(cx);
1207
1208                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1209                    loop {
1210                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1211                            expanded_dir_ids.insert(ix, entry.id);
1212                        }
1213
1214                        if let Some(parent_entry) =
1215                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1216                        {
1217                            entry = parent_entry;
1218                        } else {
1219                            break;
1220                        }
1221                    }
1222                }
1223            }
1224        });
1225    }
1226
1227    fn for_each_visible_entry(
1228        &self,
1229        range: Range<usize>,
1230        cx: &mut ViewContext<ProjectPanel>,
1231        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
1232    ) {
1233        let mut ix = 0;
1234        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
1235            if ix >= range.end {
1236                return;
1237            }
1238
1239            if ix + visible_worktree_entries.len() <= range.start {
1240                ix += visible_worktree_entries.len();
1241                continue;
1242            }
1243
1244            let end_ix = range.end.min(ix + visible_worktree_entries.len());
1245            let (git_status_setting, show_file_icons, show_folder_icons) = {
1246                let settings = ProjectPanelSettings::get_global(cx);
1247                (
1248                    settings.git_status,
1249                    settings.file_icons,
1250                    settings.folder_icons,
1251                )
1252            };
1253            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1254                let snapshot = worktree.read(cx).snapshot();
1255                let root_name = OsStr::new(snapshot.root_name());
1256                let expanded_entry_ids = self
1257                    .expanded_dir_ids
1258                    .get(&snapshot.id())
1259                    .map(Vec::as_slice)
1260                    .unwrap_or(&[]);
1261
1262                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
1263                for entry in visible_worktree_entries[entry_range].iter() {
1264                    let status = git_status_setting.then(|| entry.git_status).flatten();
1265                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
1266                    let icon = match entry.kind {
1267                        EntryKind::File(_) => {
1268                            if show_file_icons {
1269                                Some(FileAssociations::get_icon(&entry.path, cx))
1270                            } else {
1271                                None
1272                            }
1273                        }
1274                        _ => {
1275                            if show_folder_icons {
1276                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
1277                            } else {
1278                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
1279                            }
1280                        }
1281                    };
1282
1283                    let mut details = EntryDetails {
1284                        filename: entry
1285                            .path
1286                            .file_name()
1287                            .unwrap_or(root_name)
1288                            .to_string_lossy()
1289                            .to_string(),
1290                        icon,
1291                        path: entry.path.clone(),
1292                        depth: entry.path.components().count(),
1293                        kind: entry.kind,
1294                        is_ignored: entry.is_ignored,
1295                        is_expanded,
1296                        is_selected: self.selection.map_or(false, |e| {
1297                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
1298                        }),
1299                        is_editing: false,
1300                        is_processing: false,
1301                        is_cut: self
1302                            .clipboard_entry
1303                            .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
1304                        git_status: status,
1305                    };
1306
1307                    if let Some(edit_state) = &self.edit_state {
1308                        let is_edited_entry = if edit_state.is_new_entry {
1309                            entry.id == NEW_ENTRY_ID
1310                        } else {
1311                            entry.id == edit_state.entry_id
1312                        };
1313
1314                        if is_edited_entry {
1315                            if let Some(processing_filename) = &edit_state.processing_filename {
1316                                details.is_processing = true;
1317                                details.filename.clear();
1318                                details.filename.push_str(processing_filename);
1319                            } else {
1320                                if edit_state.is_new_entry {
1321                                    details.filename.clear();
1322                                }
1323                                details.is_editing = true;
1324                            }
1325                        }
1326                    }
1327
1328                    callback(entry.id, details, cx);
1329                }
1330            }
1331            ix = end_ix;
1332        }
1333    }
1334
1335    fn render_entry_visual_element(
1336        details: &EntryDetails,
1337        editor: Option<&View<Editor>>,
1338        padding: Pixels,
1339        cx: &mut ViewContext<Self>,
1340    ) -> Div<Self> {
1341        let show_editor = details.is_editing && !details.is_processing;
1342
1343        let theme = cx.theme();
1344        let filename_text_color = details
1345            .git_status
1346            .as_ref()
1347            .map(|status| match status {
1348                GitFileStatus::Added => theme.status().created,
1349                GitFileStatus::Modified => theme.status().modified,
1350                GitFileStatus::Conflict => theme.status().conflict,
1351            })
1352            .unwrap_or(theme.status().info);
1353
1354        h_stack()
1355            .child(if let Some(icon) = &details.icon {
1356                div().child(
1357                    // todo!() Marshall: Can we use our `IconElement` component here?
1358                    svg()
1359                        .size(rems(0.9375))
1360                        .flex_none()
1361                        .path(icon.to_string())
1362                        .text_color(cx.theme().colors().icon),
1363                )
1364            } else {
1365                div()
1366            })
1367            .child(
1368                if let (Some(editor), true) = (editor, show_editor) {
1369                    div().w_full().child(editor.clone())
1370                } else {
1371                    div()
1372                        .text_color(filename_text_color)
1373                        .child(Label::new(details.filename.clone()))
1374                }
1375                .ml_1(),
1376            )
1377            .pl(padding)
1378    }
1379
1380    fn render_entry(
1381        &self,
1382        entry_id: ProjectEntryId,
1383        details: EntryDetails,
1384        // dragged_entry_destination: &mut Option<Arc<Path>>,
1385        cx: &mut ViewContext<Self>,
1386    ) -> Div<Self, StatefulInteractivity<Self>> {
1387        let kind = details.kind;
1388        let settings = ProjectPanelSettings::get_global(cx);
1389        const INDENT_SIZE: Pixels = px(16.0);
1390        let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
1391        let show_editor = details.is_editing && !details.is_processing;
1392        let is_selected = self
1393            .selection
1394            .map_or(false, |selection| selection.entry_id == entry_id);
1395
1396        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
1397            .id(entry_id.to_proto() as usize)
1398            .w_full()
1399            .cursor_pointer()
1400            .when(is_selected, |this| {
1401                this.bg(cx.theme().colors().element_selected)
1402            })
1403            .hover(|style| style.bg(cx.theme().colors().element_hover))
1404            .on_click(move |this, event, cx| {
1405                if !show_editor {
1406                    if kind.is_dir() {
1407                        this.toggle_expanded(entry_id, cx);
1408                    } else {
1409                        if event.down.modifiers.command {
1410                            this.split_entry(entry_id, cx);
1411                        } else {
1412                            this.open_entry(entry_id, event.up.click_count > 1, cx);
1413                        }
1414                    }
1415                }
1416            })
1417            .on_mouse_down(MouseButton::Right, move |this, event, cx| {
1418                this.deploy_context_menu(event.position, entry_id, cx);
1419            })
1420        // .on_drop::<ProjectEntryId>(|this, event, cx| {
1421        //     this.move_entry(
1422        //         *dragged_entry,
1423        //         entry_id,
1424        //         matches!(details.kind, EntryKind::File(_)),
1425        //         cx,
1426        //     );
1427        // })
1428    }
1429}
1430
1431impl Render for ProjectPanel {
1432    type Element = Div<Self, StatefulInteractivity<Self>, FocusableKeyDispatch<Self>>;
1433
1434    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
1435        let has_worktree = self.visible_entries.len() != 0;
1436
1437        if has_worktree {
1438            div()
1439                .id("project-panel")
1440                .size_full()
1441                .context("ProjectPanel")
1442                .on_action(Self::select_next)
1443                .on_action(Self::select_prev)
1444                .on_action(Self::expand_selected_entry)
1445                .on_action(Self::collapse_selected_entry)
1446                .on_action(Self::collapse_all_entries)
1447                .on_action(Self::new_file)
1448                .on_action(Self::new_directory)
1449                .on_action(Self::rename)
1450                .on_action(Self::delete)
1451                .on_action(Self::confirm)
1452                .on_action(Self::open_file)
1453                .on_action(Self::cancel)
1454                .on_action(Self::cut)
1455                .on_action(Self::copy)
1456                .on_action(Self::copy_path)
1457                .on_action(Self::copy_relative_path)
1458                .on_action(Self::paste)
1459                .on_action(Self::reveal_in_finder)
1460                .on_action(Self::open_in_terminal)
1461                .on_action(Self::new_search_in_directory)
1462                .track_focus(&self.focus_handle)
1463                .child(
1464                    uniform_list(
1465                        "entries",
1466                        self.visible_entries
1467                            .iter()
1468                            .map(|(_, worktree_entries)| worktree_entries.len())
1469                            .sum(),
1470                        |this: &mut Self, range, cx| {
1471                            let mut items = SmallVec::new();
1472                            this.for_each_visible_entry(range, cx, |id, details, cx| {
1473                                items.push(this.render_entry(id, details, cx));
1474                            });
1475                            items
1476                        },
1477                    )
1478                    .size_full()
1479                    .track_scroll(self.list.clone()),
1480                )
1481        } else {
1482            v_stack()
1483                .id("empty-project_panel")
1484                .track_focus(&self.focus_handle)
1485        }
1486    }
1487}
1488
1489impl EventEmitter<Event> for ProjectPanel {}
1490
1491impl EventEmitter<PanelEvent> for ProjectPanel {}
1492
1493impl workspace::dock::Panel for ProjectPanel {
1494    fn position(&self, cx: &WindowContext) -> DockPosition {
1495        match ProjectPanelSettings::get_global(cx).dock {
1496            ProjectPanelDockPosition::Left => DockPosition::Left,
1497            ProjectPanelDockPosition::Right => DockPosition::Right,
1498        }
1499    }
1500
1501    fn position_is_valid(&self, position: DockPosition) -> bool {
1502        matches!(position, DockPosition::Left | DockPosition::Right)
1503    }
1504
1505    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1506        settings::update_settings_file::<ProjectPanelSettings>(
1507            self.fs.clone(),
1508            cx,
1509            move |settings| {
1510                let dock = match position {
1511                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
1512                    DockPosition::Right => ProjectPanelDockPosition::Right,
1513                };
1514                settings.dock = Some(dock);
1515            },
1516        );
1517    }
1518
1519    fn size(&self, cx: &WindowContext) -> f32 {
1520        self.width
1521            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
1522    }
1523
1524    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
1525        self.width = size;
1526        self.serialize(cx);
1527        cx.notify();
1528    }
1529
1530    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
1531        Some("icons/project.svg")
1532    }
1533
1534    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
1535        ("Project Panel".into(), Some(Box::new(ToggleFocus)))
1536    }
1537
1538    // fn should_change_position_on_event(event: &Self::Event) -> bool {
1539    //     matches!(event, Event::DockPositionChanged)
1540    // }
1541
1542    fn has_focus(&self, _: &WindowContext) -> bool {
1543        self.has_focus
1544    }
1545
1546    fn persistent_name(&self) -> &'static str {
1547        "Project Panel"
1548    }
1549
1550    fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
1551        self.focus_handle.clone()
1552    }
1553
1554    // fn is_focus_event(event: &Self::Event) -> bool {
1555    //     matches!(event, Event::Focus)
1556    // }
1557}
1558
1559impl ClipboardEntry {
1560    fn is_cut(&self) -> bool {
1561        matches!(self, Self::Cut { .. })
1562    }
1563
1564    fn entry_id(&self) -> ProjectEntryId {
1565        match self {
1566            ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
1567                *entry_id
1568            }
1569        }
1570    }
1571
1572    fn worktree_id(&self) -> WorktreeId {
1573        match self {
1574            ClipboardEntry::Copied { worktree_id, .. }
1575            | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
1576        }
1577    }
1578}
1579
1580// todo!()
1581// #[cfg(test)]
1582// mod tests {
1583//     use super::*;
1584//     use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle};
1585//     use pretty_assertions::assert_eq;
1586//     use project::FakeFs;
1587//     use serde_json::json;
1588//     use settings::SettingsStore;
1589//     use std::{
1590//         collections::HashSet,
1591//         path::{Path, PathBuf},
1592//         sync::atomic::{self, AtomicUsize},
1593//     };
1594//     use workspace::{pane, AppState};
1595
1596//     #[gpui::test]
1597//     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
1598//         init_test(cx);
1599
1600//         let fs = FakeFs::new(cx.executor().clone());
1601//         fs.insert_tree(
1602//             "/root1",
1603//             json!({
1604//                 ".dockerignore": "",
1605//                 ".git": {
1606//                     "HEAD": "",
1607//                 },
1608//                 "a": {
1609//                     "0": { "q": "", "r": "", "s": "" },
1610//                     "1": { "t": "", "u": "" },
1611//                     "2": { "v": "", "w": "", "x": "", "y": "" },
1612//                 },
1613//                 "b": {
1614//                     "3": { "Q": "" },
1615//                     "4": { "R": "", "S": "", "T": "", "U": "" },
1616//                 },
1617//                 "C": {
1618//                     "5": {},
1619//                     "6": { "V": "", "W": "" },
1620//                     "7": { "X": "" },
1621//                     "8": { "Y": {}, "Z": "" }
1622//                 }
1623//             }),
1624//         )
1625//         .await;
1626//         fs.insert_tree(
1627//             "/root2",
1628//             json!({
1629//                 "d": {
1630//                     "9": ""
1631//                 },
1632//                 "e": {}
1633//             }),
1634//         )
1635//         .await;
1636
1637//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1638//         let workspace = cx
1639//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
1640//             .root(cx);
1641//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1642//         assert_eq!(
1643//             visible_entries_as_strings(&panel, 0..50, cx),
1644//             &[
1645//                 "v root1",
1646//                 "    > .git",
1647//                 "    > a",
1648//                 "    > b",
1649//                 "    > C",
1650//                 "      .dockerignore",
1651//                 "v root2",
1652//                 "    > d",
1653//                 "    > e",
1654//             ]
1655//         );
1656
1657//         toggle_expand_dir(&panel, "root1/b", cx);
1658//         assert_eq!(
1659//             visible_entries_as_strings(&panel, 0..50, cx),
1660//             &[
1661//                 "v root1",
1662//                 "    > .git",
1663//                 "    > a",
1664//                 "    v b  <== selected",
1665//                 "        > 3",
1666//                 "        > 4",
1667//                 "    > C",
1668//                 "      .dockerignore",
1669//                 "v root2",
1670//                 "    > d",
1671//                 "    > e",
1672//             ]
1673//         );
1674
1675//         assert_eq!(
1676//             visible_entries_as_strings(&panel, 6..9, cx),
1677//             &[
1678//                 //
1679//                 "    > C",
1680//                 "      .dockerignore",
1681//                 "v root2",
1682//             ]
1683//         );
1684//     }
1685
1686//     #[gpui::test(iterations = 30)]
1687//     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
1688//         init_test(cx);
1689
1690//         let fs = FakeFs::new(cx.background());
1691//         fs.insert_tree(
1692//             "/root1",
1693//             json!({
1694//                 ".dockerignore": "",
1695//                 ".git": {
1696//                     "HEAD": "",
1697//                 },
1698//                 "a": {
1699//                     "0": { "q": "", "r": "", "s": "" },
1700//                     "1": { "t": "", "u": "" },
1701//                     "2": { "v": "", "w": "", "x": "", "y": "" },
1702//                 },
1703//                 "b": {
1704//                     "3": { "Q": "" },
1705//                     "4": { "R": "", "S": "", "T": "", "U": "" },
1706//                 },
1707//                 "C": {
1708//                     "5": {},
1709//                     "6": { "V": "", "W": "" },
1710//                     "7": { "X": "" },
1711//                     "8": { "Y": {}, "Z": "" }
1712//                 }
1713//             }),
1714//         )
1715//         .await;
1716//         fs.insert_tree(
1717//             "/root2",
1718//             json!({
1719//                 "d": {
1720//                     "9": ""
1721//                 },
1722//                 "e": {}
1723//             }),
1724//         )
1725//         .await;
1726
1727//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1728//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1729//         let workspace = window.root(cx);
1730//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1731
1732//         select_path(&panel, "root1", cx);
1733//         assert_eq!(
1734//             visible_entries_as_strings(&panel, 0..10, cx),
1735//             &[
1736//                 "v root1  <== selected",
1737//                 "    > .git",
1738//                 "    > a",
1739//                 "    > b",
1740//                 "    > C",
1741//                 "      .dockerignore",
1742//                 "v root2",
1743//                 "    > d",
1744//                 "    > e",
1745//             ]
1746//         );
1747
1748//         // Add a file with the root folder selected. The filename editor is placed
1749//         // before the first file in the root folder.
1750//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1751//         window.read_with(cx, |cx| {
1752//             let panel = panel.read(cx);
1753//             assert!(panel.filename_editor.is_focused(cx));
1754//         });
1755//         assert_eq!(
1756//             visible_entries_as_strings(&panel, 0..10, cx),
1757//             &[
1758//                 "v root1",
1759//                 "    > .git",
1760//                 "    > a",
1761//                 "    > b",
1762//                 "    > C",
1763//                 "      [EDITOR: '']  <== selected",
1764//                 "      .dockerignore",
1765//                 "v root2",
1766//                 "    > d",
1767//                 "    > e",
1768//             ]
1769//         );
1770
1771//         let confirm = panel.update(cx, |panel, cx| {
1772//             panel
1773//                 .filename_editor
1774//                 .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
1775//             panel.confirm(&Confirm, cx).unwrap()
1776//         });
1777//         assert_eq!(
1778//             visible_entries_as_strings(&panel, 0..10, cx),
1779//             &[
1780//                 "v root1",
1781//                 "    > .git",
1782//                 "    > a",
1783//                 "    > b",
1784//                 "    > C",
1785//                 "      [PROCESSING: 'the-new-filename']  <== selected",
1786//                 "      .dockerignore",
1787//                 "v root2",
1788//                 "    > d",
1789//                 "    > e",
1790//             ]
1791//         );
1792
1793//         confirm.await.unwrap();
1794//         assert_eq!(
1795//             visible_entries_as_strings(&panel, 0..10, cx),
1796//             &[
1797//                 "v root1",
1798//                 "    > .git",
1799//                 "    > a",
1800//                 "    > b",
1801//                 "    > C",
1802//                 "      .dockerignore",
1803//                 "      the-new-filename  <== selected",
1804//                 "v root2",
1805//                 "    > d",
1806//                 "    > e",
1807//             ]
1808//         );
1809
1810//         select_path(&panel, "root1/b", cx);
1811//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1812//         assert_eq!(
1813//             visible_entries_as_strings(&panel, 0..10, cx),
1814//             &[
1815//                 "v root1",
1816//                 "    > .git",
1817//                 "    > a",
1818//                 "    v b",
1819//                 "        > 3",
1820//                 "        > 4",
1821//                 "          [EDITOR: '']  <== selected",
1822//                 "    > C",
1823//                 "      .dockerignore",
1824//                 "      the-new-filename",
1825//             ]
1826//         );
1827
1828//         panel
1829//             .update(cx, |panel, cx| {
1830//                 panel
1831//                     .filename_editor
1832//                     .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
1833//                 panel.confirm(&Confirm, cx).unwrap()
1834//             })
1835//             .await
1836//             .unwrap();
1837//         assert_eq!(
1838//             visible_entries_as_strings(&panel, 0..10, cx),
1839//             &[
1840//                 "v root1",
1841//                 "    > .git",
1842//                 "    > a",
1843//                 "    v b",
1844//                 "        > 3",
1845//                 "        > 4",
1846//                 "          another-filename.txt  <== selected",
1847//                 "    > C",
1848//                 "      .dockerignore",
1849//                 "      the-new-filename",
1850//             ]
1851//         );
1852
1853//         select_path(&panel, "root1/b/another-filename.txt", cx);
1854//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
1855//         assert_eq!(
1856//             visible_entries_as_strings(&panel, 0..10, cx),
1857//             &[
1858//                 "v root1",
1859//                 "    > .git",
1860//                 "    > a",
1861//                 "    v b",
1862//                 "        > 3",
1863//                 "        > 4",
1864//                 "          [EDITOR: 'another-filename.txt']  <== selected",
1865//                 "    > C",
1866//                 "      .dockerignore",
1867//                 "      the-new-filename",
1868//             ]
1869//         );
1870
1871//         let confirm = panel.update(cx, |panel, cx| {
1872//             panel.filename_editor.update(cx, |editor, cx| {
1873//                 let file_name_selections = editor.selections.all::<usize>(cx);
1874//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
1875//                 let file_name_selection = &file_name_selections[0];
1876//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
1877//                 assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
1878
1879//                 editor.set_text("a-different-filename.tar.gz", cx)
1880//             });
1881//             panel.confirm(&Confirm, cx).unwrap()
1882//         });
1883//         assert_eq!(
1884//             visible_entries_as_strings(&panel, 0..10, cx),
1885//             &[
1886//                 "v root1",
1887//                 "    > .git",
1888//                 "    > a",
1889//                 "    v b",
1890//                 "        > 3",
1891//                 "        > 4",
1892//                 "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
1893//                 "    > C",
1894//                 "      .dockerignore",
1895//                 "      the-new-filename",
1896//             ]
1897//         );
1898
1899//         confirm.await.unwrap();
1900//         assert_eq!(
1901//             visible_entries_as_strings(&panel, 0..10, cx),
1902//             &[
1903//                 "v root1",
1904//                 "    > .git",
1905//                 "    > a",
1906//                 "    v b",
1907//                 "        > 3",
1908//                 "        > 4",
1909//                 "          a-different-filename.tar.gz  <== selected",
1910//                 "    > C",
1911//                 "      .dockerignore",
1912//                 "      the-new-filename",
1913//             ]
1914//         );
1915
1916//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
1917//         assert_eq!(
1918//             visible_entries_as_strings(&panel, 0..10, cx),
1919//             &[
1920//                 "v root1",
1921//                 "    > .git",
1922//                 "    > a",
1923//                 "    v b",
1924//                 "        > 3",
1925//                 "        > 4",
1926//                 "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
1927//                 "    > C",
1928//                 "      .dockerignore",
1929//                 "      the-new-filename",
1930//             ]
1931//         );
1932
1933//         panel.update(cx, |panel, cx| {
1934//             panel.filename_editor.update(cx, |editor, cx| {
1935//                 let file_name_selections = editor.selections.all::<usize>(cx);
1936//                 assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
1937//                 let file_name_selection = &file_name_selections[0];
1938//                 assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
1939//                 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");
1940
1941//             });
1942//             panel.cancel(&Cancel, cx)
1943//         });
1944
1945//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
1946//         assert_eq!(
1947//             visible_entries_as_strings(&panel, 0..10, cx),
1948//             &[
1949//                 "v root1",
1950//                 "    > .git",
1951//                 "    > a",
1952//                 "    v b",
1953//                 "        > [EDITOR: '']  <== selected",
1954//                 "        > 3",
1955//                 "        > 4",
1956//                 "          a-different-filename.tar.gz",
1957//                 "    > C",
1958//                 "      .dockerignore",
1959//             ]
1960//         );
1961
1962//         let confirm = panel.update(cx, |panel, cx| {
1963//             panel
1964//                 .filename_editor
1965//                 .update(cx, |editor, cx| editor.set_text("new-dir", cx));
1966//             panel.confirm(&Confirm, cx).unwrap()
1967//         });
1968//         panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
1969//         assert_eq!(
1970//             visible_entries_as_strings(&panel, 0..10, cx),
1971//             &[
1972//                 "v root1",
1973//                 "    > .git",
1974//                 "    > a",
1975//                 "    v b",
1976//                 "        > [PROCESSING: 'new-dir']",
1977//                 "        > 3  <== selected",
1978//                 "        > 4",
1979//                 "          a-different-filename.tar.gz",
1980//                 "    > C",
1981//                 "      .dockerignore",
1982//             ]
1983//         );
1984
1985//         confirm.await.unwrap();
1986//         assert_eq!(
1987//             visible_entries_as_strings(&panel, 0..10, cx),
1988//             &[
1989//                 "v root1",
1990//                 "    > .git",
1991//                 "    > a",
1992//                 "    v b",
1993//                 "        > 3  <== selected",
1994//                 "        > 4",
1995//                 "        > new-dir",
1996//                 "          a-different-filename.tar.gz",
1997//                 "    > C",
1998//                 "      .dockerignore",
1999//             ]
2000//         );
2001
2002//         panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
2003//         assert_eq!(
2004//             visible_entries_as_strings(&panel, 0..10, cx),
2005//             &[
2006//                 "v root1",
2007//                 "    > .git",
2008//                 "    > a",
2009//                 "    v b",
2010//                 "        > [EDITOR: '3']  <== selected",
2011//                 "        > 4",
2012//                 "        > new-dir",
2013//                 "          a-different-filename.tar.gz",
2014//                 "    > C",
2015//                 "      .dockerignore",
2016//             ]
2017//         );
2018
2019//         // Dismiss the rename editor when it loses focus.
2020//         workspace.update(cx, |_, cx| cx.focus_self());
2021//         assert_eq!(
2022//             visible_entries_as_strings(&panel, 0..10, cx),
2023//             &[
2024//                 "v root1",
2025//                 "    > .git",
2026//                 "    > a",
2027//                 "    v b",
2028//                 "        > 3  <== selected",
2029//                 "        > 4",
2030//                 "        > new-dir",
2031//                 "          a-different-filename.tar.gz",
2032//                 "    > C",
2033//                 "      .dockerignore",
2034//             ]
2035//         );
2036//     }
2037
2038//     #[gpui::test(iterations = 30)]
2039//     async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
2040//         init_test(cx);
2041
2042//         let fs = FakeFs::new(cx.background());
2043//         fs.insert_tree(
2044//             "/root1",
2045//             json!({
2046//                 ".dockerignore": "",
2047//                 ".git": {
2048//                     "HEAD": "",
2049//                 },
2050//                 "a": {
2051//                     "0": { "q": "", "r": "", "s": "" },
2052//                     "1": { "t": "", "u": "" },
2053//                     "2": { "v": "", "w": "", "x": "", "y": "" },
2054//                 },
2055//                 "b": {
2056//                     "3": { "Q": "" },
2057//                     "4": { "R": "", "S": "", "T": "", "U": "" },
2058//                 },
2059//                 "C": {
2060//                     "5": {},
2061//                     "6": { "V": "", "W": "" },
2062//                     "7": { "X": "" },
2063//                     "8": { "Y": {}, "Z": "" }
2064//                 }
2065//             }),
2066//         )
2067//         .await;
2068//         fs.insert_tree(
2069//             "/root2",
2070//             json!({
2071//                 "d": {
2072//                     "9": ""
2073//                 },
2074//                 "e": {}
2075//             }),
2076//         )
2077//         .await;
2078
2079//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
2080//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2081//         let workspace = window.root(cx);
2082//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2083
2084//         select_path(&panel, "root1", cx);
2085//         assert_eq!(
2086//             visible_entries_as_strings(&panel, 0..10, cx),
2087//             &[
2088//                 "v root1  <== selected",
2089//                 "    > .git",
2090//                 "    > a",
2091//                 "    > b",
2092//                 "    > C",
2093//                 "      .dockerignore",
2094//                 "v root2",
2095//                 "    > d",
2096//                 "    > e",
2097//             ]
2098//         );
2099
2100//         // Add a file with the root folder selected. The filename editor is placed
2101//         // before the first file in the root folder.
2102//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
2103//         window.read_with(cx, |cx| {
2104//             let panel = panel.read(cx);
2105//             assert!(panel.filename_editor.is_focused(cx));
2106//         });
2107//         assert_eq!(
2108//             visible_entries_as_strings(&panel, 0..10, cx),
2109//             &[
2110//                 "v root1",
2111//                 "    > .git",
2112//                 "    > a",
2113//                 "    > b",
2114//                 "    > C",
2115//                 "      [EDITOR: '']  <== selected",
2116//                 "      .dockerignore",
2117//                 "v root2",
2118//                 "    > d",
2119//                 "    > e",
2120//             ]
2121//         );
2122
2123//         let confirm = panel.update(cx, |panel, cx| {
2124//             panel.filename_editor.update(cx, |editor, cx| {
2125//                 editor.set_text("/bdir1/dir2/the-new-filename", cx)
2126//             });
2127//             panel.confirm(&Confirm, cx).unwrap()
2128//         });
2129
2130//         assert_eq!(
2131//             visible_entries_as_strings(&panel, 0..10, cx),
2132//             &[
2133//                 "v root1",
2134//                 "    > .git",
2135//                 "    > a",
2136//                 "    > b",
2137//                 "    > C",
2138//                 "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
2139//                 "      .dockerignore",
2140//                 "v root2",
2141//                 "    > d",
2142//                 "    > e",
2143//             ]
2144//         );
2145
2146//         confirm.await.unwrap();
2147//         assert_eq!(
2148//             visible_entries_as_strings(&panel, 0..13, cx),
2149//             &[
2150//                 "v root1",
2151//                 "    > .git",
2152//                 "    > a",
2153//                 "    > b",
2154//                 "    v bdir1",
2155//                 "        v dir2",
2156//                 "              the-new-filename  <== selected",
2157//                 "    > C",
2158//                 "      .dockerignore",
2159//                 "v root2",
2160//                 "    > d",
2161//                 "    > e",
2162//             ]
2163//         );
2164//     }
2165
2166//     #[gpui::test]
2167//     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
2168//         init_test(cx);
2169
2170//         let fs = FakeFs::new(cx.background());
2171//         fs.insert_tree(
2172//             "/root1",
2173//             json!({
2174//                 "one.two.txt": "",
2175//                 "one.txt": ""
2176//             }),
2177//         )
2178//         .await;
2179
2180//         let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
2181//         let workspace = cx
2182//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2183//             .root(cx);
2184//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2185
2186//         panel.update(cx, |panel, cx| {
2187//             panel.select_next(&Default::default(), cx);
2188//             panel.select_next(&Default::default(), cx);
2189//         });
2190
2191//         assert_eq!(
2192//             visible_entries_as_strings(&panel, 0..50, cx),
2193//             &[
2194//                 //
2195//                 "v root1",
2196//                 "      one.two.txt  <== selected",
2197//                 "      one.txt",
2198//             ]
2199//         );
2200
2201//         // Regression test - file name is created correctly when
2202//         // the copied file's name contains multiple dots.
2203//         panel.update(cx, |panel, cx| {
2204//             panel.copy(&Default::default(), cx);
2205//             panel.paste(&Default::default(), cx);
2206//         });
2207//         cx.foreground().run_until_parked();
2208
2209//         assert_eq!(
2210//             visible_entries_as_strings(&panel, 0..50, cx),
2211//             &[
2212//                 //
2213//                 "v root1",
2214//                 "      one.two copy.txt",
2215//                 "      one.two.txt  <== selected",
2216//                 "      one.txt",
2217//             ]
2218//         );
2219
2220//         panel.update(cx, |panel, cx| {
2221//             panel.paste(&Default::default(), cx);
2222//         });
2223//         cx.foreground().run_until_parked();
2224
2225//         assert_eq!(
2226//             visible_entries_as_strings(&panel, 0..50, cx),
2227//             &[
2228//                 //
2229//                 "v root1",
2230//                 "      one.two copy 1.txt",
2231//                 "      one.two copy.txt",
2232//                 "      one.two.txt  <== selected",
2233//                 "      one.txt",
2234//             ]
2235//         );
2236//     }
2237
2238//     #[gpui::test]
2239//     async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
2240//         init_test_with_editor(cx);
2241
2242//         let fs = FakeFs::new(cx.background());
2243//         fs.insert_tree(
2244//             "/src",
2245//             json!({
2246//                 "test": {
2247//                     "first.rs": "// First Rust file",
2248//                     "second.rs": "// Second Rust file",
2249//                     "third.rs": "// Third Rust file",
2250//                 }
2251//             }),
2252//         )
2253//         .await;
2254
2255//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2256//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2257//         let workspace = window.root(cx);
2258//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2259
2260//         toggle_expand_dir(&panel, "src/test", cx);
2261//         select_path(&panel, "src/test/first.rs", cx);
2262//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
2263//         cx.foreground().run_until_parked();
2264//         assert_eq!(
2265//             visible_entries_as_strings(&panel, 0..10, cx),
2266//             &[
2267//                 "v src",
2268//                 "    v test",
2269//                 "          first.rs  <== selected",
2270//                 "          second.rs",
2271//                 "          third.rs"
2272//             ]
2273//         );
2274//         ensure_single_file_is_opened(window, "test/first.rs", cx);
2275
2276//         submit_deletion(window.into(), &panel, cx);
2277//         assert_eq!(
2278//             visible_entries_as_strings(&panel, 0..10, cx),
2279//             &[
2280//                 "v src",
2281//                 "    v test",
2282//                 "          second.rs",
2283//                 "          third.rs"
2284//             ],
2285//             "Project panel should have no deleted file, no other file is selected in it"
2286//         );
2287//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
2288
2289//         select_path(&panel, "src/test/second.rs", cx);
2290//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
2291//         cx.foreground().run_until_parked();
2292//         assert_eq!(
2293//             visible_entries_as_strings(&panel, 0..10, cx),
2294//             &[
2295//                 "v src",
2296//                 "    v test",
2297//                 "          second.rs  <== selected",
2298//                 "          third.rs"
2299//             ]
2300//         );
2301//         ensure_single_file_is_opened(window, "test/second.rs", cx);
2302
2303//         window.update(cx, |cx| {
2304//             let active_items = workspace
2305//                 .read(cx)
2306//                 .panes()
2307//                 .iter()
2308//                 .filter_map(|pane| pane.read(cx).active_item())
2309//                 .collect::<Vec<_>>();
2310//             assert_eq!(active_items.len(), 1);
2311//             let open_editor = active_items
2312//                 .into_iter()
2313//                 .next()
2314//                 .unwrap()
2315//                 .downcast::<Editor>()
2316//                 .expect("Open item should be an editor");
2317//             open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
2318//         });
2319//         submit_deletion(window.into(), &panel, cx);
2320//         assert_eq!(
2321//             visible_entries_as_strings(&panel, 0..10, cx),
2322//             &["v src", "    v test", "          third.rs"],
2323//             "Project panel should have no deleted file, with one last file remaining"
2324//         );
2325//         ensure_no_open_items_and_panes(window.into(), &workspace, cx);
2326//     }
2327
2328//     #[gpui::test]
2329//     async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
2330//         init_test_with_editor(cx);
2331
2332//         let fs = FakeFs::new(cx.background());
2333//         fs.insert_tree(
2334//             "/src",
2335//             json!({
2336//                 "test": {
2337//                     "first.rs": "// First Rust file",
2338//                     "second.rs": "// Second Rust file",
2339//                     "third.rs": "// Third Rust file",
2340//                 }
2341//             }),
2342//         )
2343//         .await;
2344
2345//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2346//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2347//         let workspace = window.root(cx);
2348//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2349
2350//         select_path(&panel, "src/", cx);
2351//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2352//         cx.foreground().run_until_parked();
2353//         assert_eq!(
2354//             visible_entries_as_strings(&panel, 0..10, cx),
2355//             &["v src  <== selected", "    > test"]
2356//         );
2357//         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
2358//         window.read_with(cx, |cx| {
2359//             let panel = panel.read(cx);
2360//             assert!(panel.filename_editor.is_focused(cx));
2361//         });
2362//         assert_eq!(
2363//             visible_entries_as_strings(&panel, 0..10, cx),
2364//             &["v src", "    > [EDITOR: '']  <== selected", "    > test"]
2365//         );
2366//         panel.update(cx, |panel, cx| {
2367//             panel
2368//                 .filename_editor
2369//                 .update(cx, |editor, cx| editor.set_text("test", cx));
2370//             assert!(
2371//                 panel.confirm(&Confirm, cx).is_none(),
2372//                 "Should not allow to confirm on conflicting new directory name"
2373//             )
2374//         });
2375//         assert_eq!(
2376//             visible_entries_as_strings(&panel, 0..10, cx),
2377//             &["v src", "    > test"],
2378//             "File list should be unchanged after failed folder create confirmation"
2379//         );
2380
2381//         select_path(&panel, "src/test/", cx);
2382//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2383//         cx.foreground().run_until_parked();
2384//         assert_eq!(
2385//             visible_entries_as_strings(&panel, 0..10, cx),
2386//             &["v src", "    > test  <== selected"]
2387//         );
2388//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
2389//         window.read_with(cx, |cx| {
2390//             let panel = panel.read(cx);
2391//             assert!(panel.filename_editor.is_focused(cx));
2392//         });
2393//         assert_eq!(
2394//             visible_entries_as_strings(&panel, 0..10, cx),
2395//             &[
2396//                 "v src",
2397//                 "    v test",
2398//                 "          [EDITOR: '']  <== selected",
2399//                 "          first.rs",
2400//                 "          second.rs",
2401//                 "          third.rs"
2402//             ]
2403//         );
2404//         panel.update(cx, |panel, cx| {
2405//             panel
2406//                 .filename_editor
2407//                 .update(cx, |editor, cx| editor.set_text("first.rs", cx));
2408//             assert!(
2409//                 panel.confirm(&Confirm, cx).is_none(),
2410//                 "Should not allow to confirm on conflicting new file name"
2411//             )
2412//         });
2413//         assert_eq!(
2414//             visible_entries_as_strings(&panel, 0..10, cx),
2415//             &[
2416//                 "v src",
2417//                 "    v test",
2418//                 "          first.rs",
2419//                 "          second.rs",
2420//                 "          third.rs"
2421//             ],
2422//             "File list should be unchanged after failed file create confirmation"
2423//         );
2424
2425//         select_path(&panel, "src/test/first.rs", cx);
2426//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2427//         cx.foreground().run_until_parked();
2428//         assert_eq!(
2429//             visible_entries_as_strings(&panel, 0..10, cx),
2430//             &[
2431//                 "v src",
2432//                 "    v test",
2433//                 "          first.rs  <== selected",
2434//                 "          second.rs",
2435//                 "          third.rs"
2436//             ],
2437//         );
2438//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
2439//         window.read_with(cx, |cx| {
2440//             let panel = panel.read(cx);
2441//             assert!(panel.filename_editor.is_focused(cx));
2442//         });
2443//         assert_eq!(
2444//             visible_entries_as_strings(&panel, 0..10, cx),
2445//             &[
2446//                 "v src",
2447//                 "    v test",
2448//                 "          [EDITOR: 'first.rs']  <== selected",
2449//                 "          second.rs",
2450//                 "          third.rs"
2451//             ]
2452//         );
2453//         panel.update(cx, |panel, cx| {
2454//             panel
2455//                 .filename_editor
2456//                 .update(cx, |editor, cx| editor.set_text("second.rs", cx));
2457//             assert!(
2458//                 panel.confirm(&Confirm, cx).is_none(),
2459//                 "Should not allow to confirm on conflicting file rename"
2460//             )
2461//         });
2462//         assert_eq!(
2463//             visible_entries_as_strings(&panel, 0..10, cx),
2464//             &[
2465//                 "v src",
2466//                 "    v test",
2467//                 "          first.rs  <== selected",
2468//                 "          second.rs",
2469//                 "          third.rs"
2470//             ],
2471//             "File list should be unchanged after failed rename confirmation"
2472//         );
2473//     }
2474
2475//     #[gpui::test]
2476//     async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
2477//         init_test_with_editor(cx);
2478
2479//         let fs = FakeFs::new(cx.background());
2480//         fs.insert_tree(
2481//             "/src",
2482//             json!({
2483//                 "test": {
2484//                     "first.rs": "// First Rust file",
2485//                     "second.rs": "// Second Rust file",
2486//                     "third.rs": "// Third Rust file",
2487//                 }
2488//             }),
2489//         )
2490//         .await;
2491
2492//         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2493//         let workspace = cx
2494//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2495//             .root(cx);
2496//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2497
2498//         let new_search_events_count = Arc::new(AtomicUsize::new(0));
2499//         let _subscription = panel.update(cx, |_, cx| {
2500//             let subcription_count = Arc::clone(&new_search_events_count);
2501//             cx.subscribe(&cx.handle(), move |_, _, event, _| {
2502//                 if matches!(event, Event::NewSearchInDirectory { .. }) {
2503//                     subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
2504//                 }
2505//             })
2506//         });
2507
2508//         toggle_expand_dir(&panel, "src/test", cx);
2509//         select_path(&panel, "src/test/first.rs", cx);
2510//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2511//         cx.foreground().run_until_parked();
2512//         assert_eq!(
2513//             visible_entries_as_strings(&panel, 0..10, cx),
2514//             &[
2515//                 "v src",
2516//                 "    v test",
2517//                 "          first.rs  <== selected",
2518//                 "          second.rs",
2519//                 "          third.rs"
2520//             ]
2521//         );
2522//         panel.update(cx, |panel, cx| {
2523//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
2524//         });
2525//         assert_eq!(
2526//             new_search_events_count.load(atomic::Ordering::SeqCst),
2527//             0,
2528//             "Should not trigger new search in directory when called on a file"
2529//         );
2530
2531//         select_path(&panel, "src/test", cx);
2532//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2533//         cx.foreground().run_until_parked();
2534//         assert_eq!(
2535//             visible_entries_as_strings(&panel, 0..10, cx),
2536//             &[
2537//                 "v src",
2538//                 "    v test  <== selected",
2539//                 "          first.rs",
2540//                 "          second.rs",
2541//                 "          third.rs"
2542//             ]
2543//         );
2544//         panel.update(cx, |panel, cx| {
2545//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
2546//         });
2547//         assert_eq!(
2548//             new_search_events_count.load(atomic::Ordering::SeqCst),
2549//             1,
2550//             "Should trigger new search in directory when called on a directory"
2551//         );
2552//     }
2553
2554//     #[gpui::test]
2555//     async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
2556//         init_test_with_editor(cx);
2557
2558//         let fs = FakeFs::new(cx.background());
2559//         fs.insert_tree(
2560//             "/project_root",
2561//             json!({
2562//                 "dir_1": {
2563//                     "nested_dir": {
2564//                         "file_a.py": "# File contents",
2565//                         "file_b.py": "# File contents",
2566//                         "file_c.py": "# File contents",
2567//                     },
2568//                     "file_1.py": "# File contents",
2569//                     "file_2.py": "# File contents",
2570//                     "file_3.py": "# File contents",
2571//                 },
2572//                 "dir_2": {
2573//                     "file_1.py": "# File contents",
2574//                     "file_2.py": "# File contents",
2575//                     "file_3.py": "# File contents",
2576//                 }
2577//             }),
2578//         )
2579//         .await;
2580
2581//         let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
2582//         let workspace = cx
2583//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2584//             .root(cx);
2585//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2586
2587//         panel.update(cx, |panel, cx| {
2588//             panel.collapse_all_entries(&CollapseAllEntries, cx)
2589//         });
2590//         cx.foreground().run_until_parked();
2591//         assert_eq!(
2592//             visible_entries_as_strings(&panel, 0..10, cx),
2593//             &["v project_root", "    > dir_1", "    > dir_2",]
2594//         );
2595
2596//         // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
2597//         toggle_expand_dir(&panel, "project_root/dir_1", cx);
2598//         cx.foreground().run_until_parked();
2599//         assert_eq!(
2600//             visible_entries_as_strings(&panel, 0..10, cx),
2601//             &[
2602//                 "v project_root",
2603//                 "    v dir_1  <== selected",
2604//                 "        > nested_dir",
2605//                 "          file_1.py",
2606//                 "          file_2.py",
2607//                 "          file_3.py",
2608//                 "    > dir_2",
2609//             ]
2610//         );
2611//     }
2612
2613//     #[gpui::test]
2614//     async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
2615//         init_test(cx);
2616
2617//         let fs = FakeFs::new(cx.background());
2618//         fs.as_fake().insert_tree("/root", json!({})).await;
2619//         let project = Project::test(fs, ["/root".as_ref()], cx).await;
2620//         let workspace = cx
2621//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
2622//             .root(cx);
2623//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2624
2625//         // Make a new buffer with no backing file
2626//         workspace.update(cx, |workspace, cx| {
2627//             Editor::new_file(workspace, &Default::default(), cx)
2628//         });
2629
2630//         // "Save as"" the buffer, creating a new backing file for it
2631//         let task = workspace.update(cx, |workspace, cx| {
2632//             workspace.save_active_item(workspace::SaveIntent::Save, cx)
2633//         });
2634
2635//         cx.foreground().run_until_parked();
2636//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
2637//         task.await.unwrap();
2638
2639//         // Rename the file
2640//         select_path(&panel, "root/new", cx);
2641//         assert_eq!(
2642//             visible_entries_as_strings(&panel, 0..10, cx),
2643//             &["v root", "      new  <== selected"]
2644//         );
2645//         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
2646//         panel.update(cx, |panel, cx| {
2647//             panel
2648//                 .filename_editor
2649//                 .update(cx, |editor, cx| editor.set_text("newer", cx));
2650//         });
2651//         panel
2652//             .update(cx, |panel, cx| panel.confirm(&Confirm, cx))
2653//             .unwrap()
2654//             .await
2655//             .unwrap();
2656
2657//         cx.foreground().run_until_parked();
2658//         assert_eq!(
2659//             visible_entries_as_strings(&panel, 0..10, cx),
2660//             &["v root", "      newer  <== selected"]
2661//         );
2662
2663//         workspace
2664//             .update(cx, |workspace, cx| {
2665//                 workspace.save_active_item(workspace::SaveIntent::Save, cx)
2666//             })
2667//             .await
2668//             .unwrap();
2669
2670//         cx.foreground().run_until_parked();
2671//         // assert that saving the file doesn't restore "new"
2672//         assert_eq!(
2673//             visible_entries_as_strings(&panel, 0..10, cx),
2674//             &["v root", "      newer  <== selected"]
2675//         );
2676//     }
2677
2678//     fn toggle_expand_dir(
2679//         panel: &View<ProjectPanel>,
2680//         path: impl AsRef<Path>,
2681//         cx: &mut TestAppContext,
2682//     ) {
2683//         let path = path.as_ref();
2684//         panel.update(cx, |panel, cx| {
2685//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
2686//                 let worktree = worktree.read(cx);
2687//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2688//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2689//                     panel.toggle_expanded(entry_id, cx);
2690//                     return;
2691//                 }
2692//             }
2693//             panic!("no worktree for path {:?}", path);
2694//         });
2695//     }
2696
2697//     fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut TestAppContext) {
2698//         let path = path.as_ref();
2699//         panel.update(cx, |panel, cx| {
2700//             for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
2701//                 let worktree = worktree.read(cx);
2702//                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2703//                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2704//                     panel.selection = Some(Selection {
2705//                         worktree_id: worktree.id(),
2706//                         entry_id,
2707//                     });
2708//                     return;
2709//                 }
2710//             }
2711//             panic!("no worktree for path {:?}", path);
2712//         });
2713//     }
2714
2715//     fn visible_entries_as_strings(
2716//         panel: &View<ProjectPanel>,
2717//         range: Range<usize>,
2718//         cx: &mut TestAppContext,
2719//     ) -> Vec<String> {
2720//         let mut result = Vec::new();
2721//         let mut project_entries = HashSet::new();
2722//         let mut has_editor = false;
2723
2724//         panel.update(cx, |panel, cx| {
2725//             panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
2726//                 if details.is_editing {
2727//                     assert!(!has_editor, "duplicate editor entry");
2728//                     has_editor = true;
2729//                 } else {
2730//                     assert!(
2731//                         project_entries.insert(project_entry),
2732//                         "duplicate project entry {:?} {:?}",
2733//                         project_entry,
2734//                         details
2735//                     );
2736//                 }
2737
2738//                 let indent = "    ".repeat(details.depth);
2739//                 let icon = if details.kind.is_dir() {
2740//                     if details.is_expanded {
2741//                         "v "
2742//                     } else {
2743//                         "> "
2744//                     }
2745//                 } else {
2746//                     "  "
2747//                 };
2748//                 let name = if details.is_editing {
2749//                     format!("[EDITOR: '{}']", details.filename)
2750//                 } else if details.is_processing {
2751//                     format!("[PROCESSING: '{}']", details.filename)
2752//                 } else {
2753//                     details.filename.clone()
2754//                 };
2755//                 let selected = if details.is_selected {
2756//                     "  <== selected"
2757//                 } else {
2758//                     ""
2759//                 };
2760//                 result.push(format!("{indent}{icon}{name}{selected}"));
2761//             });
2762//         });
2763
2764//         result
2765//     }
2766
2767//     fn init_test(cx: &mut TestAppContext) {
2768//         cx.foreground().forbid_parking();
2769//         cx.update(|cx| {
2770//             cx.set_global(SettingsStore::test(cx));
2771//             init_settings(cx);
2772//             theme::init(cx);
2773//             language::init(cx);
2774//             editor::init_settings(cx);
2775//             crate::init((), cx);
2776//             workspace::init_settings(cx);
2777//             client::init_settings(cx);
2778//             Project::init_settings(cx);
2779//         });
2780//     }
2781
2782//     fn init_test_with_editor(cx: &mut TestAppContext) {
2783//         cx.foreground().forbid_parking();
2784//         cx.update(|cx| {
2785//             let app_state = AppState::test(cx);
2786//             theme::init(cx);
2787//             init_settings(cx);
2788//             language::init(cx);
2789//             editor::init(cx);
2790//             pane::init(cx);
2791//             crate::init((), cx);
2792//             workspace::init(app_state.clone(), cx);
2793//             Project::init_settings(cx);
2794//         });
2795//     }
2796
2797//     fn ensure_single_file_is_opened(
2798//         window: WindowHandle<Workspace>,
2799//         expected_path: &str,
2800//         cx: &mut TestAppContext,
2801//     ) {
2802//         window.update_root(cx, |workspace, cx| {
2803//             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
2804//             assert_eq!(worktrees.len(), 1);
2805//             let worktree_id = WorktreeId::from_usize(worktrees[0].id());
2806
2807//             let open_project_paths = workspace
2808//                 .panes()
2809//                 .iter()
2810//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2811//                 .collect::<Vec<_>>();
2812//             assert_eq!(
2813//                 open_project_paths,
2814//                 vec![ProjectPath {
2815//                     worktree_id,
2816//                     path: Arc::from(Path::new(expected_path))
2817//                 }],
2818//                 "Should have opened file, selected in project panel"
2819//             );
2820//         });
2821//     }
2822
2823//     fn submit_deletion(
2824//         window: AnyWindowHandle,
2825//         panel: &View<ProjectPanel>,
2826//         cx: &mut TestAppContext,
2827//     ) {
2828//         assert!(
2829//             !window.has_pending_prompt(cx),
2830//             "Should have no prompts before the deletion"
2831//         );
2832//         panel.update(cx, |panel, cx| {
2833//             panel
2834//                 .delete(&Delete, cx)
2835//                 .expect("Deletion start")
2836//                 .detach_and_log_err(cx);
2837//         });
2838//         assert!(
2839//             window.has_pending_prompt(cx),
2840//             "Should have a prompt after the deletion"
2841//         );
2842//         window.simulate_prompt_answer(0, cx);
2843//         assert!(
2844//             !window.has_pending_prompt(cx),
2845//             "Should have no prompts after prompt was replied to"
2846//         );
2847//         cx.foreground().run_until_parked();
2848//     }
2849
2850//     fn ensure_no_open_items_and_panes(
2851//         window: AnyWindowHandle,
2852//         workspace: &View<Workspace>,
2853//         cx: &mut TestAppContext,
2854//     ) {
2855//         assert!(
2856//             !window.has_pending_prompt(cx),
2857//             "Should have no prompts after deletion operation closes the file"
2858//         );
2859//         window.read_with(cx, |cx| {
2860//             let open_project_paths = workspace
2861//                 .read(cx)
2862//                 .panes()
2863//                 .iter()
2864//                 .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2865//                 .collect::<Vec<_>>();
2866//             assert!(
2867//                 open_project_paths.is_empty(),
2868//                 "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
2869//             );
2870//         });
2871//     }
2872// }