project_panel.rs

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