project_panel.rs

   1mod project_panel_settings;
   2
   3use context_menu::{ContextMenu, ContextMenuItem};
   4use db::kvp::KEY_VALUE_STORE;
   5use drag_and_drop::{DragAndDrop, Draggable};
   6use editor::{Cancel, Editor};
   7use futures::stream::StreamExt;
   8use gpui::{
   9    actions,
  10    anyhow::{self, anyhow, Result},
  11    elements::{
  12        AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
  13        ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
  14    },
  15    geometry::vector::Vector2F,
  16    keymap_matcher::KeymapContext,
  17    platform::{CursorStyle, MouseButton, PromptLevel},
  18    Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle,
  19    Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  20};
  21use menu::{Confirm, SelectNext, SelectPrev};
  22use project::{
  23    repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
  24    Worktree, WorktreeId,
  25};
  26use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
  27use serde::{Deserialize, Serialize};
  28use settings::SettingsStore;
  29use std::{
  30    cmp::Ordering,
  31    collections::{hash_map, HashMap},
  32    ffi::OsStr,
  33    ops::Range,
  34    path::Path,
  35    sync::Arc,
  36};
  37use theme::ProjectPanelEntry;
  38use unicase::UniCase;
  39use util::{ResultExt, TryFutureExt};
  40use workspace::{
  41    dock::{DockPosition, Panel},
  42    Workspace,
  43};
  44
  45const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
  46const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
  47
  48pub struct ProjectPanel {
  49    project: ModelHandle<Project>,
  50    fs: Arc<dyn Fs>,
  51    list: UniformListState,
  52    visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
  53    last_worktree_root_id: Option<ProjectEntryId>,
  54    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
  55    selection: Option<Selection>,
  56    edit_state: Option<EditState>,
  57    filename_editor: ViewHandle<Editor>,
  58    clipboard_entry: Option<ClipboardEntry>,
  59    context_menu: ViewHandle<ContextMenu>,
  60    dragged_entry_destination: Option<Arc<Path>>,
  61    workspace: WeakViewHandle<Workspace>,
  62    has_focus: bool,
  63    width: Option<f32>,
  64    pending_serialization: Task<Option<()>>,
  65}
  66
  67#[derive(Copy, Clone)]
  68struct Selection {
  69    worktree_id: WorktreeId,
  70    entry_id: ProjectEntryId,
  71}
  72
  73#[derive(Clone, Debug)]
  74struct EditState {
  75    worktree_id: WorktreeId,
  76    entry_id: ProjectEntryId,
  77    is_new_entry: bool,
  78    is_dir: bool,
  79    processing_filename: Option<String>,
  80}
  81
  82#[derive(Copy, Clone)]
  83pub enum ClipboardEntry {
  84    Copied {
  85        worktree_id: WorktreeId,
  86        entry_id: ProjectEntryId,
  87    },
  88    Cut {
  89        worktree_id: WorktreeId,
  90        entry_id: ProjectEntryId,
  91    },
  92}
  93
  94#[derive(Debug, PartialEq, Eq)]
  95pub struct EntryDetails {
  96    filename: String,
  97    path: Arc<Path>,
  98    depth: usize,
  99    kind: EntryKind,
 100    is_ignored: bool,
 101    is_expanded: bool,
 102    is_selected: bool,
 103    is_editing: bool,
 104    is_processing: bool,
 105    is_cut: bool,
 106    git_status: Option<GitFileStatus>,
 107}
 108
 109actions!(
 110    project_panel,
 111    [
 112        ExpandSelectedEntry,
 113        CollapseSelectedEntry,
 114        NewDirectory,
 115        NewFile,
 116        Copy,
 117        CopyPath,
 118        CopyRelativePath,
 119        RevealInFinder,
 120        Cut,
 121        Paste,
 122        Delete,
 123        Rename,
 124        ToggleFocus
 125    ]
 126);
 127
 128pub fn init_settings(cx: &mut AppContext) {
 129    settings::register::<ProjectPanelSettings>(cx);
 130}
 131
 132pub fn init(cx: &mut AppContext) {
 133    init_settings(cx);
 134    cx.add_action(ProjectPanel::expand_selected_entry);
 135    cx.add_action(ProjectPanel::collapse_selected_entry);
 136    cx.add_action(ProjectPanel::select_prev);
 137    cx.add_action(ProjectPanel::select_next);
 138    cx.add_action(ProjectPanel::new_file);
 139    cx.add_action(ProjectPanel::new_directory);
 140    cx.add_action(ProjectPanel::rename);
 141    cx.add_async_action(ProjectPanel::delete);
 142    cx.add_async_action(ProjectPanel::confirm);
 143    cx.add_action(ProjectPanel::cancel);
 144    cx.add_action(ProjectPanel::cut);
 145    cx.add_action(ProjectPanel::copy);
 146    cx.add_action(ProjectPanel::copy_path);
 147    cx.add_action(ProjectPanel::copy_relative_path);
 148    cx.add_action(ProjectPanel::reveal_in_finder);
 149    cx.add_action(
 150        |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
 151            this.paste(action, cx);
 152        },
 153    );
 154}
 155
 156pub enum Event {
 157    OpenedEntry {
 158        entry_id: ProjectEntryId,
 159        focus_opened_item: bool,
 160    },
 161    DockPositionChanged,
 162    Focus,
 163}
 164
 165#[derive(Serialize, Deserialize)]
 166struct SerializedProjectPanel {
 167    width: Option<f32>,
 168}
 169
 170impl ProjectPanel {
 171    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 172        let project = workspace.project().clone();
 173        let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
 174            cx.observe(&project, |this, _, cx| {
 175                this.update_visible_entries(None, cx);
 176                cx.notify();
 177            })
 178            .detach();
 179            cx.subscribe(&project, |this, project, event, cx| match event {
 180                project::Event::ActiveEntryChanged(Some(entry_id)) => {
 181                    if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
 182                    {
 183                        this.expand_entry(worktree_id, *entry_id, cx);
 184                        this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
 185                        this.autoscroll(cx);
 186                        cx.notify();
 187                    }
 188                }
 189                project::Event::WorktreeRemoved(id) => {
 190                    this.expanded_dir_ids.remove(id);
 191                    this.update_visible_entries(None, cx);
 192                    cx.notify();
 193                }
 194                _ => {}
 195            })
 196            .detach();
 197
 198            let filename_editor = cx.add_view(|cx| {
 199                Editor::single_line(
 200                    Some(Arc::new(|theme| {
 201                        let mut style = theme.project_panel.filename_editor.clone();
 202                        style.container.background_color.take();
 203                        style
 204                    })),
 205                    cx,
 206                )
 207            });
 208
 209            cx.subscribe(&filename_editor, |this, _, event, cx| match event {
 210                editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
 211                    this.autoscroll(cx);
 212                }
 213                _ => {}
 214            })
 215            .detach();
 216            cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
 217                if !is_focused
 218                    && this
 219                        .edit_state
 220                        .as_ref()
 221                        .map_or(false, |state| state.processing_filename.is_none())
 222                {
 223                    this.edit_state = None;
 224                    this.update_visible_entries(None, cx);
 225                }
 226            })
 227            .detach();
 228
 229            let view_id = cx.view_id();
 230            let mut this = Self {
 231                project: project.clone(),
 232                fs: workspace.app_state().fs.clone(),
 233                list: Default::default(),
 234                visible_entries: Default::default(),
 235                last_worktree_root_id: Default::default(),
 236                expanded_dir_ids: Default::default(),
 237                selection: None,
 238                edit_state: None,
 239                filename_editor,
 240                clipboard_entry: None,
 241                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 242                dragged_entry_destination: None,
 243                workspace: workspace.weak_handle(),
 244                has_focus: false,
 245                width: None,
 246                pending_serialization: Task::ready(None),
 247            };
 248            this.update_visible_entries(None, cx);
 249
 250            // Update the dock position when the setting changes.
 251            let mut old_dock_position = this.position(cx);
 252            cx.observe_global::<SettingsStore, _>(move |this, cx| {
 253                let new_dock_position = this.position(cx);
 254                if new_dock_position != old_dock_position {
 255                    old_dock_position = new_dock_position;
 256                    cx.emit(Event::DockPositionChanged);
 257                }
 258            })
 259            .detach();
 260
 261            this
 262        });
 263
 264        cx.subscribe(&project_panel, {
 265            let project_panel = project_panel.downgrade();
 266            move |workspace, _, event, cx| match event {
 267                &Event::OpenedEntry {
 268                    entry_id,
 269                    focus_opened_item,
 270                } => {
 271                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
 272                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 273                            workspace
 274                                .open_path(
 275                                    ProjectPath {
 276                                        worktree_id: worktree.read(cx).id(),
 277                                        path: entry.path.clone(),
 278                                    },
 279                                    None,
 280                                    focus_opened_item,
 281                                    cx,
 282                                )
 283                                .detach_and_log_err(cx);
 284                            if !focus_opened_item {
 285                                if let Some(project_panel) = project_panel.upgrade(cx) {
 286                                    cx.focus(&project_panel);
 287                                }
 288                            }
 289                        }
 290                    }
 291                }
 292                _ => {}
 293            }
 294        })
 295        .detach();
 296
 297        project_panel
 298    }
 299
 300    pub fn load(
 301        workspace: WeakViewHandle<Workspace>,
 302        cx: AsyncAppContext,
 303    ) -> Task<Result<ViewHandle<Self>>> {
 304        cx.spawn(|mut cx| async move {
 305            let serialized_panel = if let Some(panel) = cx
 306                .background()
 307                .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
 308                .await
 309                .log_err()
 310                .flatten()
 311            {
 312                Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
 313            } else {
 314                None
 315            };
 316            workspace.update(&mut cx, |workspace, cx| {
 317                let panel = ProjectPanel::new(workspace, cx);
 318                if let Some(serialized_panel) = serialized_panel {
 319                    panel.update(cx, |panel, cx| {
 320                        panel.width = serialized_panel.width;
 321                        cx.notify();
 322                    });
 323                }
 324                panel
 325            })
 326        })
 327    }
 328
 329    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 330        let width = self.width;
 331        self.pending_serialization = cx.background().spawn(
 332            async move {
 333                KEY_VALUE_STORE
 334                    .write_kvp(
 335                        PROJECT_PANEL_KEY.into(),
 336                        serde_json::to_string(&SerializedProjectPanel { width })?,
 337                    )
 338                    .await?;
 339                anyhow::Ok(())
 340            }
 341            .log_err(),
 342        );
 343    }
 344
 345    fn deploy_context_menu(
 346        &mut self,
 347        position: Vector2F,
 348        entry_id: ProjectEntryId,
 349        cx: &mut ViewContext<Self>,
 350    ) {
 351        let project = self.project.read(cx);
 352
 353        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
 354            id
 355        } else {
 356            return;
 357        };
 358
 359        self.selection = Some(Selection {
 360            worktree_id,
 361            entry_id,
 362        });
 363
 364        let mut menu_entries = Vec::new();
 365        if let Some((worktree, entry)) = self.selected_entry(cx) {
 366            let is_root = Some(entry) == worktree.root_entry();
 367            if !project.is_remote() {
 368                menu_entries.push(ContextMenuItem::action(
 369                    "Add Folder to Project",
 370                    workspace::AddFolderToProject,
 371                ));
 372                if is_root {
 373                    let project = self.project.clone();
 374                    menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
 375                        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
 376                    }));
 377                }
 378            }
 379            menu_entries.push(ContextMenuItem::action("New File", NewFile));
 380            menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
 381            menu_entries.push(ContextMenuItem::Separator);
 382            menu_entries.push(ContextMenuItem::action("Cut", Cut));
 383            menu_entries.push(ContextMenuItem::action("Copy", Copy));
 384            menu_entries.push(ContextMenuItem::Separator);
 385            menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
 386            menu_entries.push(ContextMenuItem::action(
 387                "Copy Relative Path",
 388                CopyRelativePath,
 389            ));
 390            menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
 391            if let Some(clipboard_entry) = self.clipboard_entry {
 392                if clipboard_entry.worktree_id() == worktree.id() {
 393                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
 394                }
 395            }
 396            menu_entries.push(ContextMenuItem::Separator);
 397            menu_entries.push(ContextMenuItem::action("Rename", Rename));
 398            if !is_root {
 399                menu_entries.push(ContextMenuItem::action("Delete", Delete));
 400            }
 401        }
 402
 403        self.context_menu.update(cx, |menu, cx| {
 404            menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
 405        });
 406
 407        cx.notify();
 408    }
 409
 410    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
 411        if let Some((worktree, entry)) = self.selected_entry(cx) {
 412            if entry.is_dir() {
 413                let expanded_dir_ids =
 414                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
 415                        expanded_dir_ids
 416                    } else {
 417                        return;
 418                    };
 419
 420                match expanded_dir_ids.binary_search(&entry.id) {
 421                    Ok(_) => self.select_next(&SelectNext, cx),
 422                    Err(ix) => {
 423                        expanded_dir_ids.insert(ix, entry.id);
 424                        self.update_visible_entries(None, cx);
 425                        cx.notify();
 426                    }
 427                }
 428            }
 429        }
 430    }
 431
 432    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
 433        if let Some((worktree, mut entry)) = self.selected_entry(cx) {
 434            let expanded_dir_ids =
 435                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
 436                    expanded_dir_ids
 437                } else {
 438                    return;
 439                };
 440
 441            loop {
 442                match expanded_dir_ids.binary_search(&entry.id) {
 443                    Ok(ix) => {
 444                        expanded_dir_ids.remove(ix);
 445                        self.update_visible_entries(Some((worktree.id(), entry.id)), cx);
 446                        cx.notify();
 447                        break;
 448                    }
 449                    Err(_) => {
 450                        if let Some(parent_entry) =
 451                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
 452                        {
 453                            entry = parent_entry;
 454                        } else {
 455                            break;
 456                        }
 457                    }
 458                }
 459            }
 460        }
 461    }
 462
 463    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
 464        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
 465            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
 466                match expanded_dir_ids.binary_search(&entry_id) {
 467                    Ok(ix) => {
 468                        expanded_dir_ids.remove(ix);
 469                    }
 470                    Err(ix) => {
 471                        expanded_dir_ids.insert(ix, entry_id);
 472                    }
 473                }
 474                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
 475                cx.focus_self();
 476                cx.notify();
 477            }
 478        }
 479    }
 480
 481    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 482        if let Some(selection) = self.selection {
 483            let (mut worktree_ix, mut entry_ix, _) =
 484                self.index_for_selection(selection).unwrap_or_default();
 485            if entry_ix > 0 {
 486                entry_ix -= 1;
 487            } else if worktree_ix > 0 {
 488                worktree_ix -= 1;
 489                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
 490            } else {
 491                return;
 492            }
 493
 494            let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix];
 495            self.selection = Some(Selection {
 496                worktree_id: *worktree_id,
 497                entry_id: worktree_entries[entry_ix].id,
 498            });
 499            self.autoscroll(cx);
 500            cx.notify();
 501        } else {
 502            self.select_first(cx);
 503        }
 504    }
 505
 506    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
 507        if let Some(task) = self.confirm_edit(cx) {
 508            Some(task)
 509        } else if let Some((_, entry)) = self.selected_entry(cx) {
 510            if entry.is_file() {
 511                self.open_entry(entry.id, true, cx);
 512            }
 513            None
 514        } else {
 515            None
 516        }
 517    }
 518
 519    fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
 520        let edit_state = self.edit_state.as_mut()?;
 521        cx.focus_self();
 522
 523        let worktree_id = edit_state.worktree_id;
 524        let is_new_entry = edit_state.is_new_entry;
 525        let is_dir = edit_state.is_dir;
 526        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
 527        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
 528        let filename = self.filename_editor.read(cx).text(cx);
 529
 530        let edit_task;
 531        let edited_entry_id;
 532
 533        if is_new_entry {
 534            self.selection = Some(Selection {
 535                worktree_id,
 536                entry_id: NEW_ENTRY_ID,
 537            });
 538            let new_path = entry.path.join(&filename);
 539            edited_entry_id = NEW_ENTRY_ID;
 540            edit_task = self.project.update(cx, |project, cx| {
 541                project.create_entry((worktree_id, new_path), is_dir, cx)
 542            })?;
 543        } else {
 544            let new_path = if let Some(parent) = entry.path.clone().parent() {
 545                parent.join(&filename)
 546            } else {
 547                filename.clone().into()
 548            };
 549            edited_entry_id = entry.id;
 550            edit_task = self.project.update(cx, |project, cx| {
 551                project.rename_entry(entry.id, new_path, cx)
 552            })?;
 553        };
 554
 555        edit_state.processing_filename = Some(filename);
 556        cx.notify();
 557
 558        Some(cx.spawn(|this, mut cx| async move {
 559            let new_entry = edit_task.await;
 560            this.update(&mut cx, |this, cx| {
 561                this.edit_state.take();
 562                cx.notify();
 563            })?;
 564
 565            let new_entry = new_entry?;
 566            this.update(&mut cx, |this, cx| {
 567                if let Some(selection) = &mut this.selection {
 568                    if selection.entry_id == edited_entry_id {
 569                        selection.worktree_id = worktree_id;
 570                        selection.entry_id = new_entry.id;
 571                    }
 572                }
 573                this.update_visible_entries(None, cx);
 574                if is_new_entry && !is_dir {
 575                    this.open_entry(new_entry.id, true, cx);
 576                }
 577                cx.notify();
 578            })?;
 579            Ok(())
 580        }))
 581    }
 582
 583    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
 584        self.edit_state = None;
 585        self.update_visible_entries(None, cx);
 586        cx.focus_self();
 587        cx.notify();
 588    }
 589
 590    fn open_entry(
 591        &mut self,
 592        entry_id: ProjectEntryId,
 593        focus_opened_item: bool,
 594        cx: &mut ViewContext<Self>,
 595    ) {
 596        cx.emit(Event::OpenedEntry {
 597            entry_id,
 598            focus_opened_item,
 599        });
 600    }
 601
 602    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
 603        self.add_entry(false, cx)
 604    }
 605
 606    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
 607        self.add_entry(true, cx)
 608    }
 609
 610    fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
 611        if let Some(Selection {
 612            worktree_id,
 613            entry_id,
 614        }) = self.selection
 615        {
 616            let directory_id;
 617            if let Some((worktree, expanded_dir_ids)) = self
 618                .project
 619                .read(cx)
 620                .worktree_for_id(worktree_id, cx)
 621                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
 622            {
 623                let worktree = worktree.read(cx);
 624                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
 625                    loop {
 626                        if entry.is_dir() {
 627                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
 628                                expanded_dir_ids.insert(ix, entry.id);
 629                            }
 630                            directory_id = entry.id;
 631                            break;
 632                        } else {
 633                            if let Some(parent_path) = entry.path.parent() {
 634                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
 635                                    entry = parent_entry;
 636                                    continue;
 637                                }
 638                            }
 639                            return;
 640                        }
 641                    }
 642                } else {
 643                    return;
 644                };
 645            } else {
 646                return;
 647            };
 648
 649            self.edit_state = Some(EditState {
 650                worktree_id,
 651                entry_id: directory_id,
 652                is_new_entry: true,
 653                is_dir,
 654                processing_filename: None,
 655            });
 656            self.filename_editor
 657                .update(cx, |editor, cx| editor.clear(cx));
 658            cx.focus(&self.filename_editor);
 659            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
 660            self.autoscroll(cx);
 661            cx.notify();
 662        }
 663    }
 664
 665    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
 666        if let Some(Selection {
 667            worktree_id,
 668            entry_id,
 669        }) = self.selection
 670        {
 671            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
 672                if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
 673                    self.edit_state = Some(EditState {
 674                        worktree_id,
 675                        entry_id,
 676                        is_new_entry: false,
 677                        is_dir: entry.is_dir(),
 678                        processing_filename: None,
 679                    });
 680                    let filename = entry
 681                        .path
 682                        .file_name()
 683                        .map_or(String::new(), |s| s.to_string_lossy().to_string());
 684                    self.filename_editor.update(cx, |editor, cx| {
 685                        editor.set_text(filename, cx);
 686                        editor.select_all(&Default::default(), cx);
 687                    });
 688                    cx.focus(&self.filename_editor);
 689                    self.update_visible_entries(None, cx);
 690                    self.autoscroll(cx);
 691                    cx.notify();
 692                }
 693            }
 694
 695            cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
 696                drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
 697            })
 698        }
 699    }
 700
 701    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
 702        let Selection { entry_id, .. } = self.selection?;
 703        let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
 704        let file_name = path.file_name()?;
 705
 706        let mut answer = cx.prompt(
 707            PromptLevel::Info,
 708            &format!("Delete {file_name:?}?"),
 709            &["Delete", "Cancel"],
 710        );
 711        Some(cx.spawn(|this, mut cx| async move {
 712            if answer.next().await != Some(0) {
 713                return Ok(());
 714            }
 715            this.update(&mut cx, |this, cx| {
 716                this.project
 717                    .update(cx, |project, cx| project.delete_entry(entry_id, cx))
 718                    .ok_or_else(|| anyhow!("no such entry"))
 719            })??
 720            .await
 721        }))
 722    }
 723
 724    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 725        if let Some(selection) = self.selection {
 726            let (mut worktree_ix, mut entry_ix, _) =
 727                self.index_for_selection(selection).unwrap_or_default();
 728            if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) {
 729                if entry_ix + 1 < worktree_entries.len() {
 730                    entry_ix += 1;
 731                } else {
 732                    worktree_ix += 1;
 733                    entry_ix = 0;
 734                }
 735            }
 736
 737            if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) {
 738                if let Some(entry) = worktree_entries.get(entry_ix) {
 739                    self.selection = Some(Selection {
 740                        worktree_id: *worktree_id,
 741                        entry_id: entry.id,
 742                    });
 743                    self.autoscroll(cx);
 744                    cx.notify();
 745                }
 746            }
 747        } else {
 748            self.select_first(cx);
 749        }
 750    }
 751
 752    fn select_first(&mut self, cx: &mut ViewContext<Self>) {
 753        let worktree = self
 754            .visible_entries
 755            .first()
 756            .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
 757        if let Some(worktree) = worktree {
 758            let worktree = worktree.read(cx);
 759            let worktree_id = worktree.id();
 760            if let Some(root_entry) = worktree.root_entry() {
 761                self.selection = Some(Selection {
 762                    worktree_id,
 763                    entry_id: root_entry.id,
 764                });
 765                self.autoscroll(cx);
 766                cx.notify();
 767            }
 768        }
 769    }
 770
 771    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
 772        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
 773            self.list.scroll_to(ScrollTarget::Show(index));
 774            cx.notify();
 775        }
 776    }
 777
 778    fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
 779        if let Some((worktree, entry)) = self.selected_entry(cx) {
 780            self.clipboard_entry = Some(ClipboardEntry::Cut {
 781                worktree_id: worktree.id(),
 782                entry_id: entry.id,
 783            });
 784            cx.notify();
 785        }
 786    }
 787
 788    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
 789        if let Some((worktree, entry)) = self.selected_entry(cx) {
 790            self.clipboard_entry = Some(ClipboardEntry::Copied {
 791                worktree_id: worktree.id(),
 792                entry_id: entry.id,
 793            });
 794            cx.notify();
 795        }
 796    }
 797
 798    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) -> Option<()> {
 799        if let Some((worktree, entry)) = self.selected_entry(cx) {
 800            let clipboard_entry = self.clipboard_entry?;
 801            if clipboard_entry.worktree_id() != worktree.id() {
 802                return None;
 803            }
 804
 805            let clipboard_entry_file_name = self
 806                .project
 807                .read(cx)
 808                .path_for_entry(clipboard_entry.entry_id(), cx)?
 809                .path
 810                .file_name()?
 811                .to_os_string();
 812
 813            let mut new_path = entry.path.to_path_buf();
 814            if entry.is_file() {
 815                new_path.pop();
 816            }
 817
 818            new_path.push(&clipboard_entry_file_name);
 819            let extension = new_path.extension().map(|e| e.to_os_string());
 820            let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
 821            let mut ix = 0;
 822            while worktree.entry_for_path(&new_path).is_some() {
 823                new_path.pop();
 824
 825                let mut new_file_name = file_name_without_extension.to_os_string();
 826                new_file_name.push(" copy");
 827                if ix > 0 {
 828                    new_file_name.push(format!(" {}", ix));
 829                }
 830                if let Some(extension) = extension.as_ref() {
 831                    new_file_name.push(".");
 832                    new_file_name.push(extension);
 833                }
 834
 835                new_path.push(new_file_name);
 836                ix += 1;
 837            }
 838
 839            if clipboard_entry.is_cut() {
 840                if let Some(task) = self.project.update(cx, |project, cx| {
 841                    project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
 842                }) {
 843                    task.detach_and_log_err(cx)
 844                }
 845            } else if let Some(task) = self.project.update(cx, |project, cx| {
 846                project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
 847            }) {
 848                task.detach_and_log_err(cx)
 849            }
 850        }
 851        None
 852    }
 853
 854    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
 855        if let Some((worktree, entry)) = self.selected_entry(cx) {
 856            cx.write_to_clipboard(ClipboardItem::new(
 857                worktree
 858                    .abs_path()
 859                    .join(&entry.path)
 860                    .to_string_lossy()
 861                    .to_string(),
 862            ));
 863        }
 864    }
 865
 866    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
 867        if let Some((_, entry)) = self.selected_entry(cx) {
 868            cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string()));
 869        }
 870    }
 871
 872    fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
 873        if let Some((worktree, entry)) = self.selected_entry(cx) {
 874            cx.reveal_path(&worktree.abs_path().join(&entry.path));
 875        }
 876    }
 877
 878    fn move_entry(
 879        &mut self,
 880        entry_to_move: ProjectEntryId,
 881        destination: ProjectEntryId,
 882        destination_is_file: bool,
 883        cx: &mut ViewContext<Self>,
 884    ) {
 885        let destination_worktree = self.project.update(cx, |project, cx| {
 886            let entry_path = project.path_for_entry(entry_to_move, cx)?;
 887            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
 888
 889            let mut destination_path = destination_entry_path.as_ref();
 890            if destination_is_file {
 891                destination_path = destination_path.parent()?;
 892            }
 893
 894            let mut new_path = destination_path.to_path_buf();
 895            new_path.push(entry_path.path.file_name()?);
 896            if new_path != entry_path.path.as_ref() {
 897                let task = project.rename_entry(entry_to_move, new_path, cx)?;
 898                cx.foreground().spawn(task).detach_and_log_err(cx);
 899            }
 900
 901            Some(project.worktree_id_for_entry(destination, cx)?)
 902        });
 903
 904        if let Some(destination_worktree) = destination_worktree {
 905            self.expand_entry(destination_worktree, destination, cx);
 906        }
 907    }
 908
 909    fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
 910        let mut entry_index = 0;
 911        let mut visible_entries_index = 0;
 912        for (worktree_index, (worktree_id, worktree_entries)) in
 913            self.visible_entries.iter().enumerate()
 914        {
 915            if *worktree_id == selection.worktree_id {
 916                for entry in worktree_entries {
 917                    if entry.id == selection.entry_id {
 918                        return Some((worktree_index, entry_index, visible_entries_index));
 919                    } else {
 920                        visible_entries_index += 1;
 921                        entry_index += 1;
 922                    }
 923                }
 924                break;
 925            } else {
 926                visible_entries_index += worktree_entries.len();
 927            }
 928        }
 929        None
 930    }
 931
 932    fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
 933        let selection = self.selection?;
 934        let project = self.project.read(cx);
 935        let worktree = project.worktree_for_id(selection.worktree_id, cx)?.read(cx);
 936        Some((worktree, worktree.entry_for_id(selection.entry_id)?))
 937    }
 938
 939    fn update_visible_entries(
 940        &mut self,
 941        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
 942        cx: &mut ViewContext<Self>,
 943    ) {
 944        let project = self.project.read(cx);
 945        self.last_worktree_root_id = project
 946            .visible_worktrees(cx)
 947            .rev()
 948            .next()
 949            .and_then(|worktree| worktree.read(cx).root_entry())
 950            .map(|entry| entry.id);
 951
 952        self.visible_entries.clear();
 953        for worktree in project.visible_worktrees(cx) {
 954            let snapshot = worktree.read(cx).snapshot();
 955            let worktree_id = snapshot.id();
 956
 957            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
 958                hash_map::Entry::Occupied(e) => e.into_mut(),
 959                hash_map::Entry::Vacant(e) => {
 960                    // The first time a worktree's root entry becomes available,
 961                    // mark that root entry as expanded.
 962                    if let Some(entry) = snapshot.root_entry() {
 963                        e.insert(vec![entry.id]).as_slice()
 964                    } else {
 965                        &[]
 966                    }
 967                }
 968            };
 969
 970            let mut new_entry_parent_id = None;
 971            let mut new_entry_kind = EntryKind::Dir;
 972            if let Some(edit_state) = &self.edit_state {
 973                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
 974                    new_entry_parent_id = Some(edit_state.entry_id);
 975                    new_entry_kind = if edit_state.is_dir {
 976                        EntryKind::Dir
 977                    } else {
 978                        EntryKind::File(Default::default())
 979                    };
 980                }
 981            }
 982
 983            let mut visible_worktree_entries = Vec::new();
 984            let mut entry_iter = snapshot.entries(true);
 985
 986            while let Some(entry) = entry_iter.entry() {
 987                visible_worktree_entries.push(entry.clone());
 988                if Some(entry.id) == new_entry_parent_id {
 989                    visible_worktree_entries.push(Entry {
 990                        id: NEW_ENTRY_ID,
 991                        kind: new_entry_kind,
 992                        path: entry.path.join("\0").into(),
 993                        inode: 0,
 994                        mtime: entry.mtime,
 995                        is_symlink: false,
 996                        is_ignored: false,
 997                    });
 998                }
 999                if expanded_dir_ids.binary_search(&entry.id).is_err()
1000                    && entry_iter.advance_to_sibling()
1001                {
1002                    continue;
1003                }
1004                entry_iter.advance();
1005            }
1006            visible_worktree_entries.sort_by(|entry_a, entry_b| {
1007                let mut components_a = entry_a.path.components().peekable();
1008                let mut components_b = entry_b.path.components().peekable();
1009                loop {
1010                    match (components_a.next(), components_b.next()) {
1011                        (Some(component_a), Some(component_b)) => {
1012                            let a_is_file = components_a.peek().is_none() && entry_a.is_file();
1013                            let b_is_file = components_b.peek().is_none() && entry_b.is_file();
1014                            let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
1015                                let name_a =
1016                                    UniCase::new(component_a.as_os_str().to_string_lossy());
1017                                let name_b =
1018                                    UniCase::new(component_b.as_os_str().to_string_lossy());
1019                                name_a.cmp(&name_b)
1020                            });
1021                            if !ordering.is_eq() {
1022                                return ordering;
1023                            }
1024                        }
1025                        (Some(_), None) => break Ordering::Greater,
1026                        (None, Some(_)) => break Ordering::Less,
1027                        (None, None) => break Ordering::Equal,
1028                    }
1029                }
1030            });
1031            self.visible_entries
1032                .push((worktree_id, visible_worktree_entries));
1033        }
1034
1035        if let Some((worktree_id, entry_id)) = new_selected_entry {
1036            self.selection = Some(Selection {
1037                worktree_id,
1038                entry_id,
1039            });
1040        }
1041    }
1042
1043    fn expand_entry(
1044        &mut self,
1045        worktree_id: WorktreeId,
1046        entry_id: ProjectEntryId,
1047        cx: &mut ViewContext<Self>,
1048    ) {
1049        let project = self.project.read(cx);
1050        if let Some((worktree, expanded_dir_ids)) = project
1051            .worktree_for_id(worktree_id, cx)
1052            .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1053        {
1054            let worktree = worktree.read(cx);
1055
1056            if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1057                loop {
1058                    if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1059                        expanded_dir_ids.insert(ix, entry.id);
1060                    }
1061
1062                    if let Some(parent_entry) =
1063                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1064                    {
1065                        entry = parent_entry;
1066                    } else {
1067                        break;
1068                    }
1069                }
1070            }
1071        }
1072    }
1073
1074    fn for_each_visible_entry(
1075        &self,
1076        range: Range<usize>,
1077        cx: &mut ViewContext<ProjectPanel>,
1078        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
1079    ) {
1080        let mut ix = 0;
1081        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
1082            if ix >= range.end {
1083                return;
1084            }
1085
1086            if ix + visible_worktree_entries.len() <= range.start {
1087                ix += visible_worktree_entries.len();
1088                continue;
1089            }
1090
1091            let end_ix = range.end.min(ix + visible_worktree_entries.len());
1092            let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
1093            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1094                let snapshot = worktree.read(cx).snapshot();
1095                let root_name = OsStr::new(snapshot.root_name());
1096                let expanded_entry_ids = self
1097                    .expanded_dir_ids
1098                    .get(&snapshot.id())
1099                    .map(Vec::as_slice)
1100                    .unwrap_or(&[]);
1101
1102                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
1103                for (entry, repo) in
1104                    snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
1105                {
1106                    let status = (git_status_setting
1107                        && entry.path.parent().is_some()
1108                        && !entry.is_ignored)
1109                        .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
1110                        .flatten();
1111
1112                    let mut details = EntryDetails {
1113                        filename: entry
1114                            .path
1115                            .file_name()
1116                            .unwrap_or(root_name)
1117                            .to_string_lossy()
1118                            .to_string(),
1119                        path: entry.path.clone(),
1120                        depth: entry.path.components().count(),
1121                        kind: entry.kind,
1122                        is_ignored: entry.is_ignored,
1123                        is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
1124                        is_selected: self.selection.map_or(false, |e| {
1125                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
1126                        }),
1127                        is_editing: false,
1128                        is_processing: false,
1129                        is_cut: self
1130                            .clipboard_entry
1131                            .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
1132                        git_status: status,
1133                    };
1134
1135                    if let Some(edit_state) = &self.edit_state {
1136                        let is_edited_entry = if edit_state.is_new_entry {
1137                            entry.id == NEW_ENTRY_ID
1138                        } else {
1139                            entry.id == edit_state.entry_id
1140                        };
1141
1142                        if is_edited_entry {
1143                            if let Some(processing_filename) = &edit_state.processing_filename {
1144                                details.is_processing = true;
1145                                details.filename.clear();
1146                                details.filename.push_str(processing_filename);
1147                            } else {
1148                                if edit_state.is_new_entry {
1149                                    details.filename.clear();
1150                                }
1151                                details.is_editing = true;
1152                            }
1153                        }
1154                    }
1155
1156                    callback(entry.id, details, cx);
1157                }
1158            }
1159            ix = end_ix;
1160        }
1161    }
1162
1163    fn render_entry_visual_element<V: View>(
1164        details: &EntryDetails,
1165        editor: Option<&ViewHandle<Editor>>,
1166        padding: f32,
1167        row_container_style: ContainerStyle,
1168        style: &ProjectPanelEntry,
1169        cx: &mut ViewContext<V>,
1170    ) -> AnyElement<V> {
1171        let kind = details.kind;
1172        let show_editor = details.is_editing && !details.is_processing;
1173
1174        let mut filename_text_style = style.text.clone();
1175        filename_text_style.color = details
1176            .git_status
1177            .as_ref()
1178            .map(|status| match status {
1179                GitFileStatus::Added => style.status.git.inserted,
1180                GitFileStatus::Modified => style.status.git.modified,
1181                GitFileStatus::Conflict => style.status.git.conflict,
1182            })
1183            .unwrap_or(style.text.color);
1184
1185        Flex::row()
1186            .with_child(
1187                if kind == EntryKind::Dir {
1188                    if details.is_expanded {
1189                        Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
1190                    } else {
1191                        Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
1192                    }
1193                    .constrained()
1194                } else {
1195                    Empty::new().constrained()
1196                }
1197                .with_max_width(style.icon_size)
1198                .with_max_height(style.icon_size)
1199                .aligned()
1200                .constrained()
1201                .with_width(style.icon_size),
1202            )
1203            .with_child(if show_editor && editor.is_some() {
1204                ChildView::new(editor.as_ref().unwrap(), cx)
1205                    .contained()
1206                    .with_margin_left(style.icon_spacing)
1207                    .aligned()
1208                    .left()
1209                    .flex(1.0, true)
1210                    .into_any()
1211            } else {
1212                Label::new(details.filename.clone(), filename_text_style)
1213                    .contained()
1214                    .with_margin_left(style.icon_spacing)
1215                    .aligned()
1216                    .left()
1217                    .into_any()
1218            })
1219            .constrained()
1220            .with_height(style.height)
1221            .contained()
1222            .with_style(row_container_style)
1223            .with_padding_left(padding)
1224            .into_any_named("project panel entry visual element")
1225    }
1226
1227    fn render_entry(
1228        entry_id: ProjectEntryId,
1229        details: EntryDetails,
1230        editor: &ViewHandle<Editor>,
1231        dragged_entry_destination: &mut Option<Arc<Path>>,
1232        theme: &theme::ProjectPanel,
1233        cx: &mut ViewContext<Self>,
1234    ) -> AnyElement<Self> {
1235        let kind = details.kind;
1236        let path = details.path.clone();
1237        let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
1238
1239        let entry_style = if details.is_cut {
1240            &theme.cut_entry
1241        } else if details.is_ignored {
1242            &theme.ignored_entry
1243        } else {
1244            &theme.entry
1245        };
1246
1247        let show_editor = details.is_editing && !details.is_processing;
1248
1249        MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
1250            let mut style = entry_style.style_for(state, details.is_selected).clone();
1251
1252            if cx
1253                .global::<DragAndDrop<Workspace>>()
1254                .currently_dragged::<ProjectEntryId>(cx.window_id())
1255                .is_some()
1256                && dragged_entry_destination
1257                    .as_ref()
1258                    .filter(|destination| details.path.starts_with(destination))
1259                    .is_some()
1260            {
1261                style = entry_style.active.clone().unwrap();
1262            }
1263
1264            let row_container_style = if show_editor {
1265                theme.filename_editor.container
1266            } else {
1267                style.container
1268            };
1269
1270            Self::render_entry_visual_element(
1271                &details,
1272                Some(editor),
1273                padding,
1274                row_container_style,
1275                &style,
1276                cx,
1277            )
1278        })
1279        .on_click(MouseButton::Left, move |event, this, cx| {
1280            if !show_editor {
1281                if kind == EntryKind::Dir {
1282                    this.toggle_expanded(entry_id, cx);
1283                } else {
1284                    this.open_entry(entry_id, event.click_count > 1, cx);
1285                }
1286            }
1287        })
1288        .on_down(MouseButton::Right, move |event, this, cx| {
1289            this.deploy_context_menu(event.position, entry_id, cx);
1290        })
1291        .on_up(MouseButton::Left, move |_, this, cx| {
1292            if let Some((_, dragged_entry)) = cx
1293                .global::<DragAndDrop<Workspace>>()
1294                .currently_dragged::<ProjectEntryId>(cx.window_id())
1295            {
1296                this.move_entry(
1297                    *dragged_entry,
1298                    entry_id,
1299                    matches!(details.kind, EntryKind::File(_)),
1300                    cx,
1301                );
1302            }
1303        })
1304        .on_move(move |_, this, cx| {
1305            if cx
1306                .global::<DragAndDrop<Workspace>>()
1307                .currently_dragged::<ProjectEntryId>(cx.window_id())
1308                .is_some()
1309            {
1310                this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
1311                    path.parent().map(|parent| Arc::from(parent))
1312                } else {
1313                    Some(path.clone())
1314                };
1315            }
1316        })
1317        .as_draggable(entry_id, {
1318            let row_container_style = theme.dragged_entry.container;
1319
1320            move |_, cx: &mut ViewContext<Workspace>| {
1321                let theme = theme::current(cx).clone();
1322                Self::render_entry_visual_element(
1323                    &details,
1324                    None,
1325                    padding,
1326                    row_container_style,
1327                    &theme.project_panel.dragged_entry,
1328                    cx,
1329                )
1330            }
1331        })
1332        .with_cursor_style(CursorStyle::PointingHand)
1333        .into_any_named("project panel entry")
1334    }
1335}
1336
1337impl View for ProjectPanel {
1338    fn ui_name() -> &'static str {
1339        "ProjectPanel"
1340    }
1341
1342    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
1343        enum ProjectPanel {}
1344        let theme = &theme::current(cx).project_panel;
1345        let mut container_style = theme.container;
1346        let padding = std::mem::take(&mut container_style.padding);
1347        let last_worktree_root_id = self.last_worktree_root_id;
1348
1349        let has_worktree = self.visible_entries.len() != 0;
1350
1351        if has_worktree {
1352            Stack::new()
1353                .with_child(
1354                    MouseEventHandler::<ProjectPanel, _>::new(0, cx, |_, cx| {
1355                        UniformList::new(
1356                            self.list.clone(),
1357                            self.visible_entries
1358                                .iter()
1359                                .map(|(_, worktree_entries)| worktree_entries.len())
1360                                .sum(),
1361                            cx,
1362                            move |this, range, items, cx| {
1363                                let theme = theme::current(cx).clone();
1364                                let mut dragged_entry_destination =
1365                                    this.dragged_entry_destination.clone();
1366                                this.for_each_visible_entry(range, cx, |id, details, cx| {
1367                                    items.push(Self::render_entry(
1368                                        id,
1369                                        details,
1370                                        &this.filename_editor,
1371                                        &mut dragged_entry_destination,
1372                                        &theme.project_panel,
1373                                        cx,
1374                                    ));
1375                                });
1376                                this.dragged_entry_destination = dragged_entry_destination;
1377                            },
1378                        )
1379                        .with_padding_top(padding.top)
1380                        .with_padding_bottom(padding.bottom)
1381                        .contained()
1382                        .with_style(container_style)
1383                        .expanded()
1384                    })
1385                    .on_down(MouseButton::Right, move |event, this, cx| {
1386                        // When deploying the context menu anywhere below the last project entry,
1387                        // act as if the user clicked the root of the last worktree.
1388                        if let Some(entry_id) = last_worktree_root_id {
1389                            this.deploy_context_menu(event.position, entry_id, cx);
1390                        }
1391                    }),
1392                )
1393                .with_child(ChildView::new(&self.context_menu, cx))
1394                .into_any_named("project panel")
1395        } else {
1396            Flex::column()
1397                .with_child(
1398                    MouseEventHandler::<Self, _>::new(2, cx, {
1399                        let button_style = theme.open_project_button.clone();
1400                        let context_menu_item_style = theme::current(cx).context_menu.item.clone();
1401                        move |state, cx| {
1402                            let button_style = button_style.style_for(state, false).clone();
1403                            let context_menu_item =
1404                                context_menu_item_style.style_for(state, true).clone();
1405
1406                            theme::ui::keystroke_label(
1407                                "Open a project",
1408                                &button_style,
1409                                &context_menu_item.keystroke,
1410                                Box::new(workspace::Open),
1411                                cx,
1412                            )
1413                        }
1414                    })
1415                    .on_click(MouseButton::Left, move |_, this, cx| {
1416                        if let Some(workspace) = this.workspace.upgrade(cx) {
1417                            workspace.update(cx, |workspace, cx| {
1418                                if let Some(task) = workspace.open(&Default::default(), cx) {
1419                                    task.detach_and_log_err(cx);
1420                                }
1421                            })
1422                        }
1423                    })
1424                    .with_cursor_style(CursorStyle::PointingHand),
1425                )
1426                .contained()
1427                .with_style(container_style)
1428                .into_any_named("empty project panel")
1429        }
1430    }
1431
1432    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
1433        Self::reset_to_default_keymap_context(keymap);
1434        keymap.add_identifier("menu");
1435    }
1436
1437    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1438        if !self.has_focus {
1439            self.has_focus = true;
1440            cx.emit(Event::Focus);
1441        }
1442    }
1443
1444    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
1445        self.has_focus = false;
1446    }
1447}
1448
1449impl Entity for ProjectPanel {
1450    type Event = Event;
1451}
1452
1453impl workspace::dock::Panel for ProjectPanel {
1454    fn position(&self, cx: &WindowContext) -> DockPosition {
1455        match settings::get::<ProjectPanelSettings>(cx).dock {
1456            ProjectPanelDockPosition::Left => DockPosition::Left,
1457            ProjectPanelDockPosition::Right => DockPosition::Right,
1458        }
1459    }
1460
1461    fn position_is_valid(&self, position: DockPosition) -> bool {
1462        matches!(position, DockPosition::Left | DockPosition::Right)
1463    }
1464
1465    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1466        settings::update_settings_file::<ProjectPanelSettings>(
1467            self.fs.clone(),
1468            cx,
1469            move |settings| {
1470                let dock = match position {
1471                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
1472                    DockPosition::Right => ProjectPanelDockPosition::Right,
1473                };
1474                settings.dock = Some(dock);
1475            },
1476        );
1477    }
1478
1479    fn size(&self, cx: &WindowContext) -> f32 {
1480        self.width
1481            .unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
1482    }
1483
1484    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
1485        self.width = Some(size);
1486        self.serialize(cx);
1487        cx.notify();
1488    }
1489
1490    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
1491        false
1492    }
1493
1494    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
1495        false
1496    }
1497
1498    fn is_zoomed(&self, _: &WindowContext) -> bool {
1499        false
1500    }
1501
1502    fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
1503
1504    fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
1505
1506    fn icon_path(&self) -> &'static str {
1507        "icons/folder_tree_16.svg"
1508    }
1509
1510    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
1511        ("Project Panel".into(), Some(Box::new(ToggleFocus)))
1512    }
1513
1514    fn should_change_position_on_event(event: &Self::Event) -> bool {
1515        matches!(event, Event::DockPositionChanged)
1516    }
1517
1518    fn should_activate_on_event(_: &Self::Event) -> bool {
1519        false
1520    }
1521
1522    fn should_close_on_event(_: &Self::Event) -> bool {
1523        false
1524    }
1525
1526    fn has_focus(&self, _: &WindowContext) -> bool {
1527        self.has_focus
1528    }
1529
1530    fn is_focus_event(event: &Self::Event) -> bool {
1531        matches!(event, Event::Focus)
1532    }
1533}
1534
1535impl ClipboardEntry {
1536    fn is_cut(&self) -> bool {
1537        matches!(self, Self::Cut { .. })
1538    }
1539
1540    fn entry_id(&self) -> ProjectEntryId {
1541        match self {
1542            ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
1543                *entry_id
1544            }
1545        }
1546    }
1547
1548    fn worktree_id(&self) -> WorktreeId {
1549        match self {
1550            ClipboardEntry::Copied { worktree_id, .. }
1551            | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
1552        }
1553    }
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558    use super::*;
1559    use gpui::{TestAppContext, ViewHandle};
1560    use project::FakeFs;
1561    use serde_json::json;
1562    use settings::SettingsStore;
1563    use std::{collections::HashSet, path::Path};
1564    use workspace::{pane, AppState};
1565
1566    #[gpui::test]
1567    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
1568        init_test(cx);
1569
1570        let fs = FakeFs::new(cx.background());
1571        fs.insert_tree(
1572            "/root1",
1573            json!({
1574                ".dockerignore": "",
1575                ".git": {
1576                    "HEAD": "",
1577                },
1578                "a": {
1579                    "0": { "q": "", "r": "", "s": "" },
1580                    "1": { "t": "", "u": "" },
1581                    "2": { "v": "", "w": "", "x": "", "y": "" },
1582                },
1583                "b": {
1584                    "3": { "Q": "" },
1585                    "4": { "R": "", "S": "", "T": "", "U": "" },
1586                },
1587                "C": {
1588                    "5": {},
1589                    "6": { "V": "", "W": "" },
1590                    "7": { "X": "" },
1591                    "8": { "Y": {}, "Z": "" }
1592                }
1593            }),
1594        )
1595        .await;
1596        fs.insert_tree(
1597            "/root2",
1598            json!({
1599                "d": {
1600                    "9": ""
1601                },
1602                "e": {}
1603            }),
1604        )
1605        .await;
1606
1607        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1608        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1609        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1610        assert_eq!(
1611            visible_entries_as_strings(&panel, 0..50, cx),
1612            &[
1613                "v root1",
1614                "    > .git",
1615                "    > a",
1616                "    > b",
1617                "    > C",
1618                "      .dockerignore",
1619                "v root2",
1620                "    > d",
1621                "    > e",
1622            ]
1623        );
1624
1625        toggle_expand_dir(&panel, "root1/b", cx);
1626        assert_eq!(
1627            visible_entries_as_strings(&panel, 0..50, cx),
1628            &[
1629                "v root1",
1630                "    > .git",
1631                "    > a",
1632                "    v b  <== selected",
1633                "        > 3",
1634                "        > 4",
1635                "    > C",
1636                "      .dockerignore",
1637                "v root2",
1638                "    > d",
1639                "    > e",
1640            ]
1641        );
1642
1643        assert_eq!(
1644            visible_entries_as_strings(&panel, 6..9, cx),
1645            &[
1646                //
1647                "    > C",
1648                "      .dockerignore",
1649                "v root2",
1650            ]
1651        );
1652    }
1653
1654    #[gpui::test(iterations = 30)]
1655    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
1656        init_test(cx);
1657
1658        let fs = FakeFs::new(cx.background());
1659        fs.insert_tree(
1660            "/root1",
1661            json!({
1662                ".dockerignore": "",
1663                ".git": {
1664                    "HEAD": "",
1665                },
1666                "a": {
1667                    "0": { "q": "", "r": "", "s": "" },
1668                    "1": { "t": "", "u": "" },
1669                    "2": { "v": "", "w": "", "x": "", "y": "" },
1670                },
1671                "b": {
1672                    "3": { "Q": "" },
1673                    "4": { "R": "", "S": "", "T": "", "U": "" },
1674                },
1675                "C": {
1676                    "5": {},
1677                    "6": { "V": "", "W": "" },
1678                    "7": { "X": "" },
1679                    "8": { "Y": {}, "Z": "" }
1680                }
1681            }),
1682        )
1683        .await;
1684        fs.insert_tree(
1685            "/root2",
1686            json!({
1687                "d": {
1688                    "9": ""
1689                },
1690                "e": {}
1691            }),
1692        )
1693        .await;
1694
1695        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
1696        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1697        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1698
1699        select_path(&panel, "root1", cx);
1700        assert_eq!(
1701            visible_entries_as_strings(&panel, 0..10, cx),
1702            &[
1703                "v root1  <== selected",
1704                "    > .git",
1705                "    > a",
1706                "    > b",
1707                "    > C",
1708                "      .dockerignore",
1709                "v root2",
1710                "    > d",
1711                "    > e",
1712            ]
1713        );
1714
1715        // Add a file with the root folder selected. The filename editor is placed
1716        // before the first file in the root folder.
1717        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1718        cx.read_window(window_id, |cx| {
1719            let panel = panel.read(cx);
1720            assert!(panel.filename_editor.is_focused(cx));
1721        });
1722        assert_eq!(
1723            visible_entries_as_strings(&panel, 0..10, cx),
1724            &[
1725                "v root1",
1726                "    > .git",
1727                "    > a",
1728                "    > b",
1729                "    > C",
1730                "      [EDITOR: '']  <== selected",
1731                "      .dockerignore",
1732                "v root2",
1733                "    > d",
1734                "    > e",
1735            ]
1736        );
1737
1738        let confirm = panel.update(cx, |panel, cx| {
1739            panel
1740                .filename_editor
1741                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
1742            panel.confirm(&Confirm, cx).unwrap()
1743        });
1744        assert_eq!(
1745            visible_entries_as_strings(&panel, 0..10, cx),
1746            &[
1747                "v root1",
1748                "    > .git",
1749                "    > a",
1750                "    > b",
1751                "    > C",
1752                "      [PROCESSING: 'the-new-filename']  <== selected",
1753                "      .dockerignore",
1754                "v root2",
1755                "    > d",
1756                "    > e",
1757            ]
1758        );
1759
1760        confirm.await.unwrap();
1761        assert_eq!(
1762            visible_entries_as_strings(&panel, 0..10, cx),
1763            &[
1764                "v root1",
1765                "    > .git",
1766                "    > a",
1767                "    > b",
1768                "    > C",
1769                "      .dockerignore",
1770                "      the-new-filename  <== selected",
1771                "v root2",
1772                "    > d",
1773                "    > e",
1774            ]
1775        );
1776
1777        select_path(&panel, "root1/b", cx);
1778        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
1779        assert_eq!(
1780            visible_entries_as_strings(&panel, 0..10, cx),
1781            &[
1782                "v root1",
1783                "    > .git",
1784                "    > a",
1785                "    v b",
1786                "        > 3",
1787                "        > 4",
1788                "          [EDITOR: '']  <== selected",
1789                "    > C",
1790                "      .dockerignore",
1791                "      the-new-filename",
1792            ]
1793        );
1794
1795        panel
1796            .update(cx, |panel, cx| {
1797                panel
1798                    .filename_editor
1799                    .update(cx, |editor, cx| editor.set_text("another-filename", cx));
1800                panel.confirm(&Confirm, cx).unwrap()
1801            })
1802            .await
1803            .unwrap();
1804        assert_eq!(
1805            visible_entries_as_strings(&panel, 0..10, cx),
1806            &[
1807                "v root1",
1808                "    > .git",
1809                "    > a",
1810                "    v b",
1811                "        > 3",
1812                "        > 4",
1813                "          another-filename  <== selected",
1814                "    > C",
1815                "      .dockerignore",
1816                "      the-new-filename",
1817            ]
1818        );
1819
1820        select_path(&panel, "root1/b/another-filename", cx);
1821        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
1822        assert_eq!(
1823            visible_entries_as_strings(&panel, 0..10, cx),
1824            &[
1825                "v root1",
1826                "    > .git",
1827                "    > a",
1828                "    v b",
1829                "        > 3",
1830                "        > 4",
1831                "          [EDITOR: 'another-filename']  <== selected",
1832                "    > C",
1833                "      .dockerignore",
1834                "      the-new-filename",
1835            ]
1836        );
1837
1838        let confirm = panel.update(cx, |panel, cx| {
1839            panel
1840                .filename_editor
1841                .update(cx, |editor, cx| editor.set_text("a-different-filename", cx));
1842            panel.confirm(&Confirm, cx).unwrap()
1843        });
1844        assert_eq!(
1845            visible_entries_as_strings(&panel, 0..10, cx),
1846            &[
1847                "v root1",
1848                "    > .git",
1849                "    > a",
1850                "    v b",
1851                "        > 3",
1852                "        > 4",
1853                "          [PROCESSING: 'a-different-filename']  <== selected",
1854                "    > C",
1855                "      .dockerignore",
1856                "      the-new-filename",
1857            ]
1858        );
1859
1860        confirm.await.unwrap();
1861        assert_eq!(
1862            visible_entries_as_strings(&panel, 0..10, cx),
1863            &[
1864                "v root1",
1865                "    > .git",
1866                "    > a",
1867                "    v b",
1868                "        > 3",
1869                "        > 4",
1870                "          a-different-filename  <== selected",
1871                "    > C",
1872                "      .dockerignore",
1873                "      the-new-filename",
1874            ]
1875        );
1876
1877        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
1878        assert_eq!(
1879            visible_entries_as_strings(&panel, 0..10, cx),
1880            &[
1881                "v root1",
1882                "    > .git",
1883                "    > a",
1884                "    v b",
1885                "        > [EDITOR: '']  <== selected",
1886                "        > 3",
1887                "        > 4",
1888                "          a-different-filename",
1889                "    > C",
1890                "      .dockerignore",
1891            ]
1892        );
1893
1894        let confirm = panel.update(cx, |panel, cx| {
1895            panel
1896                .filename_editor
1897                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
1898            panel.confirm(&Confirm, cx).unwrap()
1899        });
1900        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
1901        assert_eq!(
1902            visible_entries_as_strings(&panel, 0..10, cx),
1903            &[
1904                "v root1",
1905                "    > .git",
1906                "    > a",
1907                "    v b",
1908                "        > [PROCESSING: 'new-dir']",
1909                "        > 3  <== selected",
1910                "        > 4",
1911                "          a-different-filename",
1912                "    > C",
1913                "      .dockerignore",
1914            ]
1915        );
1916
1917        confirm.await.unwrap();
1918        assert_eq!(
1919            visible_entries_as_strings(&panel, 0..10, cx),
1920            &[
1921                "v root1",
1922                "    > .git",
1923                "    > a",
1924                "    v b",
1925                "        > 3  <== selected",
1926                "        > 4",
1927                "        > new-dir",
1928                "          a-different-filename",
1929                "    > C",
1930                "      .dockerignore",
1931            ]
1932        );
1933
1934        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
1935        assert_eq!(
1936            visible_entries_as_strings(&panel, 0..10, cx),
1937            &[
1938                "v root1",
1939                "    > .git",
1940                "    > a",
1941                "    v b",
1942                "        > [EDITOR: '3']  <== selected",
1943                "        > 4",
1944                "        > new-dir",
1945                "          a-different-filename",
1946                "    > C",
1947                "      .dockerignore",
1948            ]
1949        );
1950
1951        // Dismiss the rename editor when it loses focus.
1952        workspace.update(cx, |_, cx| cx.focus_self());
1953        assert_eq!(
1954            visible_entries_as_strings(&panel, 0..10, cx),
1955            &[
1956                "v root1",
1957                "    > .git",
1958                "    > a",
1959                "    v b",
1960                "        > 3  <== selected",
1961                "        > 4",
1962                "        > new-dir",
1963                "          a-different-filename",
1964                "    > C",
1965                "      .dockerignore",
1966            ]
1967        );
1968    }
1969
1970    #[gpui::test]
1971    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
1972        init_test(cx);
1973
1974        let fs = FakeFs::new(cx.background());
1975        fs.insert_tree(
1976            "/root1",
1977            json!({
1978                "one.two.txt": "",
1979                "one.txt": ""
1980            }),
1981        )
1982        .await;
1983
1984        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1985        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1986        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
1987
1988        panel.update(cx, |panel, cx| {
1989            panel.select_next(&Default::default(), cx);
1990            panel.select_next(&Default::default(), cx);
1991        });
1992
1993        assert_eq!(
1994            visible_entries_as_strings(&panel, 0..50, cx),
1995            &[
1996                //
1997                "v root1",
1998                "      one.two.txt  <== selected",
1999                "      one.txt",
2000            ]
2001        );
2002
2003        // Regression test - file name is created correctly when
2004        // the copied file's name contains multiple dots.
2005        panel.update(cx, |panel, cx| {
2006            panel.copy(&Default::default(), cx);
2007            panel.paste(&Default::default(), cx);
2008        });
2009        cx.foreground().run_until_parked();
2010
2011        assert_eq!(
2012            visible_entries_as_strings(&panel, 0..50, cx),
2013            &[
2014                //
2015                "v root1",
2016                "      one.two copy.txt",
2017                "      one.two.txt  <== selected",
2018                "      one.txt",
2019            ]
2020        );
2021
2022        panel.update(cx, |panel, cx| {
2023            panel.paste(&Default::default(), cx);
2024        });
2025        cx.foreground().run_until_parked();
2026
2027        assert_eq!(
2028            visible_entries_as_strings(&panel, 0..50, cx),
2029            &[
2030                //
2031                "v root1",
2032                "      one.two copy 1.txt",
2033                "      one.two copy.txt",
2034                "      one.two.txt  <== selected",
2035                "      one.txt",
2036            ]
2037        );
2038    }
2039
2040    #[gpui::test]
2041    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
2042        init_test_with_editor(cx);
2043
2044        let fs = FakeFs::new(cx.background());
2045        fs.insert_tree(
2046            "/src",
2047            json!({
2048                "test": {
2049                    "first.rs": "// First Rust file",
2050                    "second.rs": "// Second Rust file",
2051                    "third.rs": "// Third Rust file",
2052                }
2053            }),
2054        )
2055        .await;
2056
2057        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
2058        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2059        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
2060
2061        toggle_expand_dir(&panel, "src/test", cx);
2062        select_path(&panel, "src/test/first.rs", cx);
2063        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2064        cx.foreground().run_until_parked();
2065        assert_eq!(
2066            visible_entries_as_strings(&panel, 0..10, cx),
2067            &[
2068                "v src",
2069                "    v test",
2070                "          first.rs  <== selected",
2071                "          second.rs",
2072                "          third.rs"
2073            ]
2074        );
2075        ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
2076
2077        submit_deletion(window_id, &panel, cx);
2078        assert_eq!(
2079            visible_entries_as_strings(&panel, 0..10, cx),
2080            &[
2081                "v src",
2082                "    v test",
2083                "          second.rs",
2084                "          third.rs"
2085            ],
2086            "Project panel should have no deleted file, no other file is selected in it"
2087        );
2088        ensure_no_open_items_and_panes(window_id, &workspace, cx);
2089
2090        select_path(&panel, "src/test/second.rs", cx);
2091        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
2092        cx.foreground().run_until_parked();
2093        assert_eq!(
2094            visible_entries_as_strings(&panel, 0..10, cx),
2095            &[
2096                "v src",
2097                "    v test",
2098                "          second.rs  <== selected",
2099                "          third.rs"
2100            ]
2101        );
2102        ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
2103
2104        cx.update_window(window_id, |cx| {
2105            let active_items = workspace
2106                .read(cx)
2107                .panes()
2108                .iter()
2109                .filter_map(|pane| pane.read(cx).active_item())
2110                .collect::<Vec<_>>();
2111            assert_eq!(active_items.len(), 1);
2112            let open_editor = active_items
2113                .into_iter()
2114                .next()
2115                .unwrap()
2116                .downcast::<Editor>()
2117                .expect("Open item should be an editor");
2118            open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
2119        });
2120        submit_deletion(window_id, &panel, cx);
2121        assert_eq!(
2122            visible_entries_as_strings(&panel, 0..10, cx),
2123            &["v src", "    v test", "          third.rs"],
2124            "Project panel should have no deleted file, with one last file remaining"
2125        );
2126        ensure_no_open_items_and_panes(window_id, &workspace, cx);
2127    }
2128
2129    fn toggle_expand_dir(
2130        panel: &ViewHandle<ProjectPanel>,
2131        path: impl AsRef<Path>,
2132        cx: &mut TestAppContext,
2133    ) {
2134        let path = path.as_ref();
2135        panel.update(cx, |panel, cx| {
2136            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
2137                let worktree = worktree.read(cx);
2138                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2139                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2140                    panel.toggle_expanded(entry_id, cx);
2141                    return;
2142                }
2143            }
2144            panic!("no worktree for path {:?}", path);
2145        });
2146    }
2147
2148    fn select_path(
2149        panel: &ViewHandle<ProjectPanel>,
2150        path: impl AsRef<Path>,
2151        cx: &mut TestAppContext,
2152    ) {
2153        let path = path.as_ref();
2154        panel.update(cx, |panel, cx| {
2155            for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
2156                let worktree = worktree.read(cx);
2157                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
2158                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
2159                    panel.selection = Some(Selection {
2160                        worktree_id: worktree.id(),
2161                        entry_id,
2162                    });
2163                    return;
2164                }
2165            }
2166            panic!("no worktree for path {:?}", path);
2167        });
2168    }
2169
2170    fn visible_entries_as_strings(
2171        panel: &ViewHandle<ProjectPanel>,
2172        range: Range<usize>,
2173        cx: &mut TestAppContext,
2174    ) -> Vec<String> {
2175        let mut result = Vec::new();
2176        let mut project_entries = HashSet::new();
2177        let mut has_editor = false;
2178
2179        panel.update(cx, |panel, cx| {
2180            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
2181                if details.is_editing {
2182                    assert!(!has_editor, "duplicate editor entry");
2183                    has_editor = true;
2184                } else {
2185                    assert!(
2186                        project_entries.insert(project_entry),
2187                        "duplicate project entry {:?} {:?}",
2188                        project_entry,
2189                        details
2190                    );
2191                }
2192
2193                let indent = "    ".repeat(details.depth);
2194                let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) {
2195                    if details.is_expanded {
2196                        "v "
2197                    } else {
2198                        "> "
2199                    }
2200                } else {
2201                    "  "
2202                };
2203                let name = if details.is_editing {
2204                    format!("[EDITOR: '{}']", details.filename)
2205                } else if details.is_processing {
2206                    format!("[PROCESSING: '{}']", details.filename)
2207                } else {
2208                    details.filename.clone()
2209                };
2210                let selected = if details.is_selected {
2211                    "  <== selected"
2212                } else {
2213                    ""
2214                };
2215                result.push(format!("{indent}{icon}{name}{selected}"));
2216            });
2217        });
2218
2219        result
2220    }
2221
2222    fn init_test(cx: &mut TestAppContext) {
2223        cx.foreground().forbid_parking();
2224        cx.update(|cx| {
2225            cx.set_global(SettingsStore::test(cx));
2226            init_settings(cx);
2227            theme::init((), cx);
2228            language::init(cx);
2229            editor::init_settings(cx);
2230            crate::init(cx);
2231            workspace::init_settings(cx);
2232        });
2233    }
2234
2235    fn init_test_with_editor(cx: &mut TestAppContext) {
2236        cx.foreground().forbid_parking();
2237        cx.update(|cx| {
2238            let app_state = AppState::test(cx);
2239            theme::init((), cx);
2240            init_settings(cx);
2241            language::init(cx);
2242            editor::init(cx);
2243            pane::init(cx);
2244            crate::init(cx);
2245            workspace::init(app_state.clone(), cx);
2246        });
2247    }
2248
2249    fn ensure_single_file_is_opened(
2250        window_id: usize,
2251        workspace: &ViewHandle<Workspace>,
2252        expected_path: &str,
2253        cx: &mut TestAppContext,
2254    ) {
2255        cx.read_window(window_id, |cx| {
2256            let workspace = workspace.read(cx);
2257            let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
2258            assert_eq!(worktrees.len(), 1);
2259            let worktree_id = WorktreeId::from_usize(worktrees[0].id());
2260
2261            let open_project_paths = workspace
2262                .panes()
2263                .iter()
2264                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2265                .collect::<Vec<_>>();
2266            assert_eq!(
2267                open_project_paths,
2268                vec![ProjectPath {
2269                    worktree_id,
2270                    path: Arc::from(Path::new(expected_path))
2271                }],
2272                "Should have opened file, selected in project panel"
2273            );
2274        });
2275    }
2276
2277    fn submit_deletion(
2278        window_id: usize,
2279        panel: &ViewHandle<ProjectPanel>,
2280        cx: &mut TestAppContext,
2281    ) {
2282        assert!(
2283            !cx.has_pending_prompt(window_id),
2284            "Should have no prompts before the deletion"
2285        );
2286        panel.update(cx, |panel, cx| {
2287            panel
2288                .delete(&Delete, cx)
2289                .expect("Deletion start")
2290                .detach_and_log_err(cx);
2291        });
2292        assert!(
2293            cx.has_pending_prompt(window_id),
2294            "Should have a prompt after the deletion"
2295        );
2296        cx.simulate_prompt_answer(window_id, 0);
2297        assert!(
2298            !cx.has_pending_prompt(window_id),
2299            "Should have no prompts after prompt was replied to"
2300        );
2301        cx.foreground().run_until_parked();
2302    }
2303
2304    fn ensure_no_open_items_and_panes(
2305        window_id: usize,
2306        workspace: &ViewHandle<Workspace>,
2307        cx: &mut TestAppContext,
2308    ) {
2309        assert!(
2310            !cx.has_pending_prompt(window_id),
2311            "Should have no prompts after deletion operation closes the file"
2312        );
2313        cx.read_window(window_id, |cx| {
2314            let open_project_paths = workspace
2315                .read(cx)
2316                .panes()
2317                .iter()
2318                .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
2319                .collect::<Vec<_>>();
2320            assert!(
2321                open_project_paths.is_empty(),
2322                "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
2323            );
2324        });
2325    }
2326}