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