Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:16:53.294Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

project_panel.rs

7746 lines · 319.0 KB · rust
1pub mod project_panel_settings;
2mod undo;
3mod utils;
4
5use anyhow::{Context as _, Result};
6use client::{ErrorCode, ErrorExt};
7use collections::{BTreeSet, HashMap, hash_map};
8use editor::{
9    Editor, EditorEvent, MultiBufferOffset,
10    items::{
11        entry_diagnostic_aware_icon_decoration_and_color,
12        entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
13    },
14};
15use feature_flags::{FeatureFlagAppExt, ProjectPanelUndoRedoFeatureFlag};
16use file_icons::FileIcons;
17use fs::TrashId;
18use git;
19use git::status::GitSummary;
20use git_ui;
21use git_ui::file_diff_view::FileDiffView;
22use gpui::{
23    Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardEntry as GpuiClipboardEntry,
24    ClipboardItem, Context, CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter,
25    ExternalPaths, FocusHandle, Focusable, FontWeight, Hsla, InteractiveElement, KeyContext,
26    ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
27    MouseButton, MouseDownEvent, MouseExitEvent, ParentElement, PathPromptOptions, Pixels, Point,
28    PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task,
29    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, hsla,
30    linear_color_stop, linear_gradient, point, px, size, transparent_white, uniform_list,
31};
32use language::DiagnosticSeverity;
33use markdown_preview::markdown_preview_view::MarkdownPreviewView;
34use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
35use notifications::status_toast::StatusToast;
36use project::{
37    Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
38    ProjectPath, Worktree, WorktreeId,
39    git_store::{GitStoreEvent, RepositoryEvent, git_traversal::ChildEntriesGitIter},
40    project_settings::GoToDiagnosticSeverityFilter,
41};
42use project_panel_settings::ProjectPanelSettings;
43use rayon::slice::ParallelSliceMut;
44use schemars::JsonSchema;
45use serde::Deserialize;
46use settings::{
47    DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
48    update_settings_file,
49};
50use smallvec::SmallVec;
51use std::{
52    any::TypeId,
53    cell::OnceCell,
54    cmp,
55    collections::HashSet,
56    ops::Neg,
57    ops::Range,
58    path::{Path, PathBuf},
59    sync::Arc,
60    time::{Duration, Instant},
61};
62use theme_settings::ThemeSettings;
63use ui::{
64    ContextMenu, DecoratedIcon, IconDecoration, IconDecorationKind, IndentGuideColors,
65    IndentGuideLayout, Indicator, KeyBinding, ListItem, ListItemSpacing, ProjectEmptyState,
66    ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
67};
68use util::{
69    ResultExt, TakeUntilExt, TryFutureExt,
70    markdown::MarkdownInlineCode,
71    maybe,
72    paths::{PathStyle, compare_paths},
73    rel_path::{RelPath, RelPathBuf},
74};
75use workspace::{
76    DraggedSelection, OpenInTerminal, OpenMode, OpenOptions, OpenVisible, PreviewTabsSettings,
77    SelectedEntry, SplitDirection, Workspace, WorkspaceSettings,
78    dock::{DockPosition, Panel, PanelEvent},
79    focus_follows_mouse::FocusFollowsMouse as _,
80    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
81};
82use worktree::CreatedEntry;
83use zed_actions::{
84    project_panel::{Toggle, ToggleFocus},
85    workspace::OpenWithSystem,
86};
87
88use crate::{
89    project_panel_settings::ProjectPanelScrollbarProxy,
90    undo::{Change, UndoManager},
91};
92
93const PROJECT_PANEL_KEY: &str = "ProjectPanel";
94const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
95
96struct VisibleEntriesForWorktree {
97    worktree_id: WorktreeId,
98    entries: Vec<GitEntry>,
99    index: OnceCell<HashSet<Arc<RelPath>>>,
100}
101
102struct State {
103    last_worktree_root_id: Option<ProjectEntryId>,
104    /// Maps from leaf project entry ID to the currently selected ancestor.
105    /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
106    /// project entries (and all non-leaf nodes are guaranteed to be directories).
107    ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
108    visible_entries: Vec<VisibleEntriesForWorktree>,
109    max_width_item_index: Option<usize>,
110    edit_state: Option<EditState>,
111    temporarily_unfolded_pending_state: Option<TemporaryUnfoldedPendingState>,
112    unfolded_dir_ids: HashSet<ProjectEntryId>,
113    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
114}
115
116impl State {
117    fn is_unfolded(&self, entry_id: &ProjectEntryId) -> bool {
118        self.unfolded_dir_ids.contains(entry_id)
119            || self.edit_state.as_ref().map_or(false, |edit_state| {
120                edit_state.temporarily_unfolded == Some(*entry_id)
121            })
122    }
123
124    fn derive(old: &Self) -> Self {
125        Self {
126            last_worktree_root_id: None,
127            ancestors: Default::default(),
128            visible_entries: Default::default(),
129            max_width_item_index: None,
130            edit_state: old.edit_state.clone(),
131            temporarily_unfolded_pending_state: None,
132            unfolded_dir_ids: old.unfolded_dir_ids.clone(),
133            expanded_dir_ids: old.expanded_dir_ids.clone(),
134        }
135    }
136}
137
138pub struct ProjectPanel {
139    project: Entity<Project>,
140    fs: Arc<dyn Fs>,
141    focus_handle: FocusHandle,
142    scroll_handle: UniformListScrollHandle,
143    // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
144    // hovered over the start/end of a list.
145    hover_scroll_task: Option<Task<()>>,
146    rendered_entries_len: usize,
147    folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
148    drag_target_entry: Option<DragTarget>,
149    marked_entries: Vec<SelectedEntry>,
150    selection: Option<SelectedEntry>,
151    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
152    filename_editor: Entity<Editor>,
153    clipboard: Option<ClipboardEntry>,
154    _dragged_entry_destination: Option<Arc<Path>>,
155    workspace: WeakEntity<Workspace>,
156    diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
157    diagnostic_counts: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticCount>,
158    diagnostic_summary_update: Task<()>,
159    // We keep track of the mouse down state on entries so we don't flash the UI
160    // in case a user clicks to open a file.
161    mouse_down: bool,
162    hover_expand_task: Option<Task<()>>,
163    previous_drag_position: Option<Point<Pixels>>,
164    sticky_items_count: usize,
165    last_reported_update: Instant,
166    update_visible_entries_task: UpdateVisibleEntriesTask,
167    undo_manager: UndoManager,
168    state: State,
169}
170
171struct UpdateVisibleEntriesTask {
172    _visible_entries_task: Task<()>,
173    focus_filename_editor: bool,
174    autoscroll: bool,
175}
176
177#[derive(Debug)]
178struct TemporaryUnfoldedPendingState {
179    previously_focused_leaf_entry: SelectedEntry,
180    temporarily_unfolded_active_entry_id: ProjectEntryId,
181}
182
183impl Default for UpdateVisibleEntriesTask {
184    fn default() -> Self {
185        UpdateVisibleEntriesTask {
186            _visible_entries_task: Task::ready(()),
187            focus_filename_editor: Default::default(),
188            autoscroll: Default::default(),
189        }
190    }
191}
192
193enum DragTarget {
194    /// Dragging on an entry
195    Entry {
196        /// The entry currently under the mouse cursor during a drag operation
197        entry_id: ProjectEntryId,
198        /// Highlight this entry along with all of its children
199        highlight_entry_id: ProjectEntryId,
200    },
201    /// Dragging on background
202    Background,
203}
204
205#[derive(Copy, Clone, Debug)]
206struct FoldedDirectoryDragTarget {
207    entry_id: ProjectEntryId,
208    index: usize,
209    /// Whether we are dragging over the delimiter rather than the component itself.
210    is_delimiter_target: bool,
211}
212
213#[derive(Clone, Debug)]
214enum ValidationState {
215    None,
216    Warning(String),
217    Error(String),
218}
219
220#[derive(Clone, Debug)]
221struct EditState {
222    worktree_id: WorktreeId,
223    entry_id: ProjectEntryId,
224    leaf_entry_id: Option<ProjectEntryId>,
225    is_dir: bool,
226    depth: usize,
227    processing_filename: Option<Arc<RelPath>>,
228    previously_focused: Option<SelectedEntry>,
229    validation_state: ValidationState,
230    temporarily_unfolded: Option<ProjectEntryId>,
231}
232
233impl EditState {
234    fn is_new_entry(&self) -> bool {
235        self.leaf_entry_id.is_none()
236    }
237}
238
239#[derive(Clone, Debug)]
240enum ClipboardEntry {
241    Copied(BTreeSet<SelectedEntry>),
242    Cut(BTreeSet<SelectedEntry>),
243}
244
245#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
246struct DiagnosticCount {
247    error_count: usize,
248    warning_count: usize,
249}
250
251impl DiagnosticCount {
252    fn capped_error_count(&self) -> String {
253        Self::capped_count(self.error_count)
254    }
255
256    fn capped_warning_count(&self) -> String {
257        Self::capped_count(self.warning_count)
258    }
259
260    fn capped_count(count: usize) -> String {
261        if count > 99 {
262            "99+".to_string()
263        } else {
264            count.to_string()
265        }
266    }
267}
268
269#[derive(Debug, PartialEq, Eq, Clone)]
270struct EntryDetails {
271    filename: String,
272    icon: Option<SharedString>,
273    path: Arc<RelPath>,
274    depth: usize,
275    kind: EntryKind,
276    is_ignored: bool,
277    is_expanded: bool,
278    is_selected: bool,
279    is_marked: bool,
280    is_editing: bool,
281    is_processing: bool,
282    is_cut: bool,
283    sticky: Option<StickyDetails>,
284    filename_text_color: Color,
285    diagnostic_severity: Option<DiagnosticSeverity>,
286    diagnostic_count: Option<DiagnosticCount>,
287    git_status: GitSummary,
288    is_private: bool,
289    worktree_id: WorktreeId,
290    canonical_path: Option<Arc<Path>>,
291}
292
293#[derive(Debug, PartialEq, Eq, Clone)]
294struct StickyDetails {
295    sticky_index: usize,
296}
297
298/// Permanently deletes the selected file or directory.
299#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
300#[action(namespace = project_panel)]
301#[serde(deny_unknown_fields)]
302struct Delete {
303    #[serde(default)]
304    pub skip_prompt: bool,
305}
306
307/// Moves the selected file or directory to the system trash.
308#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
309#[action(namespace = project_panel)]
310#[serde(deny_unknown_fields)]
311struct Trash {
312    #[serde(default)]
313    pub skip_prompt: bool,
314}
315
316/// Selects the next entry with diagnostics.
317#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
318#[action(namespace = project_panel)]
319#[serde(deny_unknown_fields)]
320struct SelectNextDiagnostic {
321    #[serde(default)]
322    pub severity: GoToDiagnosticSeverityFilter,
323}
324
325/// Selects the previous entry with diagnostics.
326#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
327#[action(namespace = project_panel)]
328#[serde(deny_unknown_fields)]
329struct SelectPrevDiagnostic {
330    #[serde(default)]
331    pub severity: GoToDiagnosticSeverityFilter,
332}
333
334actions!(
335    project_panel,
336    [
337        /// Expands the selected entry in the project tree.
338        ExpandSelectedEntry,
339        /// Collapses the selected entry in the project tree.
340        CollapseSelectedEntry,
341        /// Collapses the selected entry and its children in the project tree.
342        CollapseSelectedEntryAndChildren,
343        /// Expands the selected entry and its children in the project tree.
344        ExpandSelectedEntryAndChildren,
345        /// Collapses all entries in the project tree.
346        CollapseAllEntries,
347        /// Expands all entries in the project tree.
348        ExpandAllEntries,
349        /// Creates a new directory.
350        NewDirectory,
351        /// Creates a new file.
352        NewFile,
353        /// Copies the selected file or directory.
354        Copy,
355        /// Duplicates the selected file or directory.
356        Duplicate,
357        /// Reveals the selected item in the system file manager.
358        RevealInFileManager,
359        /// Removes the selected folder from the project.
360        RemoveFromProject,
361        /// Cuts the selected file or directory.
362        Cut,
363        /// Pastes the previously cut or copied item.
364        Paste,
365        /// Downloads the selected remote file
366        DownloadFromRemote,
367        /// Renames the selected file or directory.
368        Rename,
369        /// Opens the selected file in the editor.
370        Open,
371        /// Opens the selected file in a permanent tab.
372        OpenPermanent,
373        /// Opens the selected file in a vertical split.
374        OpenSplitVertical,
375        /// Opens the selected file in a horizontal split.
376        OpenSplitHorizontal,
377        /// Toggles visibility of git-ignored files.
378        ToggleHideGitIgnore,
379        /// Toggles visibility of hidden files.
380        ToggleHideHidden,
381        /// Starts a new search in the selected directory.
382        NewSearchInDirectory,
383        /// Unfolds the selected directory.
384        UnfoldDirectory,
385        /// Folds the selected directory.
386        FoldDirectory,
387        /// Scroll half a page upwards
388        ScrollUp,
389        /// Scroll half a page downwards
390        ScrollDown,
391        /// Scroll until the cursor displays at the center
392        ScrollCursorCenter,
393        /// Scroll until the cursor displays at the top
394        ScrollCursorTop,
395        /// Scroll until the cursor displays at the bottom
396        ScrollCursorBottom,
397        /// Selects the parent directory.
398        SelectParent,
399        /// Selects the next entry with git changes.
400        SelectNextGitEntry,
401        /// Selects the previous entry with git changes.
402        SelectPrevGitEntry,
403        /// Selects the next directory.
404        SelectNextDirectory,
405        /// Selects the previous directory.
406        SelectPrevDirectory,
407        /// Opens a diff view to compare two marked files.
408        CompareMarkedFiles,
409        /// Undoes the last file operation.
410        Undo,
411        /// Redoes the last undone file operation.
412        Redo,
413        /// Opens a markdown preview for the selected file.
414        OpenMarkdownPreview,
415    ]
416);
417
418#[derive(Clone, Debug, Default)]
419struct FoldedAncestors {
420    current_ancestor_depth: usize,
421    ancestors: Vec<ProjectEntryId>,
422}
423
424impl FoldedAncestors {
425    fn max_ancestor_depth(&self) -> usize {
426        self.ancestors.len()
427    }
428
429    /// Note: This returns None for last item in ancestors list
430    fn active_ancestor(&self) -> Option<ProjectEntryId> {
431        if self.current_ancestor_depth == 0 {
432            return None;
433        }
434        self.ancestors.get(self.current_ancestor_depth).copied()
435    }
436
437    fn active_index(&self) -> usize {
438        self.max_ancestor_depth()
439            .saturating_sub(1)
440            .saturating_sub(self.current_ancestor_depth)
441    }
442
443    fn set_active_index(&mut self, index: usize) -> bool {
444        let new_depth = self
445            .max_ancestor_depth()
446            .saturating_sub(1)
447            .saturating_sub(index);
448        if self.current_ancestor_depth != new_depth {
449            self.current_ancestor_depth = new_depth;
450            true
451        } else {
452            false
453        }
454    }
455
456    fn active_component(&self, file_name: &str) -> Option<String> {
457        Path::new(file_name)
458            .components()
459            .nth(self.active_index())
460            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
461    }
462}
463
464pub fn init(cx: &mut App) {
465    cx.observe_new(|workspace: &mut Workspace, _, _| {
466        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
467            workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
468        });
469        workspace.register_action(|workspace, _: &Toggle, window, cx| {
470            if !workspace.toggle_panel_focus::<ProjectPanel>(window, cx) {
471                workspace.close_panel::<ProjectPanel>(window, cx);
472            }
473        });
474
475        workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
476            let fs = workspace.app_state().fs.clone();
477            update_settings_file(fs, cx, move |setting, _| {
478                setting.project_panel.get_or_insert_default().hide_gitignore = Some(
479                    !setting
480                        .project_panel
481                        .get_or_insert_default()
482                        .hide_gitignore
483                        .unwrap_or(false),
484                );
485            })
486        });
487
488        workspace.register_action(|workspace, _: &ToggleHideHidden, _, cx| {
489            let fs = workspace.app_state().fs.clone();
490            update_settings_file(fs, cx, move |setting, _| {
491                setting.project_panel.get_or_insert_default().hide_hidden = Some(
492                    !setting
493                        .project_panel
494                        .get_or_insert_default()
495                        .hide_hidden
496                        .unwrap_or(false),
497                );
498            })
499        });
500
501        workspace.register_action(|workspace, action: &CollapseAllEntries, window, cx| {
502            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
503                panel.update(cx, |panel, cx| {
504                    panel.collapse_all_entries(action, window, cx);
505                });
506            }
507        });
508
509        workspace.register_action(|workspace, action: &ExpandAllEntries, window, cx| {
510            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
511                panel.update(cx, |panel, cx| {
512                    panel.expand_all_entries(action, window, cx);
513                });
514            }
515        });
516
517        workspace.register_action(|workspace, action: &Rename, window, cx| {
518            workspace.open_panel::<ProjectPanel>(window, cx);
519            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
520                panel.update(cx, |panel, cx| {
521                    if let Some(first_marked) = panel.marked_entries.first() {
522                        let first_marked = *first_marked;
523                        panel.marked_entries.clear();
524                        panel.selection = Some(first_marked);
525                    }
526                    panel.rename(action, window, cx);
527                });
528            }
529        });
530
531        workspace.register_action(|workspace, action: &Duplicate, window, cx| {
532            workspace.open_panel::<ProjectPanel>(window, cx);
533            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
534                panel.update(cx, |panel, cx| {
535                    panel.duplicate(action, window, cx);
536                });
537            }
538        });
539
540        workspace.register_action(|workspace, action: &Delete, window, cx| {
541            if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
542                panel.update(cx, |panel, cx| panel.delete(action, window, cx));
543            }
544        });
545
546        // Forwards `git::FileHistory` to `git_ui::git_graph` when the project
547        // panel is the focused source of selection. Lives here (and not in
548        // `git_ui`) so that `git_ui` does not need to depend on
549        // `project_panel`, which would create a dependency cycle.
550        workspace.register_action_renderer(|div, workspace, window, cx| {
551            let Some(panel) = workspace.panel::<ProjectPanel>(cx) else {
552                return div;
553            };
554            if !panel.read(cx).focus_handle(cx).contains_focused(window, cx) {
555                return div;
556            }
557            if panel.read(cx).selected_entry_project_path(cx).is_none() {
558                return div;
559            }
560            let workspace = workspace.weak_handle();
561            div.capture_action(move |_: &git::FileHistory, window, cx| {
562                workspace
563                    .update(cx, |workspace, cx| {
564                        let Some(panel) = workspace.panel::<ProjectPanel>(cx) else {
565                            return;
566                        };
567                        let Some(project_path) = panel.read(cx).selected_entry_project_path(cx)
568                        else {
569                            return;
570                        };
571                        let Some((repo_id, log_source)) =
572                            git_ui::git_graph::resolve_file_history_target_from_project_path(
573                                workspace,
574                                &project_path,
575                                cx,
576                            )
577                        else {
578                            return;
579                        };
580                        let git_store = workspace.project().read(cx).git_store().clone();
581                        git_ui::git_graph::open_or_reuse_graph(
582                            workspace, repo_id, git_store, log_source, None, window, cx,
583                        );
584                    })
585                    .log_err();
586                cx.stop_propagation();
587            })
588        });
589    })
590    .detach();
591}
592
593#[derive(Debug)]
594pub enum Event {
595    OpenedEntry {
596        entry_id: ProjectEntryId,
597        focus_opened_item: bool,
598        allow_preview: bool,
599    },
600    SplitEntry {
601        entry_id: ProjectEntryId,
602        allow_preview: bool,
603        split_direction: Option<SplitDirection>,
604    },
605    Focus,
606}
607
608struct DraggedProjectEntryView {
609    selection: SelectedEntry,
610    icon: Option<SharedString>,
611    filename: String,
612    click_offset: Point<Pixels>,
613    selections: Arc<[SelectedEntry]>,
614}
615
616struct ItemColors {
617    default: Hsla,
618    hover: Hsla,
619    drag_over: Hsla,
620    marked: Hsla,
621    focused: Hsla,
622}
623
624fn get_item_color(is_sticky: bool, cx: &App) -> ItemColors {
625    let colors = cx.theme().colors();
626
627    ItemColors {
628        default: if is_sticky {
629            colors.panel_overlay_background
630        } else {
631            colors.panel_background
632        },
633        hover: if is_sticky {
634            colors.panel_overlay_hover
635        } else {
636            colors.element_hover
637        },
638        marked: colors.element_selected,
639        focused: colors.panel_focused_border,
640        drag_over: colors.drop_target_background,
641    }
642}
643
644enum DeleteEntryOutcome {
645    /// Entry was successfully trashed, returning the `Change` that can be
646    /// recorded to the undo stack.
647    Trashed(Change),
648    /// Entry was successfully deleted, or there was nothing to delete.
649    Deleted,
650    /// Deleting or trashing the entry failed at the OS level.
651    Failed,
652}
653
654/// Small wrapper around a pending task returned by either
655/// [`Project::trash_entry`] or [`Project::delete_entry`].
656///
657/// Since the two operations return different types, this enum lets
658/// [`ProjectPanel::remove`] collect and resolve both uniformly, while carrying
659/// the intention (trash vs delete).
660enum RemoveEntryTask {
661    Trash(Task<Result<TrashId>>),
662    Delete(Task<Result<()>>),
663}
664
665impl ProjectPanel {
666    fn new(
667        workspace: &mut Workspace,
668        window: &mut Window,
669        cx: &mut Context<Workspace>,
670    ) -> Entity<Self> {
671        let project = workspace.project().clone();
672        let git_store = project.read(cx).git_store().clone();
673        let path_style = project.read(cx).path_style(cx);
674        let project_panel = cx.new(|cx| {
675            let focus_handle = cx.focus_handle();
676            cx.on_focus(&focus_handle, window, Self::focus_in).detach();
677
678            cx.subscribe_in(
679                &git_store,
680                window,
681                |this, _, event, window, cx| match event {
682                    GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, _)
683                    | GitStoreEvent::RepositoryAdded
684                    | GitStoreEvent::RepositoryRemoved(_) => {
685                        this.update_visible_entries(None, false, false, window, cx);
686                        cx.notify();
687                    }
688                    _ => {}
689                },
690            )
691            .detach();
692
693            cx.subscribe_in(
694                &project,
695                window,
696                |this, project, event, window, cx| match event {
697                    project::Event::ActiveEntryChanged(Some(entry_id)) => {
698                        if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
699                            this.reveal_entry(project.clone(), *entry_id, true, window, cx)
700                                .ok();
701                        }
702                    }
703                    project::Event::ActiveEntryChanged(None) => {
704                        let is_active_item_file_diff_view = this
705                            .workspace
706                            .upgrade()
707                            .and_then(|ws| ws.read(cx).active_item(cx))
708                            .map(|item| {
709                                item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some()
710                            })
711                            .unwrap_or(false);
712                        if !is_active_item_file_diff_view {
713                            this.marked_entries.clear();
714                        }
715                    }
716                    project::Event::RevealInProjectPanel(entry_id) => {
717                        if let Some(()) = this
718                            .reveal_entry(project.clone(), *entry_id, false, window, cx)
719                            .log_err()
720                        {
721                            cx.emit(PanelEvent::Activate);
722                        }
723                    }
724                    project::Event::ActivateProjectPanel => {
725                        cx.emit(PanelEvent::Activate);
726                    }
727                    project::Event::DiskBasedDiagnosticsFinished { .. }
728                    | project::Event::DiagnosticsUpdated { .. } => {
729                        if ProjectPanelSettings::get_global(cx).show_diagnostics
730                            != ShowDiagnostics::Off
731                        {
732                            this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
733                                cx.background_executor()
734                                    .timer(Duration::from_millis(30))
735                                    .await;
736                                this.update(cx, |this, cx| {
737                                    this.update_diagnostics(cx);
738                                    cx.notify();
739                                })
740                                .log_err();
741                            });
742                        }
743                    }
744                    project::Event::WorktreeRemoved(id) => {
745                        this.state.expanded_dir_ids.remove(id);
746                        this.update_visible_entries(None, false, false, window, cx);
747                        cx.notify();
748                    }
749                    project::Event::WorktreeUpdatedEntries(_, _)
750                    | project::Event::WorktreeAdded(_)
751                    | project::Event::WorktreeOrderChanged => {
752                        this.update_visible_entries(None, false, false, window, cx);
753                        cx.notify();
754                    }
755                    project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
756                        this.synchronously_expand_all_directories(
757                            *worktree_id,
758                            *entry_id,
759                            window,
760                            cx,
761                        );
762                    }
763                    _ => {}
764                },
765            )
766            .detach();
767
768            let filename_editor = cx.new(|cx| Editor::single_line(window, cx));
769
770            cx.subscribe_in(
771                &filename_editor,
772                window,
773                |project_panel, _, editor_event, window, cx| match editor_event {
774                    EditorEvent::BufferEdited => {
775                        project_panel.populate_validation_error(cx);
776                        project_panel.autoscroll(cx);
777                    }
778                    EditorEvent::SelectionsChanged { .. } => {
779                        project_panel.autoscroll(cx);
780                    }
781                    EditorEvent::Blurred => {
782                        if project_panel
783                            .state
784                            .edit_state
785                            .as_ref()
786                            .is_some_and(|state| state.processing_filename.is_none())
787                        {
788                            match project_panel.confirm_edit(false, window, cx) {
789                                Some(task) => {
790                                    task.detach_and_notify_err(
791                                        project_panel.workspace.clone(),
792                                        window,
793                                        cx,
794                                    );
795                                }
796                                None => {
797                                    project_panel.discard_edit_state(window, cx);
798                                }
799                            }
800                        }
801                    }
802                    _ => {}
803                },
804            )
805            .detach();
806
807            cx.observe_global::<FileIcons>(|_, cx| {
808                cx.notify();
809            })
810            .detach();
811
812            let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
813            cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
814                let new_settings = *ProjectPanelSettings::get_global(cx);
815                if project_panel_settings != new_settings {
816                    if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
817                        this.update_visible_entries(None, false, false, window, cx);
818                    }
819                    if project_panel_settings.hide_root != new_settings.hide_root {
820                        this.update_visible_entries(None, false, false, window, cx);
821                    }
822                    if project_panel_settings.hide_hidden != new_settings.hide_hidden {
823                        this.update_visible_entries(None, false, false, window, cx);
824                    }
825                    if project_panel_settings.sort_mode != new_settings.sort_mode {
826                        this.update_visible_entries(None, false, false, window, cx);
827                    }
828                    if project_panel_settings.sort_order != new_settings.sort_order {
829                        this.update_visible_entries(None, false, false, window, cx);
830                    }
831                    if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
832                        this.sticky_items_count = 0;
833                    }
834                    project_panel_settings = new_settings;
835                    this.update_diagnostics(cx);
836                    cx.notify();
837                }
838            })
839            .detach();
840
841            let scroll_handle = UniformListScrollHandle::new();
842            let weak_project_panel = cx.weak_entity();
843            let mut this = Self {
844                project: project.clone(),
845                hover_scroll_task: None,
846                fs: workspace.app_state().fs.clone(),
847                focus_handle,
848                rendered_entries_len: 0,
849                folded_directory_drag_target: None,
850                drag_target_entry: None,
851                marked_entries: Default::default(),
852                selection: None,
853                context_menu: None,
854                filename_editor,
855                clipboard: None,
856                _dragged_entry_destination: None,
857                workspace: workspace.weak_handle(),
858                diagnostics: Default::default(),
859                diagnostic_counts: Default::default(),
860                diagnostic_summary_update: Task::ready(()),
861                scroll_handle,
862                mouse_down: false,
863                hover_expand_task: None,
864                previous_drag_position: None,
865                sticky_items_count: 0,
866                last_reported_update: Instant::now(),
867                state: State {
868                    max_width_item_index: None,
869                    edit_state: None,
870                    temporarily_unfolded_pending_state: None,
871                    last_worktree_root_id: Default::default(),
872                    visible_entries: Default::default(),
873                    ancestors: Default::default(),
874                    expanded_dir_ids: Default::default(),
875                    unfolded_dir_ids: Default::default(),
876                },
877                update_visible_entries_task: Default::default(),
878                undo_manager: UndoManager::new(
879                    workspace.weak_handle(),
880                    weak_project_panel,
881                    project.read(cx).is_via_collab(),
882                    &cx,
883                ),
884            };
885            this.update_visible_entries(None, false, false, window, cx);
886
887            this
888        });
889
890        cx.subscribe_in(&project_panel, window, {
891            let project_panel = project_panel.downgrade();
892            move |workspace, _, event, window, cx| match event {
893                &Event::OpenedEntry {
894                    entry_id,
895                    focus_opened_item,
896                    allow_preview,
897                } => {
898                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
899                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
900                            let file_path = entry.path.clone();
901                            let worktree_id = worktree.read(cx).id();
902                            let entry_id = entry.id;
903                            let is_via_ssh = project.read(cx).is_via_remote_server();
904
905                            workspace
906                                .open_path_preview(
907                                    ProjectPath {
908                                        worktree_id,
909                                        path: file_path.clone(),
910                                    },
911                                    None,
912                                    focus_opened_item,
913                                    allow_preview,
914                                    true,
915                                    window, cx,
916                                )
917                                .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| {
918                                    match e.error_code() {
919                                        ErrorCode::Disconnected => if is_via_ssh {
920                                            Some("Disconnected from SSH host".to_string())
921                                        } else {
922                                            Some("Disconnected from remote project".to_string())
923                                        },
924                                        ErrorCode::UnsharedItem => Some(format!(
925                                            "{} is not shared by the host. This could be because it has been marked as `private`",
926                                            file_path.display(path_style)
927                                        )),
928                                        // See note in worktree.rs where this error originates. Returning Some in this case prevents
929                                        // the error popup from saying "Try Again", which is a red herring in this case
930                                        ErrorCode::Internal if e.to_string().contains("File is too large to load") => Some(e.to_string()),
931                                        _ => None,
932                                    }
933                                });
934
935                            if let Some(project_panel) = project_panel.upgrade() {
936                                // Always select and mark the entry, regardless of whether it is opened or not.
937                                project_panel.update(cx, |project_panel, _| {
938                                    let entry = SelectedEntry { worktree_id, entry_id };
939                                    project_panel.marked_entries.clear();
940                                    project_panel.marked_entries.push(entry);
941                                    project_panel.selection = Some(entry);
942                                });
943                                if !focus_opened_item {
944                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
945                                    window.focus(&focus_handle, cx);
946                                }
947                            }
948                        }
949                }
950                &Event::SplitEntry {
951                    entry_id,
952                    allow_preview,
953                    split_direction,
954                } => {
955                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
956                        && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
957                            workspace
958                                .split_path_preview(
959                                    ProjectPath {
960                                        worktree_id: worktree.read(cx).id(),
961                                        path: entry.path.clone(),
962                                    },
963                                    allow_preview,
964                                    split_direction,
965                                    window, cx,
966                                )
967                                .detach_and_log_err(cx);
968                        }
969                }
970
971                _ => {}
972            }
973        })
974        .detach();
975
976        project_panel
977    }
978
979    pub async fn load(
980        workspace: WeakEntity<Workspace>,
981        mut cx: AsyncWindowContext,
982    ) -> Result<Entity<Self>> {
983        workspace.update_in(&mut cx, |workspace, window, cx| {
984            ProjectPanel::new(workspace, window, cx)
985        })
986    }
987
988    fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
989        let mut diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity> =
990            Default::default();
991        let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
992
993        if show_diagnostics_setting != ShowDiagnostics::Off {
994            self.project
995                .read(cx)
996                .diagnostic_summaries(false, cx)
997                .filter_map(|(path, _, diagnostic_summary)| {
998                    if diagnostic_summary.error_count > 0 {
999                        Some((path, DiagnosticSeverity::ERROR))
1000                    } else if show_diagnostics_setting == ShowDiagnostics::All
1001                        && diagnostic_summary.warning_count > 0
1002                    {
1003                        Some((path, DiagnosticSeverity::WARNING))
1004                    } else {
1005                        None
1006                    }
1007                })
1008                .for_each(|(project_path, diagnostic_severity)| {
1009                    let ancestors = project_path.path.ancestors().collect::<Vec<_>>();
1010                    for path in ancestors.into_iter().rev() {
1011                        Self::update_strongest_diagnostic_severity(
1012                            &mut diagnostics,
1013                            &project_path,
1014                            path.into(),
1015                            diagnostic_severity,
1016                        );
1017                    }
1018                });
1019        }
1020        self.diagnostics = diagnostics;
1021
1022        let diagnostic_badges = ProjectPanelSettings::get_global(cx).diagnostic_badges;
1023        self.diagnostic_counts =
1024            if diagnostic_badges && show_diagnostics_setting != ShowDiagnostics::Off {
1025                self.project.read(cx).diagnostic_summaries(false, cx).fold(
1026                    HashMap::default(),
1027                    |mut counts, (project_path, _, summary)| {
1028                        let entry = counts
1029                            .entry((project_path.worktree_id, project_path.path))
1030                            .or_default();
1031                        entry.error_count += summary.error_count;
1032                        if show_diagnostics_setting == ShowDiagnostics::All {
1033                            entry.warning_count += summary.warning_count;
1034                        }
1035                        counts
1036                    },
1037                )
1038            } else {
1039                Default::default()
1040            };
1041    }
1042
1043    fn update_strongest_diagnostic_severity(
1044        diagnostics: &mut HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
1045        project_path: &ProjectPath,
1046        path_buffer: Arc<RelPath>,
1047        diagnostic_severity: DiagnosticSeverity,
1048    ) {
1049        diagnostics
1050            .entry((project_path.worktree_id, path_buffer))
1051            .and_modify(|strongest_diagnostic_severity| {
1052                *strongest_diagnostic_severity =
1053                    cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
1054            })
1055            .or_insert(diagnostic_severity);
1056    }
1057
1058    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1059        if !self.focus_handle.contains_focused(window, cx) {
1060            cx.emit(Event::Focus);
1061        }
1062    }
1063
1064    fn deploy_context_menu(
1065        &mut self,
1066        position: Point<Pixels>,
1067        entry_id: ProjectEntryId,
1068        window: &mut Window,
1069        cx: &mut Context<Self>,
1070    ) {
1071        let project = self.project.read(cx);
1072
1073        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
1074            id
1075        } else {
1076            return;
1077        };
1078
1079        self.selection = Some(SelectedEntry {
1080            worktree_id,
1081            entry_id,
1082        });
1083
1084        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1085            let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1086            let worktree = worktree.read(cx);
1087            let is_root = Some(entry) == worktree.root_entry();
1088            let is_dir = entry.is_dir();
1089            let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
1090            let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
1091            let is_read_only = project.is_read_only(cx);
1092            let is_remote = project.is_remote();
1093            let is_collab = project.is_via_collab();
1094            let is_local = project.is_local() || project.is_via_wsl_with_host_interop(cx);
1095            let is_markdown = !is_dir && MarkdownPreviewView::is_markdown_path(&*entry.path);
1096
1097            let settings = ProjectPanelSettings::get_global(cx);
1098            let visible_worktrees_count = project.visible_worktrees(cx).count();
1099            let should_hide_rename = is_root
1100                && (cfg!(target_os = "windows")
1101                    || (settings.hide_root && visible_worktrees_count == 1));
1102            let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
1103
1104            let (has_git_repo, has_history) = {
1105                let project_path = project::ProjectPath {
1106                    worktree_id,
1107                    path: entry.path.clone(),
1108                };
1109                let git_store = project.git_store().read(cx);
1110                let has_git_repo = git_store
1111                    .repository_and_path_for_project_path(&project_path, cx)
1112                    .is_some();
1113                let has_history = has_git_repo
1114                    && !git_store
1115                        .project_path_git_status(&project_path, cx)
1116                        .is_some_and(|status| status.is_created());
1117                (has_git_repo, has_history)
1118            };
1119
1120            let has_pasteable_content = self.has_pasteable_content(cx);
1121            let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
1122                menu.context(self.focus_handle.clone()).map(|menu| {
1123                    if is_read_only {
1124                        menu.when(is_markdown, |menu| {
1125                            menu.action("Open Markdown Preview", Box::new(OpenMarkdownPreview))
1126                        })
1127                        .when(is_dir, |menu| {
1128                            menu.action("Search Inside", Box::new(NewSearchInDirectory))
1129                        })
1130                    } else {
1131                        menu.action("New File", Box::new(NewFile))
1132                            .action("New Folder", Box::new(NewDirectory))
1133                            .separator()
1134                            .when(is_local, |menu| {
1135                                menu.action(
1136                                    ui::utils::reveal_in_file_manager_label(is_remote),
1137                                    Box::new(RevealInFileManager),
1138                                )
1139                            })
1140                            .when(is_local, |menu| {
1141                                menu.action("Open in Default App", Box::new(OpenWithSystem))
1142                            })
1143                            .action("Open in Terminal", Box::new(OpenInTerminal))
1144                            .when(is_markdown, |menu| {
1145                                menu.action("Open Markdown Preview", Box::new(OpenMarkdownPreview))
1146                            })
1147                            .when(is_dir, |menu| {
1148                                menu.separator()
1149                                    .action("Find in Folder…", Box::new(NewSearchInDirectory))
1150                            })
1151                            .when(is_unfoldable, |menu| {
1152                                menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1153                            })
1154                            .when(is_foldable, |menu| {
1155                                menu.action("Fold Directory", Box::new(FoldDirectory))
1156                            })
1157                            .when(should_show_compare, |menu| {
1158                                menu.separator()
1159                                    .action("Compare Marked Files", Box::new(CompareMarkedFiles))
1160                            })
1161                            .separator()
1162                            .action("Cut", Box::new(Cut))
1163                            .action("Copy", Box::new(Copy))
1164                            .action("Duplicate", Box::new(Duplicate))
1165                            .action_disabled_when(!has_pasteable_content, "Paste", Box::new(Paste))
1166                            .when(
1167                                !is_collab && cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(),
1168                                |menu| {
1169                                    let can_undo = self.undo_manager.can_undo();
1170                                    let can_redo = self.undo_manager.can_redo();
1171
1172                                    menu.action_disabled_when(!can_undo, "Undo", Box::new(Undo))
1173                                        .action_disabled_when(!can_redo, "Redo", Box::new(Redo))
1174                                },
1175                            )
1176                            .when(is_remote, |menu| {
1177                                menu.separator()
1178                                    .action("Download...", Box::new(DownloadFromRemote))
1179                            })
1180                            .separator()
1181                            .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1182                            .action(
1183                                "Copy Relative Path",
1184                                Box::new(zed_actions::workspace::CopyRelativePath),
1185                            )
1186                            .when(has_git_repo, |menu| {
1187                                menu.separator()
1188                                    .when(!is_dir && self.has_git_changes(entry_id), |menu| {
1189                                        menu.action(
1190                                            "Restore File",
1191                                            Box::new(git::RestoreFile { skip_prompt: false }),
1192                                        )
1193                                    })
1194                                    .action("Add to .gitignore", Box::new(git::AddToGitignore))
1195                                    .action(
1196                                        "Add to .git/info/exclude",
1197                                        Box::new(git::AddToGitInfoExclude),
1198                                    )
1199                                    .when(has_history, |menu| {
1200                                        menu.action("View History", Box::new(git::FileHistory))
1201                                    })
1202                            })
1203                            .when(!should_hide_rename, |menu| {
1204                                menu.separator().action("Rename", Box::new(Rename))
1205                            })
1206                            .when(!is_root && !is_collab, |menu| {
1207                                menu.action("Trash", Box::new(Trash { skip_prompt: false }))
1208                            })
1209                            .when(!is_root, |menu| {
1210                                menu.action("Delete", Box::new(Delete { skip_prompt: false }))
1211                            })
1212                            .when(!is_collab && is_root, |menu| {
1213                                menu.separator()
1214                                    .action(
1215                                        "Add Folders to Project…",
1216                                        Box::new(workspace::AddFolderToProject),
1217                                    )
1218                                    .action("Remove from Project", Box::new(RemoveFromProject))
1219                            })
1220                            .when(is_dir && !is_root, |menu| {
1221                                menu.separator()
1222                                    .action("Expand All", Box::new(ExpandSelectedEntryAndChildren))
1223                                    .action(
1224                                        "Collapse All",
1225                                        Box::new(CollapseSelectedEntryAndChildren),
1226                                    )
1227                            })
1228                            .when(is_dir && is_root, |menu| {
1229                                menu.separator()
1230                                    .action("Expand All", Box::new(ExpandAllEntries))
1231                                    .action("Collapse All", Box::new(CollapseAllEntries))
1232                            })
1233                    }
1234                })
1235            });
1236
1237            window.focus(&context_menu.focus_handle(cx), cx);
1238            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
1239                this.context_menu.take();
1240                cx.notify();
1241            });
1242            self.context_menu = Some((context_menu, position, subscription));
1243        }
1244
1245        cx.notify();
1246    }
1247
1248    fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool {
1249        for visible in &self.state.visible_entries {
1250            if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) {
1251                let total_modified =
1252                    git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified;
1253                let total_deleted =
1254                    git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted;
1255                return total_modified > 0 || total_deleted > 0;
1256            }
1257        }
1258        false
1259    }
1260
1261    fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1262        if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) {
1263            return false;
1264        }
1265
1266        if let Some(parent_path) = entry.path.parent() {
1267            let snapshot = worktree.snapshot();
1268            let mut child_entries = snapshot.child_entries(parent_path);
1269            if let Some(child) = child_entries.next()
1270                && child_entries.next().is_none()
1271            {
1272                return child.kind.is_dir();
1273            }
1274        };
1275        false
1276    }
1277
1278    fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
1279        if entry.is_dir() {
1280            let snapshot = worktree.snapshot();
1281
1282            let mut child_entries = snapshot.child_entries(&entry.path);
1283            if let Some(child) = child_entries.next()
1284                && child_entries.next().is_none()
1285            {
1286                return child.kind.is_dir();
1287            }
1288        }
1289        false
1290    }
1291
1292    fn expand_selected_entry(
1293        &mut self,
1294        _: &ExpandSelectedEntry,
1295        window: &mut Window,
1296        cx: &mut Context<Self>,
1297    ) {
1298        if let Some((worktree, entry)) = self.selected_entry(cx) {
1299            if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1300                && folded_ancestors.current_ancestor_depth > 0
1301            {
1302                folded_ancestors.current_ancestor_depth -= 1;
1303                cx.notify();
1304                return;
1305            }
1306            if entry.is_dir() {
1307                let worktree_id = worktree.id();
1308                let entry_id = entry.id;
1309                let expanded_dir_ids = if let Some(expanded_dir_ids) =
1310                    self.state.expanded_dir_ids.get_mut(&worktree_id)
1311                {
1312                    expanded_dir_ids
1313                } else {
1314                    return;
1315                };
1316
1317                match expanded_dir_ids.binary_search(&entry_id) {
1318                    Ok(_) => self.select_next(&SelectNext, window, cx),
1319                    Err(ix) => {
1320                        self.project.update(cx, |project, cx| {
1321                            project.expand_entry(worktree_id, entry_id, cx);
1322                        });
1323
1324                        expanded_dir_ids.insert(ix, entry_id);
1325                        self.update_visible_entries(None, false, false, window, cx);
1326                        cx.notify();
1327                    }
1328                }
1329            }
1330        }
1331    }
1332
1333    fn collapse_selected_entry(
1334        &mut self,
1335        _: &CollapseSelectedEntry,
1336        window: &mut Window,
1337        cx: &mut Context<Self>,
1338    ) {
1339        let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
1340            return;
1341        };
1342        self.collapse_entry(entry.clone(), worktree, window, cx)
1343    }
1344
1345    fn collapse_entry(
1346        &mut self,
1347        entry: Entry,
1348        worktree: Entity<Worktree>,
1349        window: &mut Window,
1350        cx: &mut Context<Self>,
1351    ) {
1352        let worktree = worktree.read(cx);
1353        if let Some(folded_ancestors) = self.state.ancestors.get_mut(&entry.id)
1354            && folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth()
1355        {
1356            folded_ancestors.current_ancestor_depth += 1;
1357            cx.notify();
1358            return;
1359        }
1360        let worktree_id = worktree.id();
1361        let expanded_dir_ids =
1362            if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1363                expanded_dir_ids
1364            } else {
1365                return;
1366            };
1367
1368        let mut entry = &entry;
1369        loop {
1370            let entry_id = entry.id;
1371            match expanded_dir_ids.binary_search(&entry_id) {
1372                Ok(ix) => {
1373                    expanded_dir_ids.remove(ix);
1374                    self.update_visible_entries(
1375                        Some((worktree_id, entry_id)),
1376                        false,
1377                        false,
1378                        window,
1379                        cx,
1380                    );
1381                    cx.notify();
1382                    break;
1383                }
1384                Err(_) => {
1385                    if let Some(parent_entry) =
1386                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1387                    {
1388                        entry = parent_entry;
1389                    } else {
1390                        break;
1391                    }
1392                }
1393            }
1394        }
1395    }
1396
1397    fn collapse_selected_entry_and_children(
1398        &mut self,
1399        _: &CollapseSelectedEntryAndChildren,
1400        window: &mut Window,
1401        cx: &mut Context<Self>,
1402    ) {
1403        if let Some((worktree, entry)) = self.selected_entry(cx) {
1404            let worktree_id = worktree.id();
1405            let entry_id = entry.id;
1406
1407            self.collapse_all_for_entry(worktree_id, entry_id, cx);
1408
1409            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1410            cx.notify();
1411        }
1412    }
1413
1414    fn collapse_worktree_expanded_dirs(
1415        &mut self,
1416        worktree_id: WorktreeId,
1417        root_id: ProjectEntryId,
1418        cx: &App,
1419    ) {
1420        let single_worktree = self.project.read(cx).visible_worktrees(cx).count() == 1;
1421        if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
1422            if single_worktree {
1423                expanded_dir_ids.retain(|id| id == &root_id);
1424            } else {
1425                expanded_dir_ids.clear();
1426            }
1427        }
1428    }
1429
1430    fn all_worktree_roots(&self, cx: &App) -> Vec<(WorktreeId, ProjectEntryId)> {
1431        self.project
1432            .read(cx)
1433            .visible_worktrees(cx)
1434            .filter_map(|worktree| {
1435                let worktree = worktree.read(cx);
1436                Some((worktree.id(), worktree.root_entry()?.id))
1437            })
1438            .collect()
1439    }
1440
1441    fn expand_worktree_roots(
1442        &mut self,
1443        roots: Vec<(WorktreeId, ProjectEntryId)>,
1444        window: &mut Window,
1445        cx: &mut Context<Self>,
1446    ) {
1447        for (worktree_id, root_id) in roots {
1448            self.expand_all_for_entry(worktree_id, root_id, cx);
1449            self.synchronously_expand_all_directories_internal(worktree_id, root_id, cx);
1450        }
1451
1452        self.update_visible_entries(None, false, false, window, cx);
1453        cx.notify();
1454    }
1455
1456    fn collapse_worktree_roots(
1457        &mut self,
1458        roots: Vec<(WorktreeId, ProjectEntryId)>,
1459        window: &mut Window,
1460        cx: &mut Context<Self>,
1461    ) {
1462        for (worktree_id, root_id) in roots {
1463            self.collapse_worktree_expanded_dirs(worktree_id, root_id, cx);
1464        }
1465
1466        self.update_visible_entries(None, false, false, window, cx);
1467        cx.notify();
1468    }
1469
1470    fn collapse_all_entries(
1471        &mut self,
1472        _: &CollapseAllEntries,
1473        window: &mut Window,
1474        cx: &mut Context<Self>,
1475    ) {
1476        let roots = self.all_worktree_roots(cx);
1477        self.collapse_worktree_roots(roots, window, cx);
1478    }
1479
1480    fn expand_all_entries(
1481        &mut self,
1482        _: &ExpandAllEntries,
1483        window: &mut Window,
1484        cx: &mut Context<Self>,
1485    ) {
1486        let roots = self.all_worktree_roots(cx);
1487        self.expand_worktree_roots(roots, window, cx);
1488    }
1489
1490    fn expand_all_for_entry_and_refresh(
1491        &mut self,
1492        worktree_id: WorktreeId,
1493        entry_id: ProjectEntryId,
1494        window: &mut Window,
1495        cx: &mut Context<Self>,
1496    ) {
1497        self.expand_all_for_entry(worktree_id, entry_id, cx);
1498        self.synchronously_expand_all_directories(worktree_id, entry_id, window, cx);
1499    }
1500
1501    fn expand_selected_entry_and_children(
1502        &mut self,
1503        _: &ExpandSelectedEntryAndChildren,
1504        window: &mut Window,
1505        cx: &mut Context<Self>,
1506    ) {
1507        if let Some((worktree, entry)) = self.selected_entry(cx) {
1508            let worktree_id = worktree.id();
1509            let entry_id = entry.id;
1510            self.expand_all_for_entry_and_refresh(worktree_id, entry_id, window, cx);
1511        }
1512    }
1513
1514    fn synchronously_expand_all_directories(
1515        &mut self,
1516        worktree_id: WorktreeId,
1517        entry_id: ProjectEntryId,
1518        window: &mut Window,
1519        cx: &mut Context<Self>,
1520    ) {
1521        self.synchronously_expand_all_directories_internal(worktree_id, entry_id, cx);
1522        self.update_visible_entries(None, false, false, window, cx);
1523        cx.notify();
1524    }
1525
1526    fn synchronously_expand_all_directories_internal(
1527        &mut self,
1528        worktree_id: WorktreeId,
1529        entry_id: ProjectEntryId,
1530        cx: &mut Context<Self>,
1531    ) {
1532        let project = self.project.read(cx);
1533        let Some((worktree, expanded_dir_ids)) = project
1534            .worktree_for_id(worktree_id, cx)
1535            .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1536        else {
1537            return;
1538        };
1539
1540        let worktree = worktree.read(cx);
1541        let Some(entry) = worktree.entry_for_id(entry_id) else {
1542            return;
1543        };
1544        let include_ignored_dirs = !entry.is_ignored;
1545
1546        if let Err(ix) = expanded_dir_ids.binary_search(&entry_id) {
1547            expanded_dir_ids.insert(ix, entry_id);
1548        }
1549
1550        let mut dirs_to_expand = vec![entry_id];
1551        while let Some(current_id) = dirs_to_expand.pop() {
1552            let Some(current_entry) = worktree.entry_for_id(current_id) else {
1553                continue;
1554            };
1555            for child in worktree.child_entries(&current_entry.path) {
1556                if !child.is_dir() || (include_ignored_dirs && child.is_ignored) {
1557                    continue;
1558                }
1559
1560                dirs_to_expand.push(child.id);
1561
1562                if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
1563                    expanded_dir_ids.insert(ix, child.id);
1564                }
1565                self.state.unfolded_dir_ids.insert(child.id);
1566            }
1567        }
1568    }
1569
1570    fn toggle_expanded(
1571        &mut self,
1572        entry_id: ProjectEntryId,
1573        window: &mut Window,
1574        cx: &mut Context<Self>,
1575    ) {
1576        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1577            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1578        {
1579            self.project.update(cx, |project, cx| {
1580                match expanded_dir_ids.binary_search(&entry_id) {
1581                    Ok(ix) => {
1582                        expanded_dir_ids.remove(ix);
1583                    }
1584                    Err(ix) => {
1585                        project.expand_entry(worktree_id, entry_id, cx);
1586                        expanded_dir_ids.insert(ix, entry_id);
1587                    }
1588                }
1589            });
1590            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1591            window.focus(&self.focus_handle, cx);
1592            cx.notify();
1593        }
1594    }
1595
1596    fn toggle_expand_all(
1597        &mut self,
1598        entry_id: ProjectEntryId,
1599        window: &mut Window,
1600        cx: &mut Context<Self>,
1601    ) {
1602        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx)
1603            && let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id)
1604        {
1605            match expanded_dir_ids.binary_search(&entry_id) {
1606                Ok(_ix) => {
1607                    self.collapse_all_for_entry(worktree_id, entry_id, cx);
1608                }
1609                Err(_ix) => {
1610                    self.expand_all_for_entry(worktree_id, entry_id, cx);
1611                }
1612            }
1613            self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
1614            window.focus(&self.focus_handle, cx);
1615            cx.notify();
1616        }
1617    }
1618
1619    fn expand_all_for_entry(
1620        &mut self,
1621        worktree_id: WorktreeId,
1622        entry_id: ProjectEntryId,
1623        cx: &mut Context<Self>,
1624    ) {
1625        self.project.update(cx, |project, cx| {
1626            if let Some((worktree, expanded_dir_ids)) = project
1627                .worktree_for_id(worktree_id, cx)
1628                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1629            {
1630                if let Some(task) = project.expand_all_for_entry(worktree_id, entry_id, cx) {
1631                    task.detach();
1632                }
1633
1634                let worktree = worktree.read(cx);
1635
1636                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1637                    loop {
1638                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1639                            expanded_dir_ids.insert(ix, entry.id);
1640                        }
1641
1642                        if let Some(parent_entry) =
1643                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1644                        {
1645                            entry = parent_entry;
1646                        } else {
1647                            break;
1648                        }
1649                    }
1650                }
1651            }
1652        });
1653    }
1654
1655    fn collapse_all_for_entry(
1656        &mut self,
1657        worktree_id: WorktreeId,
1658        entry_id: ProjectEntryId,
1659        cx: &mut Context<Self>,
1660    ) {
1661        self.project.update(cx, |project, cx| {
1662            if let Some((worktree, expanded_dir_ids)) = project
1663                .worktree_for_id(worktree_id, cx)
1664                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
1665            {
1666                let worktree = worktree.read(cx);
1667                let mut dirs_to_collapse = vec![entry_id];
1668                let auto_fold_enabled = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1669                while let Some(current_id) = dirs_to_collapse.pop() {
1670                    let Some(current_entry) = worktree.entry_for_id(current_id) else {
1671                        continue;
1672                    };
1673                    if let Ok(ix) = expanded_dir_ids.binary_search(&current_id) {
1674                        expanded_dir_ids.remove(ix);
1675                    }
1676                    if auto_fold_enabled {
1677                        self.state.unfolded_dir_ids.remove(&current_id);
1678                    }
1679                    for child in worktree.child_entries(&current_entry.path) {
1680                        if child.is_dir() {
1681                            dirs_to_collapse.push(child.id);
1682                        }
1683                    }
1684                }
1685            }
1686        });
1687    }
1688
1689    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1690        if let Some(edit_state) = &self.state.edit_state
1691            && edit_state.processing_filename.is_none()
1692        {
1693            self.filename_editor.update(cx, |editor, cx| {
1694                editor.move_to_beginning_of_line(
1695                    &editor::actions::MoveToBeginningOfLine {
1696                        stop_at_soft_wraps: false,
1697                        stop_at_indent: false,
1698                    },
1699                    window,
1700                    cx,
1701                );
1702            });
1703            return;
1704        }
1705        if let Some(selection) = self.selection {
1706            let (mut worktree_ix, mut entry_ix, _) =
1707                self.index_for_selection(selection).unwrap_or_default();
1708            if entry_ix > 0 {
1709                entry_ix -= 1;
1710            } else if worktree_ix > 0 {
1711                worktree_ix -= 1;
1712                entry_ix = self.state.visible_entries[worktree_ix].entries.len() - 1;
1713            } else {
1714                return;
1715            }
1716
1717            let VisibleEntriesForWorktree {
1718                worktree_id,
1719                entries,
1720                ..
1721            } = &self.state.visible_entries[worktree_ix];
1722            let selection = SelectedEntry {
1723                worktree_id: *worktree_id,
1724                entry_id: entries[entry_ix].id,
1725            };
1726            self.selection = Some(selection);
1727            if window.modifiers().shift {
1728                self.marked_entries.push(selection);
1729            }
1730            self.autoscroll(cx);
1731            cx.notify();
1732        } else {
1733            self.select_first(&SelectFirst {}, window, cx);
1734        }
1735    }
1736
1737    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1738        if let Some(task) = self.confirm_edit(true, window, cx) {
1739            task.detach_and_notify_err(self.workspace.clone(), window, cx);
1740        }
1741    }
1742
1743    fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
1744        let preview_tabs_enabled =
1745            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
1746        self.open_internal(true, !preview_tabs_enabled, None, window, cx);
1747    }
1748
1749    fn open_permanent(&mut self, _: &OpenPermanent, window: &mut Window, cx: &mut Context<Self>) {
1750        self.open_internal(false, true, None, window, cx);
1751    }
1752
1753    fn open_split_vertical(
1754        &mut self,
1755        _: &OpenSplitVertical,
1756        window: &mut Window,
1757        cx: &mut Context<Self>,
1758    ) {
1759        self.open_internal(false, true, Some(SplitDirection::vertical(cx)), window, cx);
1760    }
1761
1762    fn open_split_horizontal(
1763        &mut self,
1764        _: &OpenSplitHorizontal,
1765        window: &mut Window,
1766        cx: &mut Context<Self>,
1767    ) {
1768        self.open_internal(
1769            false,
1770            true,
1771            Some(SplitDirection::horizontal(cx)),
1772            window,
1773            cx,
1774        );
1775    }
1776
1777    fn open_markdown_preview(
1778        &mut self,
1779        _: &OpenMarkdownPreview,
1780        window: &mut Window,
1781        cx: &mut Context<Self>,
1782    ) {
1783        let Some((worktree, entry)) = self.selected_entry(cx) else {
1784            return;
1785        };
1786        if !entry.is_file() || !MarkdownPreviewView::is_markdown_path(&*entry.path) {
1787            return;
1788        }
1789        let project_path = ProjectPath {
1790            worktree_id: worktree.id(),
1791            path: entry.path.clone(),
1792        };
1793        self.workspace
1794            .update(cx, |workspace, cx| {
1795                MarkdownPreviewView::open_for_project_path(project_path, workspace, window, cx);
1796            })
1797            .ok();
1798    }
1799
1800    fn open_internal(
1801        &mut self,
1802        allow_preview: bool,
1803        focus_opened_item: bool,
1804        split_direction: Option<SplitDirection>,
1805        window: &mut Window,
1806        cx: &mut Context<Self>,
1807    ) {
1808        if let Some((_, entry)) = self.selected_entry(cx) {
1809            if entry.is_file() {
1810                if split_direction.is_some() {
1811                    self.split_entry(entry.id, allow_preview, split_direction, cx);
1812                } else {
1813                    self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
1814                }
1815                cx.notify();
1816            } else {
1817                self.toggle_expanded(entry.id, window, cx);
1818            }
1819        }
1820    }
1821
1822    fn populate_validation_error(&mut self, cx: &mut Context<Self>) {
1823        let edit_state = match self.state.edit_state.as_mut() {
1824            Some(state) => state,
1825            None => return,
1826        };
1827        let filename = self.filename_editor.read(cx).text(cx);
1828        if !filename.is_empty() {
1829            if filename.is_empty() {
1830                edit_state.validation_state =
1831                    ValidationState::Error("File or directory name cannot be empty.".to_string());
1832                cx.notify();
1833                return;
1834            }
1835
1836            let trimmed_filename = filename.trim();
1837            if trimmed_filename != filename {
1838                edit_state.validation_state = ValidationState::Warning(
1839                    "File or directory name contains leading or trailing whitespace.".to_string(),
1840                );
1841                cx.notify();
1842                return;
1843            }
1844            let trimmed_filename = trimmed_filename.trim_start_matches('/');
1845
1846            let Ok(filename) = RelPath::from_unix_str(trimmed_filename) else {
1847                edit_state.validation_state = ValidationState::Warning(
1848                    "File or directory name contains leading or trailing whitespace.".to_string(),
1849                );
1850                cx.notify();
1851                return;
1852            };
1853
1854            if let Some(worktree) = self
1855                .project
1856                .read(cx)
1857                .worktree_for_id(edit_state.worktree_id, cx)
1858                && let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id)
1859            {
1860                let mut already_exists = false;
1861                if edit_state.is_new_entry() {
1862                    let new_path = entry.path.join(filename);
1863                    if worktree.read(cx).entry_for_path(&new_path).is_some() {
1864                        already_exists = true;
1865                    }
1866                } else {
1867                    let new_path = if let Some(parent) = entry.path.clone().parent() {
1868                        parent.join(&filename)
1869                    } else {
1870                        filename.to_owned()
1871                    };
1872                    if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
1873                        && existing.id != entry.id
1874                    {
1875                        already_exists = true;
1876                    }
1877                };
1878                if already_exists {
1879                    edit_state.validation_state = ValidationState::Error(format!(
1880                        "File or directory '{}' already exists at location. Please choose a different name.",
1881                        filename.as_unix_str()
1882                    ));
1883                    cx.notify();
1884                    return;
1885                }
1886            }
1887        }
1888        edit_state.validation_state = ValidationState::None;
1889        cx.notify();
1890    }
1891
1892    fn confirm_edit(
1893        &mut self,
1894        refocus: bool,
1895        window: &mut Window,
1896        cx: &mut Context<Self>,
1897    ) -> Option<Task<Result<()>>> {
1898        let edit_state = self.state.edit_state.as_mut()?;
1899        let worktree_id = edit_state.worktree_id;
1900        let is_new_entry = edit_state.is_new_entry();
1901        let mut filename = self.filename_editor.read(cx).text(cx);
1902        let path_style = self.project.read(cx).path_style(cx);
1903        if path_style.is_windows() {
1904            // on windows, trailing dots are ignored in paths
1905            // this can cause project panel to create a new entry with a trailing dot
1906            // while the actual one without the dot gets populated by the file watcher
1907            while let Some(trimmed) = filename.strip_suffix('.') {
1908                filename = trimmed.to_string();
1909            }
1910        }
1911        if filename.trim().is_empty() {
1912            return None;
1913        }
1914
1915        let filename_indicates_dir = if path_style.is_windows() {
1916            filename.ends_with('/') || filename.ends_with('\\')
1917        } else {
1918            filename.ends_with('/')
1919        };
1920        let filename = if path_style.is_windows() {
1921            filename.trim_start_matches(&['/', '\\'])
1922        } else {
1923            filename.trim_start_matches('/')
1924        };
1925        let filename = RelPath::new(filename.as_ref(), path_style).ok()?.into_arc();
1926
1927        edit_state.is_dir =
1928            edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
1929        let is_dir = edit_state.is_dir;
1930        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
1931        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
1932
1933        let edit_task;
1934        let edited_entry_id;
1935        let edited_entry;
1936        let new_project_path: ProjectPath;
1937        if is_new_entry {
1938            self.selection = Some(SelectedEntry {
1939                worktree_id,
1940                entry_id: NEW_ENTRY_ID,
1941            });
1942            let new_path = entry.path.join(&filename);
1943            if worktree.read(cx).entry_for_path(&new_path).is_some() {
1944                return None;
1945            }
1946
1947            edited_entry = None;
1948            edited_entry_id = NEW_ENTRY_ID;
1949            new_project_path = (worktree_id, new_path).into();
1950            edit_task = self.project.update(cx, |project, cx| {
1951                project.create_entry(new_project_path.clone(), is_dir, cx)
1952            });
1953        } else {
1954            let new_path = if let Some(parent) = entry.path.parent() {
1955                parent.join(&filename).into()
1956            } else {
1957                filename.clone()
1958            };
1959            if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
1960                if existing.id == entry.id && refocus {
1961                    window.focus(&self.focus_handle, cx);
1962                }
1963                return None;
1964            }
1965            edited_entry_id = entry.id;
1966            edited_entry = Some(entry);
1967            new_project_path = (worktree_id, new_path).into();
1968            edit_task = self.project.update(cx, |project, cx| {
1969                project.rename_entry(edited_entry_id, new_project_path.clone(), cx)
1970            })
1971        };
1972
1973        if refocus {
1974            window.focus(&self.focus_handle, cx);
1975        }
1976        edit_state.processing_filename = Some(filename);
1977        cx.notify();
1978
1979        Some(cx.spawn_in(window, async move |project_panel, cx| {
1980            let new_entry = edit_task.await;
1981            project_panel.update(cx, |project_panel, cx| {
1982                project_panel.state.edit_state = None;
1983                cx.notify();
1984            })?;
1985
1986            match new_entry {
1987                Err(e) => {
1988                    project_panel
1989                        .update_in(cx, |project_panel, window, cx| {
1990                            project_panel.marked_entries.clear();
1991                            project_panel.update_visible_entries(None, false, false, window, cx);
1992                        })
1993                        .ok();
1994                    Err(e)?;
1995                }
1996                Ok(CreatedEntry::Included(new_entry)) => {
1997                    project_panel.update_in(cx, |project_panel, window, cx| {
1998                        // Only recording changes in included files as otherwise
1999                        // undoing some operations would fail, as
2000                        // `Worktree::entry_for_path` would return `None` for
2001                        // excluded paths.
2002                        // In the future we can look into adding support for recording
2003                        // changes in excluded paths, but that would mean updating
2004                        // methods that rely on `EntryId` to now rely on the actual
2005                        // paths.
2006                        let operation = match edited_entry {
2007                            Some(old_entry) => {
2008                                let project_path = (worktree_id, old_entry.path).into();
2009                                Change::Renamed(project_path, new_project_path)
2010                            }
2011                            None => Change::Created(new_project_path),
2012                        };
2013                        project_panel.undo_manager.record([operation]).log_err();
2014
2015                        if let Some(selection) = &mut project_panel.selection
2016                            && selection.entry_id == edited_entry_id
2017                        {
2018                            selection.worktree_id = worktree_id;
2019                            selection.entry_id = new_entry.id;
2020                            project_panel.marked_entries.clear();
2021                            project_panel.expand_to_selection(cx);
2022                        }
2023                        project_panel.update_visible_entries(None, false, false, window, cx);
2024                        if is_new_entry && !is_dir {
2025                            let settings = ProjectPanelSettings::get_global(cx);
2026                            if settings.auto_open.should_open_on_create() {
2027                                project_panel.open_entry(new_entry.id, true, false, cx);
2028                            }
2029                        }
2030                        cx.notify();
2031                    })?;
2032                }
2033                Ok(CreatedEntry::Excluded { abs_path }) => {
2034                    if let Some(open_task) = project_panel
2035                        .update_in(cx, |project_panel, window, cx| {
2036                            project_panel.marked_entries.clear();
2037                            project_panel.update_visible_entries(None, false, false, window, cx);
2038
2039                            if is_dir {
2040                                project_panel.project.update(cx, |_, cx| {
2041                                    cx.emit(project::Event::Toast {
2042                                        notification_id: "excluded-directory".into(),
2043                                        message: format!(
2044                                            concat!(
2045                                                "Created an excluded directory at {:?}.\n",
2046                                                "Alter `file_scan_exclusions` in the settings ",
2047                                                "to show it in the panel"
2048                                            ),
2049                                            abs_path
2050                                        ),
2051                                        link: None,
2052                                    })
2053                                });
2054                                None
2055                            } else {
2056                                project_panel
2057                                    .workspace
2058                                    .update(cx, |workspace, cx| {
2059                                        workspace.open_abs_path(
2060                                            abs_path,
2061                                            OpenOptions {
2062                                                visible: Some(OpenVisible::All),
2063                                                ..Default::default()
2064                                            },
2065                                            window,
2066                                            cx,
2067                                        )
2068                                    })
2069                                    .ok()
2070                            }
2071                        })
2072                        .ok()
2073                        .flatten()
2074                    {
2075                        let _ = open_task.await?;
2076                    }
2077                }
2078            }
2079            Ok(())
2080        }))
2081    }
2082
2083    async fn resolve_delete_entry(
2084        task: Option<RemoveEntryTask>,
2085        worktree_id: WorktreeId,
2086    ) -> DeleteEntryOutcome {
2087        let Some(task) = task else {
2088            // If there's no task to be awaited on, it means that the entry, or
2089            // worktree, were likely already deleted.
2090            return DeleteEntryOutcome::Deleted;
2091        };
2092
2093        match task {
2094            RemoveEntryTask::Trash(task) => match task.await {
2095                Ok(trash_id) => DeleteEntryOutcome::Trashed(Change::Trashed(worktree_id, trash_id)),
2096                Err(err) => {
2097                    log::error!("Failed to trash an entry: {err:#}",);
2098                    DeleteEntryOutcome::Failed
2099                }
2100            },
2101            RemoveEntryTask::Delete(task) => match task.await {
2102                Ok(()) => DeleteEntryOutcome::Deleted,
2103                Err(err) => {
2104                    log::error!("Failed to delete an entry: {err:#}",);
2105                    DeleteEntryOutcome::Failed
2106                }
2107            },
2108        }
2109    }
2110
2111    fn discard_edit_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2112        if let Some(edit_state) = self.state.edit_state.take() {
2113            self.state.temporarily_unfolded_pending_state = edit_state
2114                .temporarily_unfolded
2115                .and_then(|temporarily_unfolded_entry_id| {
2116                    let previously_focused_leaf_entry = edit_state.previously_focused?;
2117                    let folded_ancestors =
2118                        self.state.ancestors.get(&temporarily_unfolded_entry_id)?;
2119                    Some(TemporaryUnfoldedPendingState {
2120                        previously_focused_leaf_entry,
2121                        temporarily_unfolded_active_entry_id: folded_ancestors
2122                            .active_ancestor()
2123                            .unwrap_or(temporarily_unfolded_entry_id),
2124                    })
2125                });
2126            let previously_focused = edit_state
2127                .previously_focused
2128                .map(|entry| (entry.worktree_id, entry.entry_id));
2129            self.update_visible_entries(
2130                previously_focused,
2131                false,
2132                previously_focused.is_some(),
2133                window,
2134                cx,
2135            );
2136        }
2137    }
2138
2139    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
2140        if cx.stop_active_drag(window) {
2141            self.clear_drag_state(cx);
2142            return;
2143        }
2144        self.marked_entries.clear();
2145        cx.notify();
2146        self.discard_edit_state(window, cx);
2147        window.focus(&self.focus_handle, cx);
2148    }
2149
2150    fn open_entry(
2151        &mut self,
2152        entry_id: ProjectEntryId,
2153        focus_opened_item: bool,
2154        allow_preview: bool,
2155
2156        cx: &mut Context<Self>,
2157    ) {
2158        cx.emit(Event::OpenedEntry {
2159            entry_id,
2160            focus_opened_item,
2161            allow_preview,
2162        });
2163    }
2164
2165    fn split_entry(
2166        &mut self,
2167        entry_id: ProjectEntryId,
2168        allow_preview: bool,
2169        split_direction: Option<SplitDirection>,
2170
2171        cx: &mut Context<Self>,
2172    ) {
2173        cx.emit(Event::SplitEntry {
2174            entry_id,
2175            allow_preview,
2176            split_direction,
2177        });
2178    }
2179
2180    fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
2181        self.add_entry(false, window, cx)
2182    }
2183
2184    fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
2185        self.add_entry(true, window, cx)
2186    }
2187
2188    fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
2189        let Some((worktree_id, entry_id)) = self
2190            .selection
2191            .map(|entry| (entry.worktree_id, entry.entry_id))
2192            .or_else(|| {
2193                let entry_id = self.state.last_worktree_root_id?;
2194                let worktree_id = self
2195                    .project
2196                    .read(cx)
2197                    .worktree_for_entry(entry_id, cx)?
2198                    .read(cx)
2199                    .id();
2200
2201                self.selection = Some(SelectedEntry {
2202                    worktree_id,
2203                    entry_id,
2204                });
2205
2206                Some((worktree_id, entry_id))
2207            })
2208        else {
2209            return;
2210        };
2211
2212        let directory_id;
2213        let new_entry_id = self.resolve_entry(entry_id);
2214        if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
2215            let worktree = worktree.read(cx);
2216            let expanded_dir_ids = match self.state.expanded_dir_ids.entry(worktree_id) {
2217                hash_map::Entry::Occupied(entry) => entry.into_mut(),
2218                hash_map::Entry::Vacant(entry) => {
2219                    let Some(root_entry_id) = worktree.root_entry().map(|entry| entry.id) else {
2220                        return;
2221                    };
2222                    entry.insert(vec![root_entry_id])
2223                }
2224            };
2225
2226            if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
2227                loop {
2228                    if entry.is_dir() {
2229                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2230                            expanded_dir_ids.insert(ix, entry.id);
2231                        }
2232                        directory_id = entry.id;
2233                        break;
2234                    } else {
2235                        if let Some(parent_path) = entry.path.parent()
2236                            && let Some(parent_entry) = worktree.entry_for_path(parent_path)
2237                        {
2238                            entry = parent_entry;
2239                            continue;
2240                        }
2241                        return;
2242                    }
2243                }
2244            } else {
2245                return;
2246            };
2247        } else {
2248            return;
2249        };
2250
2251        self.marked_entries.clear();
2252        self.state.edit_state = Some(EditState {
2253            worktree_id,
2254            entry_id: directory_id,
2255            leaf_entry_id: None,
2256            is_dir,
2257            processing_filename: None,
2258            previously_focused: self.selection,
2259            depth: 0,
2260            validation_state: ValidationState::None,
2261            temporarily_unfolded: (new_entry_id != entry_id).then_some(new_entry_id),
2262        });
2263        self.filename_editor.update(cx, |editor, cx| {
2264            editor.clear(window, cx);
2265        });
2266        self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), true, true, window, cx);
2267        cx.notify();
2268    }
2269
2270    fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
2271        if let Some(ancestors) = self.state.ancestors.get(&leaf_entry_id) {
2272            ancestors
2273                .ancestors
2274                .get(ancestors.current_ancestor_depth)
2275                .copied()
2276                .unwrap_or(leaf_entry_id)
2277        } else {
2278            leaf_entry_id
2279        }
2280    }
2281
2282    pub fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context<Self>) {
2283        self.undo_manager.undo().log_err();
2284    }
2285
2286    pub fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context<Self>) {
2287        self.undo_manager.redo().log_err();
2288    }
2289
2290    fn rename_impl(
2291        &mut self,
2292        selection: Option<Range<usize>>,
2293        window: &mut Window,
2294        cx: &mut Context<Self>,
2295    ) {
2296        if let Some(SelectedEntry {
2297            worktree_id,
2298            entry_id,
2299        }) = self.selection
2300            && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx)
2301        {
2302            let sub_entry_id = self.unflatten_entry_id(entry_id);
2303            if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
2304                #[cfg(target_os = "windows")]
2305                if Some(entry) == worktree.read(cx).root_entry() {
2306                    return;
2307                }
2308
2309                if Some(entry) == worktree.read(cx).root_entry() {
2310                    let settings = ProjectPanelSettings::get_global(cx);
2311                    let visible_worktrees_count =
2312                        self.project.read(cx).visible_worktrees(cx).count();
2313                    if settings.hide_root && visible_worktrees_count == 1 {
2314                        return;
2315                    }
2316                }
2317
2318                self.state.edit_state = Some(EditState {
2319                    worktree_id,
2320                    entry_id: sub_entry_id,
2321                    leaf_entry_id: Some(entry_id),
2322                    is_dir: entry.is_dir(),
2323                    processing_filename: None,
2324                    previously_focused: None,
2325                    depth: 0,
2326                    validation_state: ValidationState::None,
2327                    temporarily_unfolded: None,
2328                });
2329                let file_name = entry.path.file_name().unwrap_or_default().to_string();
2330                let selection = selection.unwrap_or_else(|| {
2331                    // Folders have no extension, so select the whole name. Only
2332                    // files keep their extension unselected for quick renames.
2333                    let selection_end = if entry.is_dir() {
2334                        file_name.len()
2335                    } else {
2336                        let file_stem = entry.path.file_stem();
2337                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len())
2338                    };
2339                    0..selection_end
2340                });
2341                self.filename_editor.update(cx, |editor, cx| {
2342                    editor.set_text(file_name, window, cx);
2343                    editor.change_selections(Default::default(), window, cx, |s| {
2344                        s.select_ranges([
2345                            MultiBufferOffset(selection.start)..MultiBufferOffset(selection.end)
2346                        ])
2347                    });
2348                });
2349                self.update_visible_entries(None, true, true, window, cx);
2350                cx.notify();
2351            }
2352        }
2353    }
2354
2355    fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
2356        self.rename_impl(None, window, cx);
2357    }
2358
2359    fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
2360        self.remove(true, action.skip_prompt, window, cx);
2361    }
2362
2363    fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
2364        self.remove(false, action.skip_prompt, window, cx);
2365    }
2366
2367    fn restore_file(
2368        &mut self,
2369        action: &git::RestoreFile,
2370        window: &mut Window,
2371        cx: &mut Context<Self>,
2372    ) {
2373        maybe!({
2374            let selection = self.selection?;
2375            let project = self.project.read(cx);
2376
2377            let (_worktree, entry) = self.selected_sub_entry(cx)?;
2378            if entry.is_dir() {
2379                return None;
2380            }
2381
2382            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2383
2384            let git_store = project.git_store();
2385            let (repository, repo_path) = git_store
2386                .read(cx)
2387                .repository_and_path_for_project_path(&project_path, cx)?;
2388
2389            let snapshot = repository.read(cx).snapshot();
2390            let status = snapshot.status_for_path(&repo_path)?;
2391            if !status.status.is_modified() && !status.status.is_deleted() {
2392                return None;
2393            }
2394
2395            let file_name = entry.path.file_name()?.to_string();
2396
2397            let answer = if !action.skip_prompt {
2398                let prompt = format!("Discard changes to {}?", MarkdownInlineCode(&file_name));
2399                Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
2400            } else {
2401                None
2402            };
2403
2404            cx.spawn_in(window, async move |panel, cx| {
2405                if let Some(answer) = answer
2406                    && answer.await != Ok(0)
2407                {
2408                    return anyhow::Ok(());
2409                }
2410
2411                let task = panel.update(cx, |_panel, cx| {
2412                    repository.update(cx, |repo, cx| {
2413                        repo.checkout_files("HEAD", vec![repo_path], cx)
2414                    })
2415                })?;
2416
2417                if let Err(e) = task.await {
2418                    panel
2419                        .update(cx, |panel, cx| {
2420                            let message = format!("Failed to restore {}: {}", file_name, e);
2421                            let toast = StatusToast::new(message, cx, |this, _| {
2422                                this.icon(
2423                                    Icon::new(IconName::XCircle)
2424                                        .size(IconSize::Small)
2425                                        .color(Color::Error),
2426                                )
2427                                .dismiss_button(true)
2428                            });
2429                            panel
2430                                .workspace
2431                                .update(cx, |workspace, cx| {
2432                                    workspace.toggle_status_toast(toast, cx);
2433                                })
2434                                .ok();
2435                        })
2436                        .ok();
2437                }
2438
2439                panel
2440                    .update(cx, |panel, cx| {
2441                        panel.project.update(cx, |project, cx| {
2442                            if let Some(buffer_id) = project
2443                                .buffer_store()
2444                                .read(cx)
2445                                .buffer_id_for_project_path(&project_path)
2446                            {
2447                                if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
2448                                    buffer.update(cx, |buffer, cx| {
2449                                        let _ = buffer.reload(cx);
2450                                    });
2451                                }
2452                            }
2453                        })
2454                    })
2455                    .ok();
2456
2457                anyhow::Ok(())
2458            })
2459            .detach_and_log_err(cx);
2460
2461            Some(())
2462        });
2463    }
2464
2465    fn add_to_gitignore(
2466        &mut self,
2467        _: &git::AddToGitignore,
2468        _window: &mut Window,
2469        cx: &mut Context<Self>,
2470    ) {
2471        maybe!({
2472            let selection = self.selection?;
2473            let (_, entry) = self.selected_sub_entry(cx)?;
2474            let is_dir = entry.is_dir();
2475            let project = self.project.read(cx);
2476
2477            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2478
2479            let git_store = project.git_store();
2480            let (repository, repo_path) = git_store
2481                .read(cx)
2482                .repository_and_path_for_project_path(&project_path, cx)?;
2483
2484            let workspace = self.workspace.clone();
2485            let receiver =
2486                repository.update(cx, |repo, _| repo.add_path_to_gitignore(&repo_path, is_dir));
2487
2488            cx.spawn(async move |_, cx| {
2489                if let Err(e) = receiver.await? {
2490                    if let Some(workspace) = workspace.upgrade() {
2491                        cx.update(|cx| {
2492                            let message = format!("Failed to add to .gitignore: {}", e);
2493                            let toast = StatusToast::new(message, cx, |this, _| {
2494                                this.icon(Icon::new(IconName::XCircle).color(Color::Error))
2495                                    .dismiss_button(true)
2496                            });
2497                            workspace.update(cx, |workspace, cx| {
2498                                workspace.toggle_status_toast(toast, cx);
2499                            });
2500                        });
2501                    }
2502                }
2503                anyhow::Ok(())
2504            })
2505            .detach_and_log_err(cx);
2506
2507            Some(())
2508        });
2509    }
2510
2511    fn add_to_git_info_exclude(
2512        &mut self,
2513        _: &git::AddToGitInfoExclude,
2514        _window: &mut Window,
2515        cx: &mut Context<Self>,
2516    ) {
2517        maybe!({
2518            let selection = self.selection?;
2519            let (_, entry) = self.selected_sub_entry(cx)?;
2520            let is_dir = entry.is_dir();
2521            let project = self.project.read(cx);
2522
2523            let project_path = project.path_for_entry(selection.entry_id, cx)?;
2524
2525            let git_store = project.git_store();
2526            let (repository, repo_path) = git_store
2527                .read(cx)
2528                .repository_and_path_for_project_path(&project_path, cx)?;
2529
2530            let workspace = self.workspace.clone();
2531            let receiver = repository.update(cx, |repo, _| {
2532                repo.add_path_to_git_info_exclude(&repo_path, is_dir)
2533            });
2534
2535            cx.spawn(async move |_, cx| {
2536                if let Err(e) = receiver.await? {
2537                    if let Some(workspace) = workspace.upgrade() {
2538                        cx.update(|cx| {
2539                            let message = format!("Failed to add to .git/info/exclude: {}", e);
2540                            let toast = StatusToast::new(message, cx, |this, _| {
2541                                this.icon(Icon::new(IconName::XCircle).color(Color::Error))
2542                                    .dismiss_button(true)
2543                            });
2544                            workspace.update(cx, |workspace, cx| {
2545                                workspace.toggle_status_toast(toast, cx);
2546                            });
2547                        });
2548                    }
2549                }
2550                anyhow::Ok(())
2551            })
2552            .detach_and_log_err(cx);
2553
2554            Some(())
2555        });
2556    }
2557
2558    // TODO(yara|dino): trashing and deleting are conceptually distinct, even
2559    // more so with the fact that trashing can now be undone, whereas deleting
2560    // cannot.
2561    // Worth splitting them, and exploring dropping the trash confirmation now
2562    // that trash is undoable.
2563    fn remove(
2564        &mut self,
2565        trash: bool,
2566        skip_prompt: bool,
2567        window: &mut Window,
2568        cx: &mut Context<ProjectPanel>,
2569    ) {
2570        maybe!({
2571            let items_to_delete = self.disjoint_effective_entries_excluding_roots(cx);
2572            if items_to_delete.is_empty() {
2573                return None;
2574            }
2575            let project = self.project.read(cx);
2576
2577            let mut dirty_buffers = 0;
2578            let file_paths = items_to_delete
2579                .iter()
2580                .filter_map(|selection| {
2581                    let project_path = project.path_for_entry(selection.entry_id, cx)?;
2582                    dirty_buffers +=
2583                        project.dirty_buffers(cx).any(|path| path == project_path) as usize;
2584
2585                    Some((
2586                        selection.entry_id,
2587                        selection.worktree_id,
2588                        project_path.path.file_name()?.to_string(),
2589                    ))
2590                })
2591                .collect::<Vec<_>>();
2592            if file_paths.is_empty() {
2593                return None;
2594            }
2595            let answer = if !skip_prompt {
2596                let operation = if trash { "Trash" } else { "Delete" };
2597                let message_start = if trash {
2598                    "Do you want to trash"
2599                } else {
2600                    "Are you sure you want to permanently delete"
2601                };
2602                let prompt = match file_paths.first() {
2603                    Some((_, _, path)) if file_paths.len() == 1 => {
2604                        let unsaved_warning = if dirty_buffers > 0 {
2605                            "\n\nIt has unsaved changes, which will be lost."
2606                        } else {
2607                            ""
2608                        };
2609
2610                        format!(
2611                            "{message_start} {}?{unsaved_warning}",
2612                            MarkdownInlineCode(path)
2613                        )
2614                    }
2615                    _ => {
2616                        const CUTOFF_POINT: usize = 10;
2617                        let names = if file_paths.len() > CUTOFF_POINT {
2618                            let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
2619                            let mut paths = file_paths
2620                                .iter()
2621                                .map(|(_, _, path)| MarkdownInlineCode(path).to_string())
2622                                .take(CUTOFF_POINT)
2623                                .collect::<Vec<String>>();
2624                            paths.truncate(CUTOFF_POINT);
2625                            if truncated_path_counts == 1 {
2626                                paths.push(".. 1 file not shown".into());
2627                            } else {
2628                                paths.push(format!(".. {} files not shown", truncated_path_counts));
2629                            }
2630                            paths
2631                        } else {
2632                            file_paths
2633                                .iter()
2634                                .map(|(_, _, path)| MarkdownInlineCode(path).to_string())
2635                                .collect()
2636                        };
2637                        let unsaved_warning = if dirty_buffers == 0 {
2638                            String::new()
2639                        } else if dirty_buffers == 1 {
2640                            "\n\n1 of these has unsaved changes, which will be lost.".to_string()
2641                        } else {
2642                            format!(
2643                                "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
2644                            )
2645                        };
2646
2647                        format!(
2648                            "{message_start} the following {} files?\n{}{unsaved_warning}",
2649                            file_paths.len(),
2650                            names.join("\n")
2651                        )
2652                    }
2653                };
2654                let detail = (!trash).then_some("This cannot be undone.");
2655                Some(window.prompt(
2656                    PromptLevel::Info,
2657                    &prompt,
2658                    detail,
2659                    &[operation, "Cancel"],
2660                    cx,
2661                ))
2662            } else {
2663                None
2664            };
2665            let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
2666            cx.spawn_in(window, async move |panel, cx| {
2667                if let Some(answer) = answer
2668                    && answer.await != Ok(0)
2669                {
2670                    return anyhow::Ok(());
2671                }
2672
2673                let mut changes = Vec::new();
2674                let total_count = file_paths.len();
2675                let mut failed_count = 0;
2676
2677                let tasks = panel.update(cx, |panel, cx| {
2678                    panel.project.update(cx, |project, cx| {
2679                        file_paths
2680                            .into_iter()
2681                            .map(|(entry_id, worktree_id, _)| {
2682                                let task = if trash {
2683                                    project
2684                                        .trash_entry(entry_id, cx)
2685                                        .map(RemoveEntryTask::Trash)
2686                                } else {
2687                                    project
2688                                        .delete_entry(entry_id, cx)
2689                                        .map(RemoveEntryTask::Delete)
2690                                };
2691
2692                                (worktree_id, task)
2693                            })
2694                            .collect::<Vec<_>>()
2695                    })
2696                })?;
2697
2698                for (worktree_id, task) in tasks {
2699                    match Self::resolve_delete_entry(task, worktree_id).await {
2700                        DeleteEntryOutcome::Deleted => {}
2701                        DeleteEntryOutcome::Trashed(change) => changes.push(change),
2702                        DeleteEntryOutcome::Failed => failed_count += 1,
2703                    }
2704                }
2705
2706                panel.update_in(cx, |panel, window, cx| {
2707                    if trash {
2708                        panel.undo_manager.record(changes).log_err();
2709                    }
2710
2711                    if failed_count > 0 {
2712                        panel.show_remove_failure_toast(trash, total_count, failed_count, cx);
2713                    }
2714
2715                    if let Some(next_selection) = next_selection {
2716                        panel.update_visible_entries(
2717                            Some((next_selection.worktree_id, next_selection.entry_id)),
2718                            false,
2719                            true,
2720                            window,
2721                            cx,
2722                        );
2723                    } else {
2724                        panel.select_last(&SelectLast {}, window, cx);
2725                    }
2726                })?;
2727                Ok(())
2728            })
2729            .detach_and_log_err(cx);
2730            Some(())
2731        });
2732    }
2733
2734    /// Displays a toast notification, informing that the application was not
2735    /// able to delete/trash some of the files the user intended to
2736    /// delete/trash.
2737    fn show_remove_failure_toast(
2738        &self,
2739        trash: bool,
2740        total_count: usize,
2741        failed_count: usize,
2742        cx: &mut Context<Self>,
2743    ) {
2744        let message = match (trash, total_count) {
2745            (true, 1) => format!("Failed to trash {failed_count} of {total_count} file."),
2746            (true, _) => format!("Failed to trash {failed_count} of {total_count} files."),
2747            (false, 1) => format!("Failed to delete {failed_count} of {total_count} file."),
2748            (false, _) => format!("Failed to delete {failed_count} of {total_count} files."),
2749        };
2750
2751        let toast = StatusToast::new(message, cx, |this, _| {
2752            this.icon(
2753                Icon::new(IconName::XCircle)
2754                    .size(IconSize::Small)
2755                    .color(Color::Error),
2756            )
2757            .dismiss_button(true)
2758        });
2759
2760        self.workspace
2761            .update(cx, |workspace, cx| {
2762                workspace.toggle_status_toast(toast, cx);
2763            })
2764            .ok();
2765    }
2766
2767    fn find_next_selection_after_deletion(
2768        &self,
2769        sanitized_entries: BTreeSet<SelectedEntry>,
2770        cx: &mut Context<Self>,
2771    ) -> Option<SelectedEntry> {
2772        if sanitized_entries.is_empty() {
2773            return None;
2774        }
2775        let project = self.project.read(cx);
2776        let (worktree_id, worktree) = sanitized_entries
2777            .iter()
2778            .map(|entry| entry.worktree_id)
2779            .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
2780            .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
2781        let git_store = project.git_store().read(cx);
2782
2783        let marked_entries_in_worktree = sanitized_entries
2784            .iter()
2785            .filter(|e| e.worktree_id == worktree_id)
2786            .collect::<HashSet<_>>();
2787        let latest_entry = marked_entries_in_worktree
2788            .iter()
2789            .max_by(|a, b| {
2790                match (
2791                    worktree.entry_for_id(a.entry_id),
2792                    worktree.entry_for_id(b.entry_id),
2793                ) {
2794                    (Some(a), Some(b)) => compare_paths(
2795                        (a.path.as_std_path(), a.is_file()),
2796                        (b.path.as_std_path(), b.is_file()),
2797                    ),
2798                    _ => cmp::Ordering::Equal,
2799                }
2800            })
2801            .and_then(|e| worktree.entry_for_id(e.entry_id))?;
2802
2803        let parent_path = latest_entry.path.parent()?;
2804        let parent_entry = worktree.entry_for_path(parent_path)?;
2805
2806        // Remove all siblings that are being deleted except the last marked entry
2807        let repo_snapshots = git_store.repo_snapshots(cx);
2808        let worktree_snapshot = worktree.snapshot();
2809        let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
2810        let mut siblings: Vec<_> =
2811            ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
2812                .filter(|sibling| {
2813                    (sibling.id == latest_entry.id)
2814                        || (!marked_entries_in_worktree.contains(&&SelectedEntry {
2815                            worktree_id,
2816                            entry_id: sibling.id,
2817                        }) && (!hide_gitignore || !sibling.is_ignored))
2818                })
2819                .map(|entry| entry.to_owned())
2820                .collect();
2821
2822        let sort_mode = ProjectPanelSettings::get_global(cx).sort_mode;
2823        let sort_order = ProjectPanelSettings::get_global(cx).sort_order;
2824        sort_worktree_entries(&mut siblings, sort_mode, sort_order);
2825        let sibling_entry_index = siblings
2826            .iter()
2827            .position(|sibling| sibling.id == latest_entry.id)?;
2828
2829        if let Some(next_sibling) = sibling_entry_index
2830            .checked_add(1)
2831            .and_then(|i| siblings.get(i))
2832        {
2833            return Some(SelectedEntry {
2834                worktree_id,
2835                entry_id: next_sibling.id,
2836            });
2837        }
2838        if let Some(prev_sibling) = sibling_entry_index
2839            .checked_sub(1)
2840            .and_then(|i| siblings.get(i))
2841        {
2842            return Some(SelectedEntry {
2843                worktree_id,
2844                entry_id: prev_sibling.id,
2845            });
2846        }
2847        // No neighbour sibling found, fall back to parent
2848        Some(SelectedEntry {
2849            worktree_id,
2850            entry_id: parent_entry.id,
2851        })
2852    }
2853
2854    fn unfold_directory(
2855        &mut self,
2856        _: &UnfoldDirectory,
2857        window: &mut Window,
2858        cx: &mut Context<Self>,
2859    ) {
2860        if let Some((worktree, entry)) = self.selected_entry(cx) {
2861            self.state.unfolded_dir_ids.insert(entry.id);
2862
2863            let snapshot = worktree.snapshot();
2864            let mut parent_path = entry.path.parent();
2865            while let Some(path) = parent_path {
2866                if let Some(parent_entry) = worktree.entry_for_path(path) {
2867                    let mut children_iter = snapshot.child_entries(path);
2868
2869                    if children_iter.by_ref().take(2).count() > 1 {
2870                        break;
2871                    }
2872
2873                    self.state.unfolded_dir_ids.insert(parent_entry.id);
2874                    parent_path = path.parent();
2875                } else {
2876                    break;
2877                }
2878            }
2879
2880            self.update_visible_entries(None, false, true, window, cx);
2881            cx.notify();
2882        }
2883    }
2884
2885    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
2886        if let Some((worktree, entry)) = self.selected_entry(cx) {
2887            self.state.unfolded_dir_ids.remove(&entry.id);
2888
2889            let snapshot = worktree.snapshot();
2890            let mut path = &*entry.path;
2891            loop {
2892                let mut child_entries_iter = snapshot.child_entries(path);
2893                if let Some(child) = child_entries_iter.next() {
2894                    if child_entries_iter.next().is_none() && child.is_dir() {
2895                        self.state.unfolded_dir_ids.remove(&child.id);
2896                        path = &*child.path;
2897                    } else {
2898                        break;
2899                    }
2900                } else {
2901                    break;
2902                }
2903            }
2904
2905            self.update_visible_entries(None, false, true, window, cx);
2906            cx.notify();
2907        }
2908    }
2909
2910    fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context<Self>) {
2911        for _ in 0..self.rendered_entries_len / 2 {
2912            window.dispatch_action(SelectPrevious.boxed_clone(), cx);
2913        }
2914    }
2915
2916    fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context<Self>) {
2917        for _ in 0..self.rendered_entries_len / 2 {
2918            window.dispatch_action(SelectNext.boxed_clone(), cx);
2919        }
2920    }
2921
2922    fn scroll_cursor_center(
2923        &mut self,
2924        _: &ScrollCursorCenter,
2925        _: &mut Window,
2926        cx: &mut Context<Self>,
2927    ) {
2928        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2929            self.scroll_handle
2930                .scroll_to_item_strict(index, ScrollStrategy::Center);
2931            cx.notify();
2932        }
2933    }
2934
2935    fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context<Self>) {
2936        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2937            self.scroll_handle
2938                .scroll_to_item_strict(index, ScrollStrategy::Top);
2939            cx.notify();
2940        }
2941    }
2942
2943    fn scroll_cursor_bottom(
2944        &mut self,
2945        _: &ScrollCursorBottom,
2946        _: &mut Window,
2947        cx: &mut Context<Self>,
2948    ) {
2949        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2950            self.scroll_handle
2951                .scroll_to_item_strict(index, ScrollStrategy::Bottom);
2952            cx.notify();
2953        }
2954    }
2955
2956    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2957        if let Some(edit_state) = &self.state.edit_state
2958            && edit_state.processing_filename.is_none()
2959        {
2960            self.filename_editor.update(cx, |editor, cx| {
2961                editor.move_to_end_of_line(
2962                    &editor::actions::MoveToEndOfLine {
2963                        stop_at_soft_wraps: false,
2964                    },
2965                    window,
2966                    cx,
2967                );
2968            });
2969            return;
2970        }
2971        if let Some(selection) = self.selection {
2972            let (mut worktree_ix, mut entry_ix, _) =
2973                self.index_for_selection(selection).unwrap_or_default();
2974            if let Some(worktree_entries) = self
2975                .state
2976                .visible_entries
2977                .get(worktree_ix)
2978                .map(|v| &v.entries)
2979            {
2980                if entry_ix + 1 < worktree_entries.len() {
2981                    entry_ix += 1;
2982                } else {
2983                    worktree_ix += 1;
2984                    entry_ix = 0;
2985                }
2986            }
2987
2988            if let Some(VisibleEntriesForWorktree {
2989                worktree_id,
2990                entries,
2991                ..
2992            }) = self.state.visible_entries.get(worktree_ix)
2993                && let Some(entry) = entries.get(entry_ix)
2994            {
2995                let selection = SelectedEntry {
2996                    worktree_id: *worktree_id,
2997                    entry_id: entry.id,
2998                };
2999                self.selection = Some(selection);
3000                if window.modifiers().shift {
3001                    self.marked_entries.push(selection);
3002                }
3003
3004                self.autoscroll(cx);
3005                cx.notify();
3006            }
3007        } else {
3008            self.select_first(&SelectFirst {}, window, cx);
3009        }
3010    }
3011
3012    fn select_prev_diagnostic(
3013        &mut self,
3014        action: &SelectPrevDiagnostic,
3015        window: &mut Window,
3016        cx: &mut Context<Self>,
3017    ) {
3018        let selection = self.find_entry(
3019            self.selection.as_ref(),
3020            true,
3021            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3022                self.selection.is_none_or(|selection| {
3023                    if selection.worktree_id == worktree_id {
3024                        selection.entry_id != entry.id
3025                    } else {
3026                        true
3027                    }
3028                }) && entry.is_file()
3029                    && self
3030                        .diagnostics
3031                        .get(&(worktree_id, entry.path.clone()))
3032                        .is_some_and(|severity| action.severity.matches(*severity))
3033            },
3034            cx,
3035        );
3036
3037        if let Some(selection) = selection {
3038            self.selection = Some(selection);
3039            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
3040            self.update_visible_entries(
3041                Some((selection.worktree_id, selection.entry_id)),
3042                false,
3043                true,
3044                window,
3045                cx,
3046            );
3047            cx.notify();
3048        }
3049    }
3050
3051    fn select_next_diagnostic(
3052        &mut self,
3053        action: &SelectNextDiagnostic,
3054        window: &mut Window,
3055        cx: &mut Context<Self>,
3056    ) {
3057        let selection = self.find_entry(
3058            self.selection.as_ref(),
3059            false,
3060            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3061                self.selection.is_none_or(|selection| {
3062                    if selection.worktree_id == worktree_id {
3063                        selection.entry_id != entry.id
3064                    } else {
3065                        true
3066                    }
3067                }) && entry.is_file()
3068                    && self
3069                        .diagnostics
3070                        .get(&(worktree_id, entry.path.clone()))
3071                        .is_some_and(|severity| action.severity.matches(*severity))
3072            },
3073            cx,
3074        );
3075
3076        if let Some(selection) = selection {
3077            self.selection = Some(selection);
3078            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
3079            self.update_visible_entries(
3080                Some((selection.worktree_id, selection.entry_id)),
3081                false,
3082                true,
3083                window,
3084                cx,
3085            );
3086            cx.notify();
3087        }
3088    }
3089
3090    fn select_prev_git_entry(
3091        &mut self,
3092        _: &SelectPrevGitEntry,
3093        window: &mut Window,
3094        cx: &mut Context<Self>,
3095    ) {
3096        let selection = self.find_entry(
3097            self.selection.as_ref(),
3098            true,
3099            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3100                (self.selection.is_none()
3101                    || self.selection.is_some_and(|selection| {
3102                        if selection.worktree_id == worktree_id {
3103                            selection.entry_id != entry.id
3104                        } else {
3105                            true
3106                        }
3107                    }))
3108                    && entry.is_file()
3109                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
3110            },
3111            cx,
3112        );
3113
3114        if let Some(selection) = selection {
3115            self.selection = Some(selection);
3116            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
3117            self.update_visible_entries(
3118                Some((selection.worktree_id, selection.entry_id)),
3119                false,
3120                true,
3121                window,
3122                cx,
3123            );
3124            cx.notify();
3125        }
3126    }
3127
3128    fn select_prev_directory(
3129        &mut self,
3130        _: &SelectPrevDirectory,
3131        _: &mut Window,
3132        cx: &mut Context<Self>,
3133    ) {
3134        let selection = self.find_visible_entry(
3135            self.selection.as_ref(),
3136            true,
3137            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3138                self.selection.is_none_or(|selection| {
3139                    if selection.worktree_id == worktree_id {
3140                        selection.entry_id != entry.id
3141                    } else {
3142                        true
3143                    }
3144                }) && entry.is_dir()
3145            },
3146            cx,
3147        );
3148
3149        if let Some(selection) = selection {
3150            self.selection = Some(selection);
3151            self.autoscroll(cx);
3152            cx.notify();
3153        }
3154    }
3155
3156    fn select_next_directory(
3157        &mut self,
3158        _: &SelectNextDirectory,
3159        _: &mut Window,
3160        cx: &mut Context<Self>,
3161    ) {
3162        let selection = self.find_visible_entry(
3163            self.selection.as_ref(),
3164            false,
3165            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3166                self.selection.is_none_or(|selection| {
3167                    if selection.worktree_id == worktree_id {
3168                        selection.entry_id != entry.id
3169                    } else {
3170                        true
3171                    }
3172                }) && entry.is_dir()
3173            },
3174            cx,
3175        );
3176
3177        if let Some(selection) = selection {
3178            self.selection = Some(selection);
3179            self.autoscroll(cx);
3180            cx.notify();
3181        }
3182    }
3183
3184    fn select_next_git_entry(
3185        &mut self,
3186        _: &SelectNextGitEntry,
3187        window: &mut Window,
3188        cx: &mut Context<Self>,
3189    ) {
3190        let selection = self.find_entry(
3191            self.selection.as_ref(),
3192            false,
3193            &|entry: GitEntryRef, worktree_id: WorktreeId| {
3194                self.selection.is_none_or(|selection| {
3195                    if selection.worktree_id == worktree_id {
3196                        selection.entry_id != entry.id
3197                    } else {
3198                        true
3199                    }
3200                }) && entry.is_file()
3201                    && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
3202            },
3203            cx,
3204        );
3205
3206        if let Some(selection) = selection {
3207            self.selection = Some(selection);
3208            self.expand_entry(selection.worktree_id, selection.entry_id, cx);
3209            self.update_visible_entries(
3210                Some((selection.worktree_id, selection.entry_id)),
3211                false,
3212                true,
3213                window,
3214                cx,
3215            );
3216            cx.notify();
3217        }
3218    }
3219
3220    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
3221        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3222            if let Some(parent) = entry.path.parent() {
3223                let worktree = worktree.read(cx);
3224                if let Some(parent_entry) = worktree.entry_for_path(parent) {
3225                    self.selection = Some(SelectedEntry {
3226                        worktree_id: worktree.id(),
3227                        entry_id: parent_entry.id,
3228                    });
3229                    self.autoscroll(cx);
3230                    cx.notify();
3231                }
3232            }
3233        } else {
3234            self.select_first(&SelectFirst {}, window, cx);
3235        }
3236    }
3237
3238    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
3239        if let Some(VisibleEntriesForWorktree {
3240            worktree_id,
3241            entries,
3242            ..
3243        }) = self.state.visible_entries.first()
3244            && let Some(entry) = entries.first()
3245        {
3246            let selection = SelectedEntry {
3247                worktree_id: *worktree_id,
3248                entry_id: entry.id,
3249            };
3250            self.selection = Some(selection);
3251            if window.modifiers().shift {
3252                self.marked_entries.push(selection);
3253            }
3254            self.autoscroll(cx);
3255            cx.notify();
3256        }
3257    }
3258
3259    fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
3260        if let Some(VisibleEntriesForWorktree {
3261            worktree_id,
3262            entries,
3263            ..
3264        }) = self.state.visible_entries.last()
3265        {
3266            let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
3267            if let (Some(worktree), Some(entry)) = (worktree, entries.last()) {
3268                let worktree = worktree.read(cx);
3269                if let Some(entry) = worktree.entry_for_id(entry.id) {
3270                    let selection = SelectedEntry {
3271                        worktree_id: *worktree_id,
3272                        entry_id: entry.id,
3273                    };
3274                    self.selection = Some(selection);
3275                    self.autoscroll(cx);
3276                    cx.notify();
3277                }
3278            }
3279        }
3280    }
3281
3282    fn autoscroll(&mut self, cx: &mut Context<Self>) {
3283        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
3284            self.scroll_handle.scroll_to_item_with_offset(
3285                index,
3286                ScrollStrategy::Center,
3287                self.sticky_items_count,
3288            );
3289            cx.notify();
3290        }
3291    }
3292
3293    fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
3294        let entries = self.disjoint_effective_entries_excluding_roots(cx);
3295        if !entries.is_empty() {
3296            self.write_entries_to_system_clipboard(&entries, cx);
3297            self.clipboard = Some(ClipboardEntry::Cut(entries));
3298            cx.notify();
3299        }
3300    }
3301
3302    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
3303        let entries = self.disjoint_effective_entries_excluding_roots(cx);
3304        if !entries.is_empty() {
3305            self.write_entries_to_system_clipboard(&entries, cx);
3306            self.clipboard = Some(ClipboardEntry::Copied(entries));
3307            cx.notify();
3308        }
3309    }
3310
3311    fn create_paste_path(
3312        &self,
3313        source: &SelectedEntry,
3314        (worktree, target_entry): (Entity<Worktree>, &Entry),
3315        cx: &App,
3316    ) -> Option<(Arc<RelPath>, Option<Range<usize>>)> {
3317        let mut new_path = target_entry.path.to_rel_path_buf();
3318        // If we're pasting into a file, or a directory into itself, go up one level.
3319        if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
3320            new_path.pop();
3321        }
3322
3323        let source_worktree = self
3324            .project
3325            .read(cx)
3326            .worktree_for_entry(source.entry_id, cx)?;
3327        let source_entry = source_worktree.read(cx).entry_for_id(source.entry_id)?;
3328
3329        let clipboard_entry_file_name = source_entry.path.file_name()?.to_string();
3330        new_path.push(RelPath::from_unix_str(&clipboard_entry_file_name).unwrap());
3331
3332        let (extension, file_name_without_extension) = if source_entry.is_file() {
3333            (
3334                new_path.extension().map(|s| s.to_string()),
3335                new_path.file_stem()?.to_string(),
3336            )
3337        } else {
3338            (None, clipboard_entry_file_name.clone())
3339        };
3340
3341        let file_name_len = file_name_without_extension.len();
3342        let mut disambiguation_range = None;
3343        let mut ix = 0;
3344        {
3345            let worktree = worktree.read(cx);
3346            while worktree.entry_for_path(&new_path).is_some() {
3347                new_path.pop();
3348
3349                let mut new_file_name = file_name_without_extension.to_string();
3350
3351                let disambiguation = " copy";
3352                let mut disambiguation_len = disambiguation.len();
3353
3354                new_file_name.push_str(disambiguation);
3355
3356                if ix > 0 {
3357                    let extra_disambiguation = format!(" {}", ix);
3358                    disambiguation_len += extra_disambiguation.len();
3359                    new_file_name.push_str(&extra_disambiguation);
3360                }
3361                if let Some(extension) = extension.as_ref() {
3362                    new_file_name.push_str(".");
3363                    new_file_name.push_str(extension);
3364                }
3365
3366                new_path.push(RelPath::from_unix_str(&new_file_name).unwrap());
3367
3368                disambiguation_range = Some(0..(file_name_len + disambiguation_len));
3369                ix += 1;
3370            }
3371        }
3372        Some((new_path.as_rel_path().into(), disambiguation_range))
3373    }
3374
3375    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
3376        if let Some(external_paths) = self.external_paths_from_system_clipboard(cx) {
3377            let target_entry_id = self
3378                .selection
3379                .map(|s| s.entry_id)
3380                .or(self.state.last_worktree_root_id);
3381            if let Some(entry_id) = target_entry_id {
3382                self.drop_external_files(external_paths.paths(), entry_id, window, cx);
3383            }
3384            return;
3385        }
3386
3387        maybe!({
3388            let (worktree, entry) = self.selected_entry_handle(cx)?;
3389            let entry = entry.clone();
3390            let worktree_id = worktree.read(cx).id();
3391            let clipboard_entries = self
3392                .clipboard
3393                .as_ref()
3394                .filter(|clipboard| !clipboard.items().is_empty())?;
3395
3396            enum PasteTask {
3397                Rename {
3398                    task: Task<Result<CreatedEntry>>,
3399                    from: ProjectPath,
3400                    to: ProjectPath,
3401                },
3402                Copy {
3403                    task: Task<Result<Option<Entry>>>,
3404                    destination: ProjectPath,
3405                },
3406            }
3407
3408            let mut paste_tasks = Vec::new();
3409            let mut disambiguation_range = None;
3410            let clip_is_cut = clipboard_entries.is_cut();
3411            for clipboard_entry in clipboard_entries.items() {
3412                let (new_path, new_disambiguation_range) =
3413                    self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
3414                let clip_entry_id = clipboard_entry.entry_id;
3415                let destination: ProjectPath = (worktree_id, new_path).into();
3416                let task = if clipboard_entries.is_cut() {
3417                    let original_path = self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
3418                    let task = self.project.update(cx, |project, cx| {
3419                        project.rename_entry(clip_entry_id, destination.clone(), cx)
3420                    });
3421                    PasteTask::Rename {
3422                        task,
3423                        from: original_path,
3424                        to: destination,
3425                    }
3426                } else {
3427                    let task = self.project.update(cx, |project, cx| {
3428                        project.copy_entry(clip_entry_id, destination.clone(), cx)
3429                    });
3430                    PasteTask::Copy { task, destination }
3431                };
3432                paste_tasks.push(task);
3433                disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3434            }
3435
3436            let item_count = paste_tasks.len();
3437            let workspace = self.workspace.clone();
3438
3439            cx.spawn_in(window, async move |project_panel, mut cx| {
3440                let mut last_succeed = None;
3441                let mut changes = Vec::new();
3442
3443                for task in paste_tasks {
3444                    match task {
3445                        PasteTask::Rename { task, from, to } => {
3446                            if let Some(CreatedEntry::Included(entry)) = task
3447                                .await
3448                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3449                            {
3450                                changes.push(Change::Renamed(from, to));
3451                                last_succeed = Some(entry);
3452                            }
3453                        }
3454                        PasteTask::Copy { task, destination } => {
3455                            if let Some(Some(entry)) = task
3456                                .await
3457                                .notify_workspace_async_err(workspace.clone(), &mut cx)
3458                            {
3459                                changes.push(Change::Created(destination));
3460                                last_succeed = Some(entry);
3461                            }
3462                        }
3463                    }
3464                }
3465
3466                project_panel
3467                    .update(cx, |this, _| {
3468                        this.undo_manager.record(changes).log_err();
3469                    })
3470                    .ok();
3471
3472                // update selection
3473                if let Some(entry) = last_succeed {
3474                    project_panel
3475                        .update_in(cx, |project_panel, window, cx| {
3476                            project_panel.selection = Some(SelectedEntry {
3477                                worktree_id,
3478                                entry_id: entry.id,
3479                            });
3480
3481                            if item_count == 1 {
3482                                // open entry if not dir, setting is enabled, and only focus if rename is not pending
3483                                if !entry.is_dir() {
3484                                    let settings = ProjectPanelSettings::get_global(cx);
3485                                    if settings.auto_open.should_open_on_paste() {
3486                                        project_panel.open_entry(
3487                                            entry.id,
3488                                            disambiguation_range.is_none(),
3489                                            false,
3490                                            cx,
3491                                        );
3492                                    }
3493                                }
3494
3495                                // if only one entry was pasted and it was disambiguated, open the rename editor
3496                                if disambiguation_range.is_some() {
3497                                    cx.defer_in(window, |this, window, cx| {
3498                                        this.rename_impl(disambiguation_range, window, cx);
3499                                    });
3500                                }
3501                            }
3502                        })
3503                        .ok();
3504                }
3505
3506                anyhow::Ok(())
3507            })
3508            .detach_and_log_err(cx);
3509
3510            if clip_is_cut {
3511                // Convert the clipboard cut entry to a copy entry after the first paste.
3512                self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry);
3513            }
3514
3515            self.expand_entry(worktree_id, entry.id, cx);
3516            Some(())
3517        });
3518    }
3519
3520    fn download_from_remote(
3521        &mut self,
3522        _: &DownloadFromRemote,
3523        window: &mut Window,
3524        cx: &mut Context<Self>,
3525    ) {
3526        let entries = self.effective_entries();
3527        if entries.is_empty() {
3528            return;
3529        }
3530
3531        let project = self.project.read(cx);
3532
3533        // Collect file entries with their worktree_id, path, and relative path for destination
3534        // For directories, we collect all files under them recursively
3535        let mut files_to_download: Vec<(WorktreeId, Arc<RelPath>, PathBuf)> = Vec::new();
3536
3537        for selected in entries.iter() {
3538            let Some(worktree) = project.worktree_for_id(selected.worktree_id, cx) else {
3539                continue;
3540            };
3541            let worktree = worktree.read(cx);
3542            let Some(entry) = worktree.entry_for_id(selected.entry_id) else {
3543                continue;
3544            };
3545
3546            if entry.is_file() {
3547                // Single file: use just the filename
3548                let filename = entry
3549                    .path
3550                    .file_name()
3551                    .map(str::to_string)
3552                    .unwrap_or_default();
3553                files_to_download.push((
3554                    selected.worktree_id,
3555                    entry.path.clone(),
3556                    PathBuf::from(filename),
3557                ));
3558            } else if entry.is_dir() {
3559                // Directory: collect all files recursively, preserving relative paths
3560                let dir_name = entry
3561                    .path
3562                    .file_name()
3563                    .map(str::to_string)
3564                    .unwrap_or_default();
3565                let base_path = entry.path.clone();
3566
3567                // Use traverse_from_path to iterate all entries under this directory
3568                let mut traversal = worktree.traverse_from_path(true, true, true, &entry.path);
3569                while let Some(child_entry) = traversal.entry() {
3570                    // Stop when we're no longer under the directory
3571                    if !child_entry.path.starts_with(&base_path) {
3572                        break;
3573                    }
3574
3575                    if child_entry.is_file() {
3576                        // Calculate relative path from the directory root
3577                        let relative_path = child_entry
3578                            .path
3579                            .strip_prefix(&base_path)
3580                            .map(|p| PathBuf::from(dir_name.clone()).join(p.as_unix_str()))
3581                            .unwrap_or_else(|_| {
3582                                PathBuf::from(
3583                                    child_entry
3584                                        .path
3585                                        .file_name()
3586                                        .map(str::to_string)
3587                                        .unwrap_or_default(),
3588                                )
3589                            });
3590                        files_to_download.push((
3591                            selected.worktree_id,
3592                            child_entry.path.clone(),
3593                            relative_path,
3594                        ));
3595                    }
3596                    traversal.advance();
3597                }
3598            }
3599        }
3600
3601        if files_to_download.is_empty() {
3602            return;
3603        }
3604
3605        let total_files = files_to_download.len();
3606        let workspace = self.workspace.clone();
3607
3608        let destination_dir = cx.prompt_for_paths(PathPromptOptions {
3609            files: false,
3610            directories: true,
3611            multiple: false,
3612            prompt: Some("Download".into()),
3613        });
3614
3615        let fs = self.fs.clone();
3616        let notification_id =
3617            workspace::notifications::NotificationId::Named("download-progress".into());
3618        cx.spawn_in(window, async move |this, cx| {
3619            if let Ok(Ok(Some(mut paths))) = destination_dir.await {
3620                if let Some(dest_dir) = paths.pop() {
3621                    // Show initial toast
3622                    workspace
3623                        .update(cx, |workspace, cx| {
3624                            workspace.show_toast(
3625                                workspace::Toast::new(
3626                                    notification_id.clone(),
3627                                    format!("Downloading 0/{} files...", total_files),
3628                                ),
3629                                cx,
3630                            );
3631                        })
3632                        .ok();
3633
3634                    for (index, (worktree_id, entry_path, relative_path)) in
3635                        files_to_download.into_iter().enumerate()
3636                    {
3637                        // Update progress toast
3638                        workspace
3639                            .update(cx, |workspace, cx| {
3640                                workspace.show_toast(
3641                                    workspace::Toast::new(
3642                                        notification_id.clone(),
3643                                        format!(
3644                                            "Downloading {}/{} files...",
3645                                            index + 1,
3646                                            total_files
3647                                        ),
3648                                    ),
3649                                    cx,
3650                                );
3651                            })
3652                            .ok();
3653
3654                        let destination_path = dest_dir.join(&relative_path);
3655
3656                        // Create parent directories if needed
3657                        if let Some(parent) = destination_path.parent() {
3658                            if !parent.exists() {
3659                                fs.create_dir(parent).await.log_err();
3660                            }
3661                        }
3662
3663                        let download_task = this.update(cx, |this, cx| {
3664                            let project = this.project.clone();
3665                            project.update(cx, |project, cx| {
3666                                project.download_file(worktree_id, entry_path, destination_path, cx)
3667                            })
3668                        });
3669                        if let Ok(task) = download_task {
3670                            task.await.log_err();
3671                        }
3672                    }
3673
3674                    // Show completion toast
3675                    workspace
3676                        .update(cx, |workspace, cx| {
3677                            workspace.show_toast(
3678                                workspace::Toast::new(
3679                                    notification_id.clone(),
3680                                    format!("Downloaded {} files", total_files),
3681                                ),
3682                                cx,
3683                            );
3684                        })
3685                        .ok();
3686                }
3687            }
3688        })
3689        .detach();
3690    }
3691
3692    fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
3693        self.copy(&Copy {}, window, cx);
3694        self.paste(&Paste {}, window, cx);
3695    }
3696
3697    fn copy_path(
3698        &mut self,
3699        _: &zed_actions::workspace::CopyPath,
3700        _: &mut Window,
3701        cx: &mut Context<Self>,
3702    ) {
3703        let abs_file_paths = {
3704            let project = self.project.read(cx);
3705            self.effective_entries()
3706                .into_iter()
3707                .filter_map(|entry| {
3708                    let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
3709                    Some(
3710                        project
3711                            .worktree_for_id(entry.worktree_id, cx)?
3712                            .read(cx)
3713                            .absolutize(&entry_path)
3714                            .to_string_lossy()
3715                            .to_string(),
3716                    )
3717                })
3718                .collect::<Vec<_>>()
3719        };
3720        if !abs_file_paths.is_empty() {
3721            cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
3722        }
3723    }
3724
3725    fn copy_relative_path(
3726        &mut self,
3727        _: &zed_actions::workspace::CopyRelativePath,
3728        _: &mut Window,
3729        cx: &mut Context<Self>,
3730    ) {
3731        let path_style = self.project.read(cx).path_style(cx);
3732        let file_paths = {
3733            let project = self.project.read(cx);
3734            self.effective_entries()
3735                .into_iter()
3736                .filter_map(|entry| {
3737                    Some(
3738                        project
3739                            .path_for_entry(entry.entry_id, cx)?
3740                            .path
3741                            .display(path_style)
3742                            .into_owned(),
3743                    )
3744                })
3745                .collect::<Vec<_>>()
3746        };
3747        if !file_paths.is_empty() {
3748            cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
3749        }
3750    }
3751
3752    fn reveal_in_finder(
3753        &mut self,
3754        _: &RevealInFileManager,
3755        _: &mut Window,
3756        cx: &mut Context<Self>,
3757    ) {
3758        if let Some(path) = self.reveal_in_file_manager_path(cx) {
3759            self.project
3760                .update(cx, |project, cx| project.reveal_path(&path, cx));
3761        }
3762    }
3763
3764    fn remove_from_project(
3765        &mut self,
3766        _: &RemoveFromProject,
3767        _window: &mut Window,
3768        cx: &mut Context<Self>,
3769    ) {
3770        for entry in self.effective_entries().iter() {
3771            let worktree_id = entry.worktree_id;
3772            self.project
3773                .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3774        }
3775    }
3776
3777    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
3778        let mut selections_abs_path = self
3779            .marked_entries
3780            .iter()
3781            .filter_map(|entry| {
3782                let project = self.project.read(cx);
3783                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
3784                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
3785                if !entry.is_file() {
3786                    return None;
3787                }
3788                Some(worktree.read(cx).absolutize(&entry.path))
3789            })
3790            .rev();
3791
3792        let last_path = selections_abs_path.next()?;
3793        let previous_to_last = selections_abs_path.next()?;
3794        Some((previous_to_last, last_path))
3795    }
3796
3797    fn compare_marked_files(
3798        &mut self,
3799        _: &CompareMarkedFiles,
3800        window: &mut Window,
3801        cx: &mut Context<Self>,
3802    ) {
3803        let selected_files = self.file_abs_paths_to_diff(cx);
3804        if let Some((file_path1, file_path2)) = selected_files {
3805            self.workspace
3806                .update(cx, |workspace, cx| {
3807                    FileDiffView::open(
3808                        file_path1,
3809                        file_path2,
3810                        None,
3811                        workspace.weak_handle(),
3812                        window,
3813                        cx,
3814                    )
3815                    .detach_and_log_err(cx);
3816                })
3817                .ok();
3818        }
3819    }
3820
3821    fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
3822        if let Some((worktree, entry)) = self.selected_entry(cx) {
3823            let abs_path = worktree.absolutize(&entry.path);
3824            cx.open_with_system(&abs_path);
3825        }
3826    }
3827
3828    fn open_in_terminal(
3829        &mut self,
3830        _: &OpenInTerminal,
3831        window: &mut Window,
3832        cx: &mut Context<Self>,
3833    ) {
3834        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3835            let abs_path = match &entry.canonical_path {
3836                Some(canonical_path) => canonical_path.to_path_buf(),
3837                None => worktree.read(cx).absolutize(&entry.path),
3838            };
3839
3840            let working_directory = if entry.is_dir() {
3841                Some(abs_path)
3842            } else {
3843                abs_path.parent().map(|path| path.to_path_buf())
3844            };
3845            if let Some(working_directory) = working_directory {
3846                window.dispatch_action(
3847                    workspace::OpenTerminal {
3848                        working_directory,
3849                        local: false,
3850                    }
3851                    .boxed_clone(),
3852                    cx,
3853                )
3854            }
3855        }
3856    }
3857
3858    pub fn new_search_in_directory(
3859        &mut self,
3860        _: &NewSearchInDirectory,
3861        window: &mut Window,
3862        cx: &mut Context<Self>,
3863    ) {
3864        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
3865            let dir_path = if entry.is_dir() {
3866                entry.path.clone()
3867            } else {
3868                // entry is a file, use its parent directory
3869                match entry.path.parent() {
3870                    Some(parent) => Arc::from(parent),
3871                    None => {
3872                        // File at root, open search with empty filter
3873                        self.workspace
3874                            .update(cx, |workspace, cx| {
3875                                search::ProjectSearchView::new_search_in_directory(
3876                                    workspace,
3877                                    RelPath::empty(),
3878                                    window,
3879                                    cx,
3880                                );
3881                            })
3882                            .ok();
3883                        return;
3884                    }
3885                }
3886            };
3887
3888            let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
3889            let dir_path = if include_root {
3890                worktree.read(cx).root_name().join(&dir_path)
3891            } else {
3892                dir_path.to_rel_path_buf()
3893            };
3894
3895            self.workspace
3896                .update(cx, |workspace, cx| {
3897                    search::ProjectSearchView::new_search_in_directory(
3898                        workspace, &dir_path, window, cx,
3899                    );
3900                })
3901                .ok();
3902        }
3903    }
3904
3905    fn move_worktree_root(
3906        &mut self,
3907        entry_to_move: ProjectEntryId,
3908        destination: ProjectEntryId,
3909        cx: &mut Context<Self>,
3910    ) {
3911        self.project.update(cx, |project, cx| {
3912            let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
3913                return;
3914            };
3915            let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
3916                return;
3917            };
3918
3919            let worktree_id = worktree_to_move.read(cx).id();
3920            let destination_id = destination_worktree.read(cx).id();
3921
3922            project
3923                .move_worktree(worktree_id, destination_id, cx)
3924                .log_err();
3925        });
3926    }
3927
3928    fn move_worktree_entry(
3929        &mut self,
3930        entry_to_move: ProjectEntryId,
3931        destination_entry: ProjectEntryId,
3932        destination_is_file: bool,
3933        cx: &mut Context<Self>,
3934    ) -> Option<Task<Result<CreatedEntry>>> {
3935        if entry_to_move == destination_entry {
3936            return None;
3937        }
3938
3939        let (destination_worktree, rename_task) = self.project.update(cx, |project, cx| {
3940            let Some(source_path) = project.path_for_entry(entry_to_move, cx) else {
3941                return (None, None);
3942            };
3943            let Some(destination_path) = project.path_for_entry(destination_entry, cx) else {
3944                return (None, None);
3945            };
3946            let destination_worktree_id = destination_path.worktree_id;
3947
3948            let destination_dir = if destination_is_file {
3949                destination_path.path.parent().unwrap_or(RelPath::empty())
3950            } else {
3951                destination_path.path.as_ref()
3952            };
3953
3954            let Some(source_name) = source_path.path.file_name() else {
3955                return (None, None);
3956            };
3957            let Ok(source_name) = RelPath::from_unix_str(source_name) else {
3958                return (None, None);
3959            };
3960
3961            let mut new_path = destination_dir.to_rel_path_buf();
3962            new_path.push(source_name);
3963            let rename_task = (new_path.as_rel_path() != source_path.path.as_ref()).then(|| {
3964                project.rename_entry(
3965                    entry_to_move,
3966                    (destination_worktree_id, new_path).into(),
3967                    cx,
3968                )
3969            });
3970
3971            (
3972                project.worktree_id_for_entry(destination_entry, cx),
3973                rename_task,
3974            )
3975        });
3976
3977        if let Some(destination_worktree) = destination_worktree {
3978            self.expand_entry(destination_worktree, destination_entry, cx);
3979        }
3980        rename_task
3981    }
3982
3983    fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
3984        self.index_for_entry(selection.entry_id, selection.worktree_id)
3985    }
3986
3987    fn disjoint_effective_entries_excluding_roots(&self, cx: &App) -> BTreeSet<SelectedEntry> {
3988        let project = self.project.read(cx);
3989        let entries = self
3990            .effective_entries()
3991            .into_iter()
3992            .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
3993            .collect();
3994        self.disjoint_entries(entries, cx)
3995    }
3996
3997    fn disjoint_entries(
3998        &self,
3999        entries: BTreeSet<SelectedEntry>,
4000        cx: &App,
4001    ) -> BTreeSet<SelectedEntry> {
4002        let mut sanitized_entries = BTreeSet::new();
4003        if entries.is_empty() {
4004            return sanitized_entries;
4005        }
4006
4007        let project = self.project.read(cx);
4008        let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
4009            .into_iter()
4010            .fold(HashMap::default(), |mut map, entry| {
4011                map.entry(entry.worktree_id).or_default().push(entry);
4012                map
4013            });
4014
4015        for (worktree_id, worktree_entries) in entries_by_worktree {
4016            if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
4017                let worktree = worktree.read(cx);
4018                let dir_paths = worktree_entries
4019                    .iter()
4020                    .filter_map(|entry| {
4021                        worktree.entry_for_id(entry.entry_id).and_then(|entry| {
4022                            if entry.is_dir() {
4023                                Some(entry.path.as_ref())
4024                            } else {
4025                                None
4026                            }
4027                        })
4028                    })
4029                    .collect::<BTreeSet<_>>();
4030
4031                sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
4032                    let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
4033                        return false;
4034                    };
4035                    let entry_path = entry_info.path.as_ref();
4036                    let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
4037                        entry_path != dir_path && entry_path.starts_with(dir_path)
4038                    });
4039                    !inside_selected_dir
4040                }));
4041            }
4042        }
4043
4044        sanitized_entries
4045    }
4046
4047    fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
4048        if let Some(selection) = self.selection {
4049            let selection = SelectedEntry {
4050                entry_id: self.resolve_entry(selection.entry_id),
4051                worktree_id: selection.worktree_id,
4052            };
4053
4054            // Default to using just the selected item when nothing is marked.
4055            if self.marked_entries.is_empty() {
4056                return BTreeSet::from([selection]);
4057            }
4058
4059            // Allow operating on the selected item even when something else is marked,
4060            // making it easier to perform one-off actions without clearing a mark.
4061            if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
4062                return BTreeSet::from([selection]);
4063            }
4064        }
4065
4066        // Return only marked entries since we've already handled special cases where
4067        // only selection should take precedence. At this point, marked entries may or
4068        // may not include the current selection, which is intentional.
4069        self.marked_entries
4070            .iter()
4071            .map(|entry| SelectedEntry {
4072                entry_id: self.resolve_entry(entry.entry_id),
4073                worktree_id: entry.worktree_id,
4074            })
4075            .collect::<BTreeSet<_>>()
4076    }
4077
4078    /// Finds the currently selected subentry for a given leaf entry id. If a given entry
4079    /// has no ancestors, the project entry ID that's passed in is returned as-is.
4080    fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
4081        self.state
4082            .ancestors
4083            .get(&id)
4084            .and_then(|ancestors| ancestors.active_ancestor())
4085            .unwrap_or(id)
4086    }
4087
4088    pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
4089        let (worktree, entry) = self.selected_entry_handle(cx)?;
4090        Some((worktree.read(cx), entry))
4091    }
4092
4093    pub fn selected_entry_project_path(&self, cx: &App) -> Option<ProjectPath> {
4094        let (worktree, entry) = self.selected_sub_entry(cx)?;
4095        Some(ProjectPath {
4096            worktree_id: worktree.read(cx).id(),
4097            path: entry.path.clone(),
4098        })
4099    }
4100
4101    /// Compared to selected_entry, this function resolves to the currently
4102    /// selected subentry if dir auto-folding is enabled.
4103    fn selected_sub_entry<'a>(
4104        &self,
4105        cx: &'a App,
4106    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
4107        let (worktree, mut entry) = self.selected_entry_handle(cx)?;
4108
4109        let resolved_id = self.resolve_entry(entry.id);
4110        if resolved_id != entry.id {
4111            let worktree = worktree.read(cx);
4112            entry = worktree.entry_for_id(resolved_id)?;
4113        }
4114        Some((worktree, entry))
4115    }
4116
4117    fn reveal_in_file_manager_path(&self, cx: &App) -> Option<PathBuf> {
4118        if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
4119            return Some(worktree.read(cx).absolutize(&entry.path));
4120        }
4121
4122        let root_entry_id = self.state.last_worktree_root_id?;
4123        let project = self.project.read(cx);
4124        let worktree = project.worktree_for_entry(root_entry_id, cx)?;
4125        let worktree = worktree.read(cx);
4126        let root_entry = worktree.entry_for_id(root_entry_id)?;
4127        Some(worktree.absolutize(&root_entry.path))
4128    }
4129
4130    fn write_entries_to_system_clipboard(&self, entries: &BTreeSet<SelectedEntry>, cx: &mut App) {
4131        let project = self.project.read(cx);
4132        let paths: Vec<String> = entries
4133            .iter()
4134            .filter_map(|entry| {
4135                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
4136                let worktree = worktree.read(cx);
4137                let worktree_entry = worktree.entry_for_id(entry.entry_id)?;
4138                Some(
4139                    worktree
4140                        .abs_path()
4141                        .join(worktree_entry.path.as_std_path())
4142                        .to_string_lossy()
4143                        .to_string(),
4144                )
4145            })
4146            .collect();
4147        if !paths.is_empty() {
4148            cx.write_to_clipboard(ClipboardItem::new_string(paths.join("\n")));
4149        }
4150    }
4151
4152    fn external_paths_from_system_clipboard(&self, cx: &App) -> Option<ExternalPaths> {
4153        let clipboard_item = cx.read_from_clipboard()?;
4154        for entry in clipboard_item.entries() {
4155            if let GpuiClipboardEntry::ExternalPaths(paths) = entry {
4156                if !paths.paths().is_empty() {
4157                    return Some(paths.clone());
4158                }
4159            }
4160        }
4161        None
4162    }
4163
4164    fn has_pasteable_content(&self, cx: &App) -> bool {
4165        if self
4166            .clipboard
4167            .as_ref()
4168            .is_some_and(|c| !c.items().is_empty())
4169        {
4170            return true;
4171        }
4172        self.external_paths_from_system_clipboard(cx).is_some()
4173    }
4174
4175    fn selected_entry_handle<'a>(
4176        &self,
4177        cx: &'a App,
4178    ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
4179        let selection = self.selection?;
4180        let project = self.project.read(cx);
4181        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
4182        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
4183        Some((worktree, entry))
4184    }
4185
4186    fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
4187        let (worktree, entry) = self.selected_entry(cx)?;
4188        let expanded_dir_ids = self
4189            .state
4190            .expanded_dir_ids
4191            .entry(worktree.id())
4192            .or_default();
4193
4194        for path in entry.path.ancestors() {
4195            let Some(entry) = worktree.entry_for_path(path) else {
4196                continue;
4197            };
4198            if entry.is_dir()
4199                && let Err(idx) = expanded_dir_ids.binary_search(&entry.id)
4200            {
4201                expanded_dir_ids.insert(idx, entry.id);
4202            }
4203        }
4204
4205        Some(())
4206    }
4207
4208    fn create_new_git_entry(
4209        parent_entry: &Entry,
4210        git_summary: GitSummary,
4211        new_entry_kind: EntryKind,
4212    ) -> GitEntry {
4213        GitEntry {
4214            entry: Entry {
4215                id: NEW_ENTRY_ID,
4216                kind: new_entry_kind,
4217                path: parent_entry
4218                    .path
4219                    .join(RelPath::from_unix_str("\0").unwrap())
4220                    .into(),
4221                inode: 0,
4222                mtime: parent_entry.mtime,
4223                size: parent_entry.size,
4224                is_ignored: parent_entry.is_ignored,
4225                is_hidden: parent_entry.is_hidden,
4226                is_external: false,
4227                is_private: false,
4228                is_always_included: parent_entry.is_always_included,
4229                canonical_path: parent_entry.canonical_path.clone(),
4230                char_bag: parent_entry.char_bag,
4231                is_fifo: parent_entry.is_fifo,
4232            },
4233            git_summary,
4234        }
4235    }
4236
4237    fn update_visible_entries(
4238        &mut self,
4239        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
4240        focus_filename_editor: bool,
4241        autoscroll: bool,
4242        window: &mut Window,
4243        cx: &mut Context<Self>,
4244    ) {
4245        let now = Instant::now();
4246        let settings = ProjectPanelSettings::get_global(cx);
4247        let auto_collapse_dirs = settings.auto_fold_dirs;
4248        let hide_gitignore = settings.hide_gitignore;
4249        let sort_mode = settings.sort_mode;
4250        let sort_order = settings.sort_order;
4251        let project = self.project.read(cx);
4252        let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
4253
4254        let old_ancestors = self.state.ancestors.clone();
4255        let temporary_unfolded_pending_state = self.state.temporarily_unfolded_pending_state.take();
4256        let mut new_state = State::derive(&self.state);
4257        new_state.last_worktree_root_id = project
4258            .visible_worktrees(cx)
4259            .next_back()
4260            .and_then(|worktree| worktree.read(cx).root_entry())
4261            .map(|entry| entry.id);
4262        let mut max_width_item = None;
4263
4264        let visible_worktrees: Vec<_> = project
4265            .visible_worktrees(cx)
4266            .map(|worktree| worktree.read(cx).snapshot())
4267            .collect();
4268        let hide_root = settings.hide_root && visible_worktrees.len() == 1;
4269        let hide_hidden = settings.hide_hidden;
4270
4271        let visible_entries_task = cx.spawn_in(window, async move |this, cx| {
4272            let new_state = cx
4273                .background_spawn(async move {
4274                    for worktree_snapshot in visible_worktrees {
4275                        let worktree_id = worktree_snapshot.id();
4276
4277                        let mut new_entry_parent_id = None;
4278                        let mut new_entry_kind = EntryKind::Dir;
4279                        if let Some(edit_state) = &new_state.edit_state
4280                            && edit_state.worktree_id == worktree_id
4281                            && edit_state.is_new_entry()
4282                        {
4283                            new_entry_parent_id = Some(edit_state.entry_id);
4284                            new_entry_kind = if edit_state.is_dir {
4285                                EntryKind::Dir
4286                            } else {
4287                                EntryKind::File
4288                            };
4289                        }
4290
4291                        let mut visible_worktree_entries = Vec::new();
4292                        let mut entry_iter =
4293                            GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
4294                        let mut auto_folded_ancestors = vec![];
4295                        let worktree_abs_path = worktree_snapshot.abs_path();
4296                        while let Some(entry) = entry_iter.entry() {
4297                            if hide_root && Some(entry.entry) == worktree_snapshot.root_entry() {
4298                                if new_entry_parent_id == Some(entry.id) {
4299                                    visible_worktree_entries.push(Self::create_new_git_entry(
4300                                        entry.entry,
4301                                        entry.git_summary,
4302                                        new_entry_kind,
4303                                    ));
4304                                    new_entry_parent_id = None;
4305                                }
4306                                entry_iter.advance();
4307                                continue;
4308                            }
4309                            if auto_collapse_dirs && entry.kind.is_dir() {
4310                                auto_folded_ancestors.push(entry.id);
4311                                if !new_state.is_unfolded(&entry.id)
4312                                    && let Some(root_path) = worktree_snapshot.root_entry()
4313                                {
4314                                    let mut child_entries =
4315                                        worktree_snapshot.child_entries(&entry.path);
4316                                    if let Some(child) = child_entries.next()
4317                                        && entry.path != root_path.path
4318                                        && child_entries.next().is_none()
4319                                        && child.kind.is_dir()
4320                                    {
4321                                        entry_iter.advance();
4322
4323                                        continue;
4324                                    }
4325                                }
4326                                let depth = temporary_unfolded_pending_state
4327                                    .as_ref()
4328                                    .and_then(|state| {
4329                                        if state.previously_focused_leaf_entry.worktree_id
4330                                            == worktree_id
4331                                            && state.previously_focused_leaf_entry.entry_id
4332                                                == entry.id
4333                                        {
4334                                            auto_folded_ancestors.iter().rev().position(|id| {
4335                                                *id == state.temporarily_unfolded_active_entry_id
4336                                            })
4337                                        } else {
4338                                            None
4339                                        }
4340                                    })
4341                                    .unwrap_or_else(|| {
4342                                        old_ancestors
4343                                            .get(&entry.id)
4344                                            .map(|ancestor| ancestor.current_ancestor_depth)
4345                                            .unwrap_or_default()
4346                                    })
4347                                    .min(auto_folded_ancestors.len());
4348                                if let Some(edit_state) = &mut new_state.edit_state
4349                                    && edit_state.entry_id == entry.id
4350                                {
4351                                    edit_state.depth = depth;
4352                                }
4353                                let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
4354                                if ancestors.len() > 1 {
4355                                    ancestors.reverse();
4356                                    new_state.ancestors.insert(
4357                                        entry.id,
4358                                        FoldedAncestors {
4359                                            current_ancestor_depth: depth,
4360                                            ancestors,
4361                                        },
4362                                    );
4363                                }
4364                            }
4365                            auto_folded_ancestors.clear();
4366                            if (!hide_gitignore || !entry.is_ignored)
4367                                && (!hide_hidden || !entry.is_hidden)
4368                            {
4369                                visible_worktree_entries.push(entry.to_owned());
4370                            }
4371                            let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
4372                            {
4373                                entry.id == new_entry_id || {
4374                                    new_state.ancestors.get(&entry.id).is_some_and(|entries| {
4375                                        entries.ancestors.contains(&new_entry_id)
4376                                    })
4377                                }
4378                            } else {
4379                                false
4380                            };
4381                            if precedes_new_entry
4382                                && (!hide_gitignore || !entry.is_ignored)
4383                                && (!hide_hidden || !entry.is_hidden)
4384                            {
4385                                visible_worktree_entries.push(Self::create_new_git_entry(
4386                                    entry.entry,
4387                                    entry.git_summary,
4388                                    new_entry_kind,
4389                                ));
4390                            }
4391
4392                            let (depth, chars) = if Some(entry.entry)
4393                                == worktree_snapshot.root_entry()
4394                            {
4395                                let Some(path_name) = worktree_abs_path.file_name() else {
4396                                    entry_iter.advance();
4397                                    continue;
4398                                };
4399                                let depth = 0;
4400                                (depth, path_name.to_string_lossy().chars().count())
4401                            } else if entry.is_file() {
4402                                let Some(path_name) = entry
4403                                    .path
4404                                    .file_name()
4405                                    .with_context(|| {
4406                                        format!("Non-root entry has no file name: {entry:?}")
4407                                    })
4408                                    .log_err()
4409                                else {
4410                                    continue;
4411                                };
4412                                let depth = entry.path.ancestors().count() - 1;
4413                                (depth, path_name.chars().count())
4414                            } else {
4415                                let path = new_state
4416                                    .ancestors
4417                                    .get(&entry.id)
4418                                    .and_then(|ancestors| {
4419                                        let outermost_ancestor = ancestors.ancestors.last()?;
4420                                        let root_folded_entry = worktree_snapshot
4421                                            .entry_for_id(*outermost_ancestor)?
4422                                            .path
4423                                            .as_ref();
4424                                        entry.path.strip_prefix(root_folded_entry).ok().and_then(
4425                                            |suffix| {
4426                                                Some(
4427                                                    RelPath::from_unix_str(
4428                                                        root_folded_entry.file_name()?,
4429                                                    )
4430                                                    .unwrap()
4431                                                    .join(suffix),
4432                                                )
4433                                            },
4434                                        )
4435                                    })
4436                                    .or_else(|| {
4437                                        entry.path.file_name().map(|file_name| {
4438                                            RelPath::from_unix_str(file_name).unwrap().to_owned()
4439                                        })
4440                                    })
4441                                    .unwrap_or_else(|| entry.path.to_rel_path_buf());
4442                                let depth = path.components().count();
4443                                (depth, path.as_unix_str().chars().count())
4444                            };
4445                            let width_estimate =
4446                                item_width_estimate(depth, chars, entry.canonical_path.is_some());
4447
4448                            match max_width_item.as_mut() {
4449                                Some((id, worktree_id, width)) => {
4450                                    if *width < width_estimate {
4451                                        *id = entry.id;
4452                                        *worktree_id = worktree_snapshot.id();
4453                                        *width = width_estimate;
4454                                    }
4455                                }
4456                                None => {
4457                                    max_width_item =
4458                                        Some((entry.id, worktree_snapshot.id(), width_estimate))
4459                                }
4460                            }
4461
4462                            let expanded_dir_ids =
4463                                match new_state.expanded_dir_ids.entry(worktree_id) {
4464                                    hash_map::Entry::Occupied(e) => e.into_mut(),
4465                                    hash_map::Entry::Vacant(e) => {
4466                                        // The first time a worktree's root entry becomes available,
4467                                        // mark that root entry as expanded.
4468                                        if let Some(entry) = worktree_snapshot.root_entry() {
4469                                            e.insert(vec![entry.id]).as_slice()
4470                                        } else {
4471                                            &[]
4472                                        }
4473                                    }
4474                                };
4475
4476                            if expanded_dir_ids.binary_search(&entry.id).is_err()
4477                                && entry_iter.advance_to_sibling()
4478                            {
4479                                continue;
4480                            }
4481                            entry_iter.advance();
4482                        }
4483
4484                        par_sort_worktree_entries(
4485                            &mut visible_worktree_entries,
4486                            sort_mode,
4487                            sort_order,
4488                        );
4489                        new_state.visible_entries.push(VisibleEntriesForWorktree {
4490                            worktree_id,
4491                            entries: visible_worktree_entries,
4492                            index: OnceCell::new(),
4493                        })
4494                    }
4495                    if let Some((project_entry_id, worktree_id, _)) = max_width_item {
4496                        let mut visited_worktrees_length = 0;
4497                        let index = new_state
4498                            .visible_entries
4499                            .iter()
4500                            .find_map(|visible_entries| {
4501                                if worktree_id == visible_entries.worktree_id {
4502                                    visible_entries
4503                                        .entries
4504                                        .iter()
4505                                        .position(|entry| entry.id == project_entry_id)
4506                                } else {
4507                                    visited_worktrees_length += visible_entries.entries.len();
4508                                    None
4509                                }
4510                            });
4511                        if let Some(index) = index {
4512                            new_state.max_width_item_index = Some(visited_worktrees_length + index);
4513                        }
4514                    }
4515                    new_state
4516                })
4517                .await;
4518            this.update_in(cx, |this, window, cx| {
4519                this.state = new_state;
4520                if let Some((worktree_id, entry_id)) = new_selected_entry {
4521                    this.selection = Some(SelectedEntry {
4522                        worktree_id,
4523                        entry_id,
4524                    });
4525                }
4526                let elapsed = now.elapsed();
4527                if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
4528                    telemetry::event!(
4529                        "Project Panel Updated",
4530                        elapsed_ms = elapsed.as_millis() as u64,
4531                        worktree_entries = this
4532                            .state
4533                            .visible_entries
4534                            .iter()
4535                            .map(|worktree| worktree.entries.len())
4536                            .sum::<usize>(),
4537                    )
4538                }
4539                if this.update_visible_entries_task.focus_filename_editor {
4540                    this.update_visible_entries_task.focus_filename_editor = false;
4541                    this.filename_editor.update(cx, |editor, cx| {
4542                        window.focus(&editor.focus_handle(cx), cx);
4543                    });
4544                }
4545                if this.update_visible_entries_task.autoscroll {
4546                    this.update_visible_entries_task.autoscroll = false;
4547                    this.autoscroll(cx);
4548                }
4549                cx.notify();
4550            })
4551            .ok();
4552        });
4553
4554        self.update_visible_entries_task = UpdateVisibleEntriesTask {
4555            _visible_entries_task: visible_entries_task,
4556            focus_filename_editor: focus_filename_editor
4557                || self.update_visible_entries_task.focus_filename_editor,
4558            autoscroll: autoscroll || self.update_visible_entries_task.autoscroll,
4559        };
4560    }
4561
4562    fn expand_entry(
4563        &mut self,
4564        worktree_id: WorktreeId,
4565        entry_id: ProjectEntryId,
4566        cx: &mut Context<Self>,
4567    ) {
4568        self.project.update(cx, |project, cx| {
4569            if let Some((worktree, expanded_dir_ids)) = project
4570                .worktree_for_id(worktree_id, cx)
4571                .zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
4572            {
4573                project.expand_entry(worktree_id, entry_id, cx);
4574                let worktree = worktree.read(cx);
4575
4576                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
4577                    loop {
4578                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
4579                            expanded_dir_ids.insert(ix, entry.id);
4580                        }
4581
4582                        if let Some(parent_entry) =
4583                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
4584                        {
4585                            entry = parent_entry;
4586                        } else {
4587                            break;
4588                        }
4589                    }
4590                }
4591            }
4592        });
4593    }
4594
4595    fn drop_external_files(
4596        &mut self,
4597        paths: &[PathBuf],
4598        entry_id: ProjectEntryId,
4599        window: &mut Window,
4600        cx: &mut Context<Self>,
4601    ) {
4602        let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
4603
4604        let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
4605
4606        let Some((target_directory, worktree, fs)) = maybe!({
4607            let project = self.project.read(cx);
4608            let fs = project.fs().clone();
4609            let worktree = project.worktree_for_entry(entry_id, cx)?;
4610            let entry = worktree.read(cx).entry_for_id(entry_id)?;
4611            let path = entry.path.clone();
4612            let target_directory = if entry.is_dir() {
4613                path
4614            } else {
4615                path.parent()?.into()
4616            };
4617            Some((target_directory, worktree, fs))
4618        }) else {
4619            return;
4620        };
4621
4622        let mut paths_to_replace = Vec::new();
4623        for path in &paths {
4624            if let Some(name) = path.file_name()
4625                && let Some(name) = name.to_str()
4626            {
4627                let target_path = target_directory.join(RelPath::from_unix_str(name).unwrap());
4628                if worktree.read(cx).entry_for_path(&target_path).is_some() {
4629                    paths_to_replace.push((name.to_string(), path.clone()));
4630                }
4631            }
4632        }
4633
4634        cx.spawn_in(window, async move |this, cx| {
4635            async move {
4636                for (filename, original_path) in &paths_to_replace {
4637                    let prompt_message = format!(
4638                        concat!(
4639                            "A file or folder with name {} ",
4640                            "already exists in the destination folder. ",
4641                            "Do you want to replace it?"
4642                        ),
4643                        MarkdownInlineCode(filename)
4644                    );
4645                    let answer = cx
4646                        .update(|window, cx| {
4647                            window.prompt(
4648                                PromptLevel::Info,
4649                                &prompt_message,
4650                                None,
4651                                &["Replace", "Cancel"],
4652                                cx,
4653                            )
4654                        })?
4655                        .await?;
4656
4657                    if answer == 1
4658                        && let Some(item_idx) = paths.iter().position(|p| p == original_path)
4659                    {
4660                        paths.remove(item_idx);
4661                    }
4662                }
4663
4664                if paths.is_empty() {
4665                    return Ok(());
4666                }
4667
4668                let (worktree_id, task) = worktree.update(cx, |worktree, cx| {
4669                    (
4670                        worktree.id(),
4671                        worktree.copy_external_entries(target_directory, paths, fs, cx),
4672                    )
4673                });
4674
4675                let opened_entries: Vec<_> = task
4676                    .await
4677                    .with_context(|| "failed to copy external paths")?;
4678                this.update_in(cx, |this, window, cx| {
4679                    let mut did_open = false;
4680                    if open_file_after_drop && !opened_entries.is_empty() {
4681                        let settings = ProjectPanelSettings::get_global(cx);
4682                        if settings.auto_open.should_open_on_drop() {
4683                            this.open_entry(opened_entries[0], true, false, cx);
4684                            did_open = true;
4685                        }
4686                    }
4687
4688                    if !did_open {
4689                        let new_selection = opened_entries
4690                            .last()
4691                            .map(|&entry_id| (worktree_id, entry_id));
4692                        for &entry_id in &opened_entries {
4693                            this.expand_entry(worktree_id, entry_id, cx);
4694                        }
4695                        this.marked_entries.clear();
4696                        this.update_visible_entries(new_selection, false, false, window, cx);
4697                    }
4698
4699                    let changes: Vec<Change> = opened_entries
4700                        .iter()
4701                        .filter_map(|entry_id| {
4702                            worktree.read(cx).entry_for_id(*entry_id).map(|entry| {
4703                                Change::Created(ProjectPath {
4704                                    worktree_id,
4705                                    path: entry.path.clone(),
4706                                })
4707                            })
4708                        })
4709                        .collect();
4710
4711                    this.undo_manager.record(changes).log_err();
4712                })
4713            }
4714            .log_err()
4715            .await
4716        })
4717        .detach();
4718    }
4719
4720    fn refresh_drag_cursor_style(
4721        &self,
4722        modifiers: &Modifiers,
4723        window: &mut Window,
4724        cx: &mut Context<Self>,
4725    ) {
4726        if let Some(existing_cursor) = cx.active_drag_cursor_style() {
4727            let new_cursor = if Self::is_copy_modifier_set(modifiers) {
4728                CursorStyle::DragCopy
4729            } else {
4730                CursorStyle::PointingHand
4731            };
4732            if existing_cursor != new_cursor {
4733                cx.set_active_drag_cursor_style(new_cursor, window);
4734            }
4735        }
4736    }
4737
4738    fn clear_drag_state(&mut self, cx: &mut Context<Self>) {
4739        let had_drag_state = self.drag_target_entry.take().is_some()
4740            | self.folded_directory_drag_target.take().is_some()
4741            | self.hover_scroll_task.take().is_some()
4742            | self.hover_expand_task.take().is_some()
4743            | self.previous_drag_position.take().is_some();
4744        if had_drag_state {
4745            cx.notify();
4746        }
4747    }
4748
4749    fn is_copy_modifier_set(modifiers: &Modifiers) -> bool {
4750        cfg!(target_os = "macos") && modifiers.alt
4751            || cfg!(not(target_os = "macos")) && modifiers.control
4752    }
4753
4754    fn drag_onto(
4755        &mut self,
4756        selections: &DraggedSelection,
4757        target_entry_id: ProjectEntryId,
4758        is_file: bool,
4759        window: &mut Window,
4760        cx: &mut Context<Self>,
4761    ) {
4762        let resolved_selections = selections
4763            .items()
4764            .map(|entry| SelectedEntry {
4765                entry_id: self.resolve_entry(entry.entry_id),
4766                worktree_id: entry.worktree_id,
4767            })
4768            .collect::<BTreeSet<SelectedEntry>>();
4769        let entries = self.disjoint_entries(resolved_selections, cx);
4770
4771        let root_entries: Vec<ProjectEntryId> = {
4772            let project = self.project.read(cx);
4773            entries
4774                .iter()
4775                .filter(|entry| project.entry_is_worktree_root(entry.entry_id, cx))
4776                .map(|entry| entry.entry_id)
4777                .collect()
4778        };
4779        if !root_entries.is_empty() {
4780            for entry_id in root_entries {
4781                self.move_worktree_root(entry_id, target_entry_id, cx);
4782            }
4783            return;
4784        }
4785
4786        if Self::is_copy_modifier_set(&window.modifiers()) {
4787            let _ = maybe!({
4788                let project = self.project.read(cx);
4789                let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
4790                let worktree_id = target_worktree.read(cx).id();
4791                let target_entry = target_worktree
4792                    .read(cx)
4793                    .entry_for_id(target_entry_id)?
4794                    .clone();
4795
4796                let mut copy_tasks = Vec::new();
4797                let mut disambiguation_range = None;
4798                for selection in &entries {
4799                    let (new_path, new_disambiguation_range) = self.create_paste_path(
4800                        selection,
4801                        (target_worktree.clone(), &target_entry),
4802                        cx,
4803                    )?;
4804
4805                    let task = self.project.update(cx, |project, cx| {
4806                        project.copy_entry(selection.entry_id, (worktree_id, new_path).into(), cx)
4807                    });
4808                    copy_tasks.push(task);
4809                    disambiguation_range = new_disambiguation_range.or(disambiguation_range);
4810                }
4811
4812                let item_count = copy_tasks.len();
4813
4814                cx.spawn_in(window, async move |project_panel, cx| {
4815                    let mut last_succeed = None;
4816                    let mut changes = Vec::new();
4817                    for task in copy_tasks.into_iter() {
4818                        if let Some(Some(entry)) = task.await.log_err() {
4819                            last_succeed = Some(entry.id);
4820                            changes.push(Change::Created((worktree_id, entry.path).into()));
4821                        }
4822                    }
4823                    // update selection
4824                    if let Some(entry_id) = last_succeed {
4825                        project_panel.update_in(cx, |project_panel, window, cx| {
4826                            project_panel.selection = Some(SelectedEntry {
4827                                worktree_id,
4828                                entry_id,
4829                            });
4830                            // if only one entry was dragged and it was disambiguated, open the rename editor
4831                            if item_count == 1 && disambiguation_range.is_some() {
4832                                project_panel.rename_impl(disambiguation_range, window, cx);
4833                            }
4834
4835                            project_panel.undo_manager.record(changes)
4836                        })??;
4837                    }
4838
4839                    std::result::Result::Ok::<(), anyhow::Error>(())
4840                })
4841                .detach();
4842                Some(())
4843            });
4844        } else {
4845            let update_marks = !self.marked_entries.is_empty();
4846            let active_selection = selections.active_selection;
4847
4848            // For folded selections, track the leaf suffix relative to the resolved
4849            // entry so we can refresh it after the move completes.
4850            let (folded_selection_info, folded_selection_entries): (
4851                Vec<(ProjectEntryId, RelPathBuf)>,
4852                HashSet<SelectedEntry>,
4853            ) = {
4854                let project = self.project.read(cx);
4855                let mut info = Vec::new();
4856                let mut folded_entries = HashSet::default();
4857
4858                for selection in selections.items() {
4859                    let resolved_id = self.resolve_entry(selection.entry_id);
4860                    if resolved_id == selection.entry_id {
4861                        continue;
4862                    }
4863                    folded_entries.insert(*selection);
4864                    let Some(source_path) = project.path_for_entry(resolved_id, cx) else {
4865                        continue;
4866                    };
4867                    let Some(leaf_path) = project.path_for_entry(selection.entry_id, cx) else {
4868                        continue;
4869                    };
4870                    let Ok(suffix) = leaf_path.path.strip_prefix(source_path.path.as_ref()) else {
4871                        continue;
4872                    };
4873                    if suffix.as_unix_str().is_empty() {
4874                        continue;
4875                    }
4876
4877                    info.push((resolved_id, suffix.to_rel_path_buf()));
4878                }
4879                (info, folded_entries)
4880            };
4881
4882            // Capture old paths before moving so we can record undo operations.
4883            let old_paths: HashMap<ProjectEntryId, ProjectPath> = {
4884                let project = self.project.read(cx);
4885                entries
4886                    .iter()
4887                    .filter_map(|entry| {
4888                        let path = project.path_for_entry(entry.entry_id, cx)?;
4889                        Some((entry.entry_id, path))
4890                    })
4891                    .collect()
4892            };
4893            let destination_worktree_id = self
4894                .project
4895                .read(cx)
4896                .worktree_for_entry(target_entry_id, cx)
4897                .map(|wt| wt.read(cx).id());
4898
4899            // Collect move tasks paired with their source entry ID so we can correlate
4900            // results with folded selections that need refreshing.
4901            let mut move_tasks: Vec<(ProjectEntryId, Task<Result<CreatedEntry>>)> = Vec::new();
4902            for entry in entries {
4903                if let Some(task) =
4904                    self.move_worktree_entry(entry.entry_id, target_entry_id, is_file, cx)
4905                {
4906                    move_tasks.push((entry.entry_id, task));
4907                }
4908            }
4909
4910            if move_tasks.is_empty() {
4911                return;
4912            }
4913
4914            let workspace = self.workspace.clone();
4915            if folded_selection_info.is_empty() {
4916                cx.spawn_in(window, async move |project_panel, mut cx| {
4917                    let mut changes = Vec::new();
4918                    for (entry_id, task) in move_tasks {
4919                        if let Some(CreatedEntry::Included(new_entry)) = task
4920                            .await
4921                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4922                        {
4923                            if let (Some(old_path), Some(worktree_id)) =
4924                                (old_paths.get(&entry_id), destination_worktree_id)
4925                            {
4926                                changes.push(Change::Renamed(
4927                                    old_path.clone(),
4928                                    (worktree_id, new_entry.path).into(),
4929                                ));
4930                            }
4931                        }
4932                    }
4933                    project_panel
4934                        .update(cx, |this, _| {
4935                            this.undo_manager.record(changes).log_err();
4936                        })
4937                        .ok();
4938                })
4939                .detach();
4940            } else {
4941                cx.spawn_in(window, async move |project_panel, mut cx| {
4942                    // Await all move tasks and collect successful results
4943                    let mut move_results: Vec<(ProjectEntryId, Entry)> = Vec::new();
4944                    let mut operations = Vec::new();
4945                    for (entry_id, task) in move_tasks {
4946                        if let Some(CreatedEntry::Included(new_entry)) = task
4947                            .await
4948                            .notify_workspace_async_err(workspace.clone(), &mut cx)
4949                        {
4950                            if let (Some(old_path), Some(worktree_id)) =
4951                                (old_paths.get(&entry_id), destination_worktree_id)
4952                            {
4953                                operations.push(Change::Renamed(
4954                                    old_path.clone(),
4955                                    (worktree_id, new_entry.path.clone()).into(),
4956                                ));
4957                            }
4958                            move_results.push((entry_id, new_entry));
4959                        }
4960                    }
4961
4962                    if move_results.is_empty() {
4963                        return;
4964                    }
4965
4966                    project_panel
4967                        .update(cx, |this, _| {
4968                            this.undo_manager.record(operations).log_err();
4969                        })
4970                        .ok();
4971
4972                    // For folded selections, we need to refresh the leaf paths (with suffixes)
4973                    // because they may not be indexed yet after the parent directory was moved.
4974                    // First collect the paths to refresh, then refresh them.
4975                    let paths_to_refresh: Vec<(Entity<Worktree>, Arc<RelPath>)> = project_panel
4976                        .update(cx, |project_panel, cx| {
4977                            let project = project_panel.project.read(cx);
4978                            folded_selection_info
4979                                .iter()
4980                                .filter_map(|(resolved_id, suffix)| {
4981                                    let (_, new_entry) =
4982                                        move_results.iter().find(|(id, _)| id == resolved_id)?;
4983                                    let worktree = project.worktree_for_entry(new_entry.id, cx)?;
4984                                    let leaf_path = new_entry.path.join(suffix);
4985                                    Some((worktree, leaf_path.into()))
4986                                })
4987                                .collect()
4988                        })
4989                        .ok()
4990                        .unwrap_or_default();
4991
4992                    let refresh_tasks: Vec<_> = paths_to_refresh
4993                        .into_iter()
4994                        .filter_map(|(worktree, leaf_path)| {
4995                            worktree.update(cx, |worktree, cx| {
4996                                worktree
4997                                    .as_local_mut()
4998                                    .map(|local| local.refresh_entry(leaf_path, None, cx))
4999                            })
5000                        })
5001                        .collect();
5002
5003                    for task in refresh_tasks {
5004                        task.await.log_err();
5005                    }
5006
5007                    if update_marks && !folded_selection_entries.is_empty() {
5008                        project_panel
5009                            .update(cx, |project_panel, cx| {
5010                                project_panel.marked_entries.retain(|entry| {
5011                                    !folded_selection_entries.contains(entry)
5012                                        || *entry == active_selection
5013                                });
5014                                cx.notify();
5015                            })
5016                            .ok();
5017                    }
5018                })
5019                .detach();
5020            }
5021        }
5022    }
5023
5024    fn index_for_entry(
5025        &self,
5026        entry_id: ProjectEntryId,
5027        worktree_id: WorktreeId,
5028    ) -> Option<(usize, usize, usize)> {
5029        let mut total_ix = 0;
5030        for (worktree_ix, visible) in self.state.visible_entries.iter().enumerate() {
5031            if worktree_id != visible.worktree_id {
5032                total_ix += visible.entries.len();
5033                continue;
5034            }
5035
5036            return visible
5037                .entries
5038                .iter()
5039                .enumerate()
5040                .find(|(_, entry)| entry.id == entry_id)
5041                .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
5042        }
5043        None
5044    }
5045
5046    fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
5047        let mut offset = 0;
5048        for worktree in &self.state.visible_entries {
5049            let current_len = worktree.entries.len();
5050            if index < offset + current_len {
5051                return worktree
5052                    .entries
5053                    .get(index - offset)
5054                    .map(|entry| (worktree.worktree_id, entry.to_ref()));
5055            }
5056            offset += current_len;
5057        }
5058        None
5059    }
5060
5061    fn iter_visible_entries(
5062        &self,
5063        range: Range<usize>,
5064        window: &mut Window,
5065        cx: &mut Context<ProjectPanel>,
5066        callback: &mut dyn FnMut(
5067            &Entry,
5068            usize,
5069            &HashSet<Arc<RelPath>>,
5070            &mut Window,
5071            &mut Context<ProjectPanel>,
5072        ),
5073    ) {
5074        let mut ix = 0;
5075        for visible in &self.state.visible_entries {
5076            if ix >= range.end {
5077                return;
5078            }
5079
5080            if ix + visible.entries.len() <= range.start {
5081                ix += visible.entries.len();
5082                continue;
5083            }
5084
5085            let end_ix = range.end.min(ix + visible.entries.len());
5086            let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
5087            let entries = visible
5088                .index
5089                .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
5090            let base_index = ix + entry_range.start;
5091            for (i, entry) in visible.entries[entry_range].iter().enumerate() {
5092                let global_index = base_index + i;
5093                callback(entry, global_index, entries, window, cx);
5094            }
5095            ix = end_ix;
5096        }
5097    }
5098
5099    fn for_each_visible_entry(
5100        &self,
5101        range: Range<usize>,
5102        window: &mut Window,
5103        cx: &mut Context<ProjectPanel>,
5104        callback: &mut dyn FnMut(
5105            ProjectEntryId,
5106            EntryDetails,
5107            &mut Window,
5108            &mut Context<ProjectPanel>,
5109        ),
5110    ) {
5111        let mut ix = 0;
5112        for visible in &self.state.visible_entries {
5113            if ix >= range.end {
5114                return;
5115            }
5116
5117            if ix + visible.entries.len() <= range.start {
5118                ix += visible.entries.len();
5119                continue;
5120            }
5121
5122            let end_ix = range.end.min(ix + visible.entries.len());
5123            let git_status_setting = {
5124                let settings = ProjectPanelSettings::get_global(cx);
5125                settings.git_status
5126            };
5127            if let Some(worktree) = self
5128                .project
5129                .read(cx)
5130                .worktree_for_id(visible.worktree_id, cx)
5131            {
5132                let snapshot = worktree.read(cx).snapshot();
5133                let root_name = snapshot.root_name();
5134
5135                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
5136                let entries = visible
5137                    .index
5138                    .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
5139                for entry in visible.entries[entry_range].iter() {
5140                    let status = git_status_setting
5141                        .then_some(entry.git_summary)
5142                        .unwrap_or_default();
5143
5144                    let mut details = self.details_for_entry(
5145                        entry,
5146                        visible.worktree_id,
5147                        root_name,
5148                        entries,
5149                        status,
5150                        None,
5151                        window,
5152                        cx,
5153                    );
5154
5155                    if let Some(edit_state) = &self.state.edit_state {
5156                        let is_edited_entry = if edit_state.is_new_entry() {
5157                            entry.id == NEW_ENTRY_ID
5158                        } else {
5159                            entry.id == edit_state.entry_id
5160                                || self.state.ancestors.get(&entry.id).is_some_and(
5161                                    |auto_folded_dirs| {
5162                                        auto_folded_dirs.ancestors.contains(&edit_state.entry_id)
5163                                    },
5164                                )
5165                        };
5166
5167                        if is_edited_entry {
5168                            if let Some(processing_filename) = &edit_state.processing_filename {
5169                                details.is_processing = true;
5170                                if let Some(ancestors) = edit_state
5171                                    .leaf_entry_id
5172                                    .and_then(|entry| self.state.ancestors.get(&entry))
5173                                {
5174                                    let position = ancestors.ancestors.iter().position(|entry_id| *entry_id == edit_state.entry_id).expect("Edited sub-entry should be an ancestor of selected leaf entry") + 1;
5175                                    let all_components = ancestors.ancestors.len();
5176
5177                                    let prefix_components = all_components - position;
5178                                    let suffix_components = position.checked_sub(1);
5179                                    let mut previous_components =
5180                                        Path::new(&details.filename).components();
5181                                    let mut new_path = previous_components
5182                                        .by_ref()
5183                                        .take(prefix_components)
5184                                        .collect::<PathBuf>();
5185                                    if let Some(last_component) =
5186                                        processing_filename.components().next_back()
5187                                    {
5188                                        new_path.push(last_component);
5189                                        previous_components.next();
5190                                    }
5191
5192                                    if suffix_components.is_some() {
5193                                        new_path.push(previous_components);
5194                                    }
5195                                    if let Some(str) = new_path.to_str() {
5196                                        details.filename.clear();
5197                                        details.filename.push_str(str);
5198                                    }
5199                                } else {
5200                                    details.filename.clear();
5201                                    details.filename.push_str(processing_filename.as_unix_str());
5202                                }
5203                            } else {
5204                                if edit_state.is_new_entry() {
5205                                    details.filename.clear();
5206                                }
5207                                details.is_editing = true;
5208                            }
5209                        }
5210                    }
5211
5212                    callback(entry.id, details, window, cx);
5213                }
5214            }
5215            ix = end_ix;
5216        }
5217    }
5218
5219    fn find_entry_in_worktree(
5220        &self,
5221        worktree_id: WorktreeId,
5222        reverse_search: bool,
5223        only_visible_entries: bool,
5224        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
5225        cx: &mut Context<Self>,
5226    ) -> Option<GitEntry> {
5227        if only_visible_entries {
5228            let entries = self
5229                .state
5230                .visible_entries
5231                .iter()
5232                .find_map(|visible| {
5233                    if worktree_id == visible.worktree_id {
5234                        Some(&visible.entries)
5235                    } else {
5236                        None
5237                    }
5238                })?
5239                .clone();
5240
5241            return utils::ReversibleIterable::new(entries.iter(), reverse_search)
5242                .find(|ele| predicate(ele.to_ref(), worktree_id))
5243                .cloned();
5244        }
5245
5246        let repo_snapshots = self
5247            .project
5248            .read(cx)
5249            .git_store()
5250            .read(cx)
5251            .repo_snapshots(cx);
5252        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
5253        worktree.read_with(cx, |tree, _| {
5254            utils::ReversibleIterable::new(
5255                GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
5256                reverse_search,
5257            )
5258            .find_single_ended(|ele| predicate(*ele, worktree_id))
5259            .map(|ele| ele.to_owned())
5260        })
5261    }
5262
5263    fn find_entry(
5264        &self,
5265        start: Option<&SelectedEntry>,
5266        reverse_search: bool,
5267        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
5268        cx: &mut Context<Self>,
5269    ) -> Option<SelectedEntry> {
5270        let mut worktree_ids: Vec<_> = self
5271            .state
5272            .visible_entries
5273            .iter()
5274            .map(|worktree| worktree.worktree_id)
5275            .collect();
5276        let repo_snapshots = self
5277            .project
5278            .read(cx)
5279            .git_store()
5280            .read(cx)
5281            .repo_snapshots(cx);
5282
5283        let mut last_found: Option<SelectedEntry> = None;
5284
5285        if let Some(start) = start {
5286            let worktree = self
5287                .project
5288                .read(cx)
5289                .worktree_for_id(start.worktree_id, cx)?
5290                .read(cx);
5291
5292            let search = {
5293                let entry = worktree.entry_for_id(start.entry_id)?;
5294                let root_entry = worktree.root_entry()?;
5295                let tree_id = worktree.id();
5296
5297                let mut first_iter = GitTraversal::new(
5298                    &repo_snapshots,
5299                    worktree.traverse_from_path(true, true, true, entry.path.as_ref()),
5300                );
5301
5302                if reverse_search {
5303                    first_iter.next();
5304                }
5305
5306                let first = first_iter
5307                    .enumerate()
5308                    .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
5309                    .map(|(_, entry)| entry)
5310                    .find(|ele| predicate(*ele, tree_id))
5311                    .map(|ele| ele.to_owned());
5312
5313                let second_iter =
5314                    GitTraversal::new(&repo_snapshots, worktree.entries(true, 0usize));
5315
5316                let second = if reverse_search {
5317                    second_iter
5318                        .take_until(|ele| ele.id == start.entry_id)
5319                        .filter(|ele| predicate(*ele, tree_id))
5320                        .last()
5321                        .map(|ele| ele.to_owned())
5322                } else {
5323                    second_iter
5324                        .take_while(|ele| ele.id != start.entry_id)
5325                        .filter(|ele| predicate(*ele, tree_id))
5326                        .last()
5327                        .map(|ele| ele.to_owned())
5328                };
5329
5330                if reverse_search {
5331                    Some((second, first))
5332                } else {
5333                    Some((first, second))
5334                }
5335            };
5336
5337            if let Some((first, second)) = search {
5338                let first = first.map(|entry| SelectedEntry {
5339                    worktree_id: start.worktree_id,
5340                    entry_id: entry.id,
5341                });
5342
5343                let second = second.map(|entry| SelectedEntry {
5344                    worktree_id: start.worktree_id,
5345                    entry_id: entry.id,
5346                });
5347
5348                if first.is_some() {
5349                    return first;
5350                }
5351                last_found = second;
5352
5353                let idx = worktree_ids
5354                    .iter()
5355                    .enumerate()
5356                    .find(|(_, ele)| **ele == start.worktree_id)
5357                    .map(|(idx, _)| idx);
5358
5359                if let Some(idx) = idx {
5360                    worktree_ids.rotate_left(idx + 1usize);
5361                    worktree_ids.pop();
5362                }
5363            }
5364        }
5365
5366        for tree_id in worktree_ids.into_iter() {
5367            if let Some(found) =
5368                self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
5369            {
5370                return Some(SelectedEntry {
5371                    worktree_id: tree_id,
5372                    entry_id: found.id,
5373                });
5374            }
5375        }
5376
5377        last_found
5378    }
5379
5380    fn find_visible_entry(
5381        &self,
5382        start: Option<&SelectedEntry>,
5383        reverse_search: bool,
5384        predicate: &dyn Fn(GitEntryRef, WorktreeId) -> bool,
5385        cx: &mut Context<Self>,
5386    ) -> Option<SelectedEntry> {
5387        let mut worktree_ids: Vec<_> = self
5388            .state
5389            .visible_entries
5390            .iter()
5391            .map(|worktree| worktree.worktree_id)
5392            .collect();
5393
5394        let mut last_found: Option<SelectedEntry> = None;
5395
5396        if let Some(start) = start {
5397            let entries = self
5398                .state
5399                .visible_entries
5400                .iter()
5401                .find(|worktree| worktree.worktree_id == start.worktree_id)
5402                .map(|worktree| &worktree.entries)?;
5403
5404            let mut start_idx = entries
5405                .iter()
5406                .enumerate()
5407                .find(|(_, ele)| ele.id == start.entry_id)
5408                .map(|(idx, _)| idx)?;
5409
5410            if reverse_search {
5411                start_idx = start_idx.saturating_add(1usize);
5412            }
5413
5414            let (left, right) = entries.split_at_checked(start_idx)?;
5415
5416            let (first_iter, second_iter) = if reverse_search {
5417                (
5418                    utils::ReversibleIterable::new(left.iter(), reverse_search),
5419                    utils::ReversibleIterable::new(right.iter(), reverse_search),
5420                )
5421            } else {
5422                (
5423                    utils::ReversibleIterable::new(right.iter(), reverse_search),
5424                    utils::ReversibleIterable::new(left.iter(), reverse_search),
5425                )
5426            };
5427
5428            let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5429            let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
5430
5431            if first_search.is_some() {
5432                return first_search.map(|entry| SelectedEntry {
5433                    worktree_id: start.worktree_id,
5434                    entry_id: entry.id,
5435                });
5436            }
5437
5438            last_found = second_search.map(|entry| SelectedEntry {
5439                worktree_id: start.worktree_id,
5440                entry_id: entry.id,
5441            });
5442
5443            let idx = worktree_ids
5444                .iter()
5445                .enumerate()
5446                .find(|(_, ele)| **ele == start.worktree_id)
5447                .map(|(idx, _)| idx);
5448
5449            if let Some(idx) = idx {
5450                worktree_ids.rotate_left(idx + 1usize);
5451                worktree_ids.pop();
5452            }
5453        }
5454
5455        for tree_id in worktree_ids.into_iter() {
5456            if let Some(found) =
5457                self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
5458            {
5459                return Some(SelectedEntry {
5460                    worktree_id: tree_id,
5461                    entry_id: found.id,
5462                });
5463            }
5464        }
5465
5466        last_found
5467    }
5468
5469    fn calculate_depth_and_difference(
5470        entry: &Entry,
5471        visible_worktree_entries: &HashSet<Arc<RelPath>>,
5472    ) -> (usize, usize) {
5473        let (depth, difference) = entry
5474            .path
5475            .ancestors()
5476            .skip(1) // Skip the entry itself
5477            .find_map(|ancestor| {
5478                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
5479                    let entry_path_components_count = entry.path.components().count();
5480                    let parent_path_components_count = parent_entry.components().count();
5481                    let difference = entry_path_components_count - parent_path_components_count;
5482                    let depth = parent_entry
5483                        .ancestors()
5484                        .skip(1)
5485                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
5486                        .count();
5487                    Some((depth + 1, difference))
5488                } else {
5489                    None
5490                }
5491            })
5492            .unwrap_or_else(|| (0, entry.path.components().count()));
5493
5494        (depth, difference)
5495    }
5496
5497    fn highlight_entry_for_external_drag(
5498        &self,
5499        target_entry: &Entry,
5500        target_worktree: &Worktree,
5501    ) -> Option<ProjectEntryId> {
5502        // Always highlight directory or parent directory if it's file
5503        if target_entry.is_dir() {
5504            Some(target_entry.id)
5505        } else {
5506            target_entry
5507                .path
5508                .parent()
5509                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5510                .map(|parent_entry| parent_entry.id)
5511        }
5512    }
5513
5514    fn highlight_entry_for_selection_drag(
5515        &self,
5516        target_entry: &Entry,
5517        target_worktree: &Worktree,
5518        drag_state: &DraggedSelection,
5519        cx: &Context<Self>,
5520    ) -> Option<ProjectEntryId> {
5521        let target_parent_path = target_entry.path.parent();
5522
5523        // In case of single item drag, we do not highlight existing
5524        // directory which item belongs too
5525        if drag_state.items().count() == 1
5526            && drag_state.active_selection.worktree_id == target_worktree.id()
5527        {
5528            let active_entry_path = self
5529                .project
5530                .read(cx)
5531                .path_for_entry(drag_state.active_selection.entry_id, cx)?;
5532
5533            if let Some(active_parent_path) = active_entry_path.path.parent() {
5534                // Do not highlight active entry parent
5535                if active_parent_path == target_entry.path.as_ref() {
5536                    return None;
5537                }
5538
5539                // Do not highlight active entry sibling files
5540                if Some(active_parent_path) == target_parent_path && target_entry.is_file() {
5541                    return None;
5542                }
5543            }
5544        }
5545
5546        // Always highlight directory or parent directory if it's file
5547        if target_entry.is_dir() {
5548            Some(target_entry.id)
5549        } else {
5550            target_parent_path
5551                .and_then(|parent_path| target_worktree.entry_for_path(parent_path))
5552                .map(|parent_entry| parent_entry.id)
5553        }
5554    }
5555
5556    fn should_highlight_background_for_selection_drag(
5557        &self,
5558        drag_state: &DraggedSelection,
5559        last_root_id: ProjectEntryId,
5560        cx: &App,
5561    ) -> bool {
5562        // Always highlight for multiple entries
5563        if drag_state.items().count() > 1 {
5564            return true;
5565        }
5566
5567        // Since root will always have empty relative path
5568        if let Some(entry_path) = self
5569            .project
5570            .read(cx)
5571            .path_for_entry(drag_state.active_selection.entry_id, cx)
5572        {
5573            if let Some(parent_path) = entry_path.path.parent() {
5574                if !parent_path.is_empty() {
5575                    return true;
5576                }
5577            }
5578        }
5579
5580        // If parent is empty, check if different worktree
5581        if let Some(last_root_worktree_id) = self
5582            .project
5583            .read(cx)
5584            .worktree_id_for_entry(last_root_id, cx)
5585        {
5586            if drag_state.active_selection.worktree_id != last_root_worktree_id {
5587                return true;
5588            }
5589        }
5590
5591        false
5592    }
5593
5594    fn render_entry(
5595        &self,
5596        entry_id: ProjectEntryId,
5597        details: EntryDetails,
5598        marked_selections: Arc<[SelectedEntry]>,
5599        window: &mut Window,
5600        cx: &mut Context<Self>,
5601    ) -> Stateful<Div> {
5602        const GROUP_NAME: &str = "project_entry";
5603
5604        let kind = details.kind;
5605        let is_sticky = details.sticky.is_some();
5606        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
5607        let settings = ProjectPanelSettings::get_global(cx);
5608        let show_editor = details.is_editing && !details.is_processing;
5609
5610        let selection = SelectedEntry {
5611            worktree_id: details.worktree_id,
5612            entry_id,
5613        };
5614
5615        let is_marked = self.marked_entries.contains(&selection);
5616        let is_active = self
5617            .selection
5618            .is_some_and(|selection| selection.entry_id == entry_id);
5619
5620        let file_name = details.filename.clone();
5621
5622        let mut icon = details.icon.clone();
5623        if settings.file_icons && show_editor && details.kind.is_file() {
5624            let filename = self.filename_editor.read(cx).text(cx);
5625            if filename.len() > 2 {
5626                icon = FileIcons::get_icon(Path::new(&filename), cx);
5627            }
5628        }
5629
5630        let filename_text_color = details.filename_text_color;
5631        let diagnostic_severity = details.diagnostic_severity;
5632        let diagnostic_count = details.diagnostic_count;
5633        let item_colors = get_item_color(is_sticky, cx);
5634
5635        let canonical_path = details.canonical_path.clone();
5636        let path_style = self.project.read(cx).path_style(cx);
5637        let path = details.path.clone();
5638
5639        let depth = details.depth;
5640        let worktree_id = details.worktree_id;
5641
5642        let bg_color = if is_marked {
5643            item_colors.marked
5644        } else {
5645            item_colors.default
5646        };
5647
5648        let bg_hover_color = if is_marked {
5649            item_colors.marked
5650        } else {
5651            item_colors.hover
5652        };
5653
5654        let validation_color_and_message = if show_editor {
5655            match self
5656                .state
5657                .edit_state
5658                .as_ref()
5659                .map_or(ValidationState::None, |e| e.validation_state.clone())
5660            {
5661                ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)),
5662                ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)),
5663                ValidationState::None => None,
5664            }
5665        } else {
5666            None
5667        };
5668
5669        let border_color =
5670            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5671                match validation_color_and_message {
5672                    Some((color, _)) => color,
5673                    None => item_colors.focused,
5674                }
5675            } else {
5676                bg_color
5677            };
5678
5679        let border_hover_color =
5680            if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
5681                match validation_color_and_message {
5682                    Some((color, _)) => color,
5683                    None => item_colors.focused,
5684                }
5685            } else {
5686                bg_hover_color
5687            };
5688
5689        let folded_directory_drag_target = self.folded_directory_drag_target;
5690        let is_highlighted = {
5691            if let Some(highlight_entry_id) =
5692                self.drag_target_entry
5693                    .as_ref()
5694                    .and_then(|drag_target| match drag_target {
5695                        DragTarget::Entry {
5696                            highlight_entry_id, ..
5697                        } => Some(*highlight_entry_id),
5698                        DragTarget::Background => self.state.last_worktree_root_id,
5699                    })
5700            {
5701                // Highlight if same entry or it's children
5702                if entry_id == highlight_entry_id {
5703                    true
5704                } else {
5705                    maybe!({
5706                        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
5707                        let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?;
5708                        Some(path.starts_with(&highlight_entry.path))
5709                    })
5710                    .unwrap_or(false)
5711                }
5712            } else {
5713                false
5714            }
5715        };
5716        let git_indicator = settings
5717            .git_status_indicator
5718            .then(|| git_status_indicator(details.git_status))
5719            .flatten();
5720
5721        let id: ElementId = if is_sticky {
5722            SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
5723        } else {
5724            (entry_id.to_proto() as usize).into()
5725        };
5726
5727        div()
5728            .id(id.clone())
5729            .relative()
5730            .group(GROUP_NAME)
5731            .cursor_pointer()
5732            .rounded_none()
5733            .bg(bg_color)
5734            .border_1()
5735            .border_r_2()
5736            .border_color(border_color)
5737            .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
5738            .when(is_sticky, |this| this.block_mouse_except_scroll())
5739            .when(!is_sticky, |this| {
5740                this.when(
5741                    is_highlighted && folded_directory_drag_target.is_none(),
5742                    |this| {
5743                        this.border_color(transparent_white())
5744                            .bg(item_colors.drag_over)
5745                    },
5746                )
5747                .when(settings.drag_and_drop, |this| {
5748                    let path_for_external_paths = path.clone();
5749                    let path_for_dragged_selection = path.clone();
5750                    let dragged_selection = DraggedSelection {
5751                        active_selection: selection,
5752                        marked_selections: marked_selections.clone(),
5753                    };
5754
5755                    this.on_drag_move::<ExternalPaths>(cx.listener(
5756                        move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
5757                            let is_current_target =
5758                                this.drag_target_entry
5759                                    .as_ref()
5760                                    .and_then(|entry| match entry {
5761                                        DragTarget::Entry {
5762                                            entry_id: target_id,
5763                                            ..
5764                                        } => Some(*target_id),
5765                                        DragTarget::Background { .. } => None,
5766                                    })
5767                                    == Some(entry_id);
5768
5769                            if !event.bounds.contains(&event.event.position) {
5770                                // Entry responsible for setting drag target is also responsible to
5771                                // clear it up after drag is out of bounds
5772                                if is_current_target {
5773                                    this.drag_target_entry = None;
5774                                }
5775                                return;
5776                            }
5777
5778                            if is_current_target {
5779                                return;
5780                            }
5781
5782                            this.marked_entries.clear();
5783
5784                            let Some((entry_id, highlight_entry_id)) = maybe!({
5785                                let target_worktree = this
5786                                    .project
5787                                    .read(cx)
5788                                    .worktree_for_id(selection.worktree_id, cx)?
5789                                    .read(cx);
5790                                let target_entry =
5791                                    target_worktree.entry_for_path(&path_for_external_paths)?;
5792                                let highlight_entry_id = this.highlight_entry_for_external_drag(
5793                                    target_entry,
5794                                    target_worktree,
5795                                )?;
5796                                Some((target_entry.id, highlight_entry_id))
5797                            }) else {
5798                                return;
5799                            };
5800
5801                            this.drag_target_entry = Some(DragTarget::Entry {
5802                                entry_id,
5803                                highlight_entry_id,
5804                            });
5805                        },
5806                    ))
5807                    .on_drop(cx.listener(
5808                        move |this, external_paths: &ExternalPaths, window, cx| {
5809                            this.clear_drag_state(cx);
5810                            this.drop_external_files(external_paths.paths(), entry_id, window, cx);
5811                            cx.stop_propagation();
5812                        },
5813                    ))
5814                    .on_drag_move::<DraggedSelection>(cx.listener(
5815                        move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
5816                            let is_current_target =
5817                                this.drag_target_entry
5818                                    .as_ref()
5819                                    .and_then(|entry| match entry {
5820                                        DragTarget::Entry {
5821                                            entry_id: target_id,
5822                                            ..
5823                                        } => Some(*target_id),
5824                                        DragTarget::Background { .. } => None,
5825                                    })
5826                                    == Some(entry_id);
5827
5828                            if !event.bounds.contains(&event.event.position) {
5829                                // Entry responsible for setting drag target is also responsible to
5830                                // clear it up after drag is out of bounds
5831                                if is_current_target {
5832                                    this.drag_target_entry = None;
5833                                }
5834                                return;
5835                            }
5836
5837                            if is_current_target {
5838                                return;
5839                            }
5840
5841                            let drag_state = event.drag(cx);
5842
5843                            if drag_state.items().count() == 1 {
5844                                this.marked_entries.clear();
5845                                this.marked_entries.push(drag_state.active_selection);
5846                            }
5847
5848                            let Some((entry_id, highlight_entry_id)) = maybe!({
5849                                let target_worktree = this
5850                                    .project
5851                                    .read(cx)
5852                                    .worktree_for_id(selection.worktree_id, cx)?
5853                                    .read(cx);
5854                                let target_entry =
5855                                    target_worktree.entry_for_path(&path_for_dragged_selection)?;
5856                                let highlight_entry_id = this.highlight_entry_for_selection_drag(
5857                                    target_entry,
5858                                    target_worktree,
5859                                    drag_state,
5860                                    cx,
5861                                )?;
5862                                Some((target_entry.id, highlight_entry_id))
5863                            }) else {
5864                                return;
5865                            };
5866
5867                            this.drag_target_entry = Some(DragTarget::Entry {
5868                                entry_id,
5869                                highlight_entry_id,
5870                            });
5871
5872                            this.hover_expand_task.take();
5873
5874                            if !kind.is_dir()
5875                                || this
5876                                    .state
5877                                    .expanded_dir_ids
5878                                    .get(&details.worktree_id)
5879                                    .is_some_and(|ids| ids.binary_search(&entry_id).is_ok())
5880                            {
5881                                return;
5882                            }
5883
5884                            let bounds = event.bounds;
5885                            this.hover_expand_task =
5886                                Some(cx.spawn_in(window, async move |this, cx| {
5887                                    cx.background_executor()
5888                                        .timer(Duration::from_millis(500))
5889                                        .await;
5890                                    this.update_in(cx, |this, window, cx| {
5891                                        this.hover_expand_task.take();
5892                                        if this.drag_target_entry.as_ref().and_then(|entry| {
5893                                            match entry {
5894                                                DragTarget::Entry {
5895                                                    entry_id: target_id,
5896                                                    ..
5897                                                } => Some(*target_id),
5898                                                DragTarget::Background { .. } => None,
5899                                            }
5900                                        }) == Some(entry_id)
5901                                            && bounds.contains(&window.mouse_position())
5902                                        {
5903                                            this.expand_entry(worktree_id, entry_id, cx);
5904                                            this.update_visible_entries(
5905                                                Some((worktree_id, entry_id)),
5906                                                false,
5907                                                false,
5908                                                window,
5909                                                cx,
5910                                            );
5911                                            cx.notify();
5912                                        }
5913                                    })
5914                                    .ok();
5915                                }));
5916                        },
5917                    ))
5918                    .on_drag(dragged_selection, {
5919                        let active_component =
5920                            self.state.ancestors.get(&entry_id).and_then(|ancestors| {
5921                                ancestors.active_component(&details.filename)
5922                            });
5923                        move |selection, click_offset, _window, cx| {
5924                            let filename = active_component
5925                                .as_ref()
5926                                .unwrap_or_else(|| &details.filename);
5927                            cx.new(|_| DraggedProjectEntryView {
5928                                icon: details.icon.clone(),
5929                                filename: filename.clone(),
5930                                click_offset,
5931                                selection: selection.active_selection,
5932                                selections: selection.marked_selections.clone(),
5933                            })
5934                        }
5935                    })
5936                    .on_drop(cx.listener(
5937                        move |this, selections: &DraggedSelection, window, cx| {
5938                            this.clear_drag_state(cx);
5939                            if folded_directory_drag_target.is_some() {
5940                                return;
5941                            }
5942                            this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
5943                        },
5944                    ))
5945                })
5946            })
5947            .on_mouse_down(
5948                MouseButton::Left,
5949                cx.listener(move |this, _, _, cx| {
5950                    this.mouse_down = true;
5951                    cx.propagate();
5952                }),
5953            )
5954            .on_click(
5955                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
5956                    if event.is_right_click() || show_editor {
5957                        return;
5958                    }
5959                    if event.standard_click() {
5960                        project_panel.mouse_down = false;
5961                    }
5962                    cx.stop_propagation();
5963
5964                    if let Some(selection) =
5965                        project_panel.selection.filter(|_| event.modifiers().shift)
5966                    {
5967                        let current_selection = project_panel.index_for_selection(selection);
5968                        let clicked_entry = SelectedEntry {
5969                            entry_id,
5970                            worktree_id,
5971                        };
5972                        let target_selection = project_panel.index_for_selection(clicked_entry);
5973                        if let Some(((_, _, source_index), (_, _, target_index))) =
5974                            current_selection.zip(target_selection)
5975                        {
5976                            let range_start = source_index.min(target_index);
5977                            let range_end = source_index.max(target_index) + 1;
5978                            let mut new_selections = Vec::new();
5979                            project_panel.for_each_visible_entry(
5980                                range_start..range_end,
5981                                window,
5982                                cx,
5983                                &mut |entry_id, details, _, _| {
5984                                    new_selections.push(SelectedEntry {
5985                                        entry_id,
5986                                        worktree_id: details.worktree_id,
5987                                    });
5988                                },
5989                            );
5990
5991                            for selection in &new_selections {
5992                                if !project_panel.marked_entries.contains(selection) {
5993                                    project_panel.marked_entries.push(*selection);
5994                                }
5995                            }
5996
5997                            project_panel.selection = Some(clicked_entry);
5998                            if !project_panel.marked_entries.contains(&clicked_entry) {
5999                                project_panel.marked_entries.push(clicked_entry);
6000                            }
6001                        }
6002                    } else if event.modifiers().secondary() {
6003                        if event.click_count() > 1 {
6004                            project_panel.split_entry(entry_id, false, None, cx);
6005                        } else {
6006                            project_panel.selection = Some(selection);
6007                            if let Some(position) = project_panel
6008                                .marked_entries
6009                                .iter()
6010                                .position(|e| *e == selection)
6011                            {
6012                                project_panel.marked_entries.remove(position);
6013                            } else {
6014                                project_panel.marked_entries.push(selection);
6015                            }
6016                        }
6017                    } else if kind.is_dir() {
6018                        project_panel.marked_entries.clear();
6019                        if is_sticky
6020                            && let Some((_, _, index)) =
6021                                project_panel.index_for_entry(entry_id, worktree_id)
6022                        {
6023                            project_panel
6024                                .scroll_handle
6025                                .scroll_to_item_strict_with_offset(
6026                                    index,
6027                                    ScrollStrategy::Top,
6028                                    sticky_index.unwrap_or(0),
6029                                );
6030                            cx.notify();
6031                            // move down by 1px so that clicked item
6032                            // don't count as sticky anymore
6033                            cx.on_next_frame(window, |_, window, cx| {
6034                                cx.on_next_frame(window, |this, _, cx| {
6035                                    let mut offset = this.scroll_handle.offset();
6036                                    offset.y += px(1.);
6037                                    this.scroll_handle.set_offset(offset);
6038                                    cx.notify();
6039                                });
6040                            });
6041                            return;
6042                        }
6043                        if event.modifiers().alt {
6044                            project_panel.toggle_expand_all(entry_id, window, cx);
6045                        } else {
6046                            project_panel.toggle_expanded(entry_id, window, cx);
6047                        }
6048                    } else {
6049                        let preview_tabs_enabled =
6050                            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
6051                        let click_count = event.click_count();
6052                        let focus_opened_item = click_count > 1;
6053                        let allow_preview = preview_tabs_enabled && click_count == 1;
6054                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
6055                    }
6056                }),
6057            )
6058            .on_aux_click(
6059                cx.listener(move |project_panel, event: &gpui::ClickEvent, _, cx| {
6060                    if !event.is_middle_click() || show_editor || !kind.is_file() {
6061                        return;
6062                    }
6063
6064                    project_panel.open_entry(entry_id, true, false, cx);
6065                    cx.stop_propagation();
6066                }),
6067            )
6068            .child(
6069                ListItem::new(id)
6070                    .indent_level(depth)
6071                    .indent_step_size(px(settings.indent_size))
6072                    .spacing(match settings.entry_spacing {
6073                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
6074                        ProjectPanelEntrySpacing::Standard => ListItemSpacing::ExtraDense,
6075                    })
6076                    .selectable(false)
6077                    .when(
6078                        canonical_path.is_some()
6079                            || diagnostic_count.is_some()
6080                            || git_indicator.is_some(),
6081                        |this| {
6082                            let symlink_element = canonical_path.map(|path| {
6083                                div()
6084                                    .id("symlink_icon")
6085                                    .tooltip(move |_window, cx| {
6086                                        Tooltip::with_meta(
6087                                            path.to_string_lossy().into_owned(),
6088                                            None,
6089                                            "Symbolic Link",
6090                                            cx,
6091                                        )
6092                                    })
6093                                    .child(
6094                                        Icon::new(IconName::ArrowUpRight)
6095                                            .size(IconSize::Indicator)
6096                                            .color(filename_text_color),
6097                                    )
6098                            });
6099                            this.end_slot::<AnyElement>(
6100                                h_flex()
6101                                    .gap_1()
6102                                    .flex_none()
6103                                    .pr_3()
6104                                    .when_some(diagnostic_count, |this, count| {
6105                                        this.when(count.error_count > 0, |this| {
6106                                            this.child(
6107                                                Label::new(count.capped_error_count())
6108                                                    .size(LabelSize::Small)
6109                                                    .color(Color::Error),
6110                                            )
6111                                        })
6112                                        .when(
6113                                            count.warning_count > 0,
6114                                            |this| {
6115                                                this.child(
6116                                                    Label::new(count.capped_warning_count())
6117                                                        .size(LabelSize::Small)
6118                                                        .color(Color::Warning),
6119                                                )
6120                                            },
6121                                        )
6122                                    })
6123                                    .when_some(git_indicator, |this, (label, color)| {
6124                                        let git_indicator = if kind.is_dir() {
6125                                            Indicator::dot()
6126                                                .color(Color::Custom(color.color(cx).opacity(0.5)))
6127                                                .into_any_element()
6128                                        } else {
6129                                            Label::new(label)
6130                                                .size(LabelSize::Small)
6131                                                .color(color)
6132                                                .into_any_element()
6133                                        };
6134
6135                                        this.child(git_indicator)
6136                                    })
6137                                    .when_some(symlink_element, |this, el| this.child(el))
6138                                    .into_any_element(),
6139                            )
6140                        },
6141                    )
6142                    .child(if let Some(icon) = &icon {
6143                        if let Some((_, decoration_color)) =
6144                            entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
6145                        {
6146                            let is_warning = diagnostic_severity
6147                                .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
6148                                .unwrap_or(false);
6149                            div().child(
6150                                DecoratedIcon::new(
6151                                    Icon::from_path(icon.clone()).color(Color::Muted),
6152                                    Some(
6153                                        IconDecoration::new(
6154                                            if kind.is_file() {
6155                                                if is_warning {
6156                                                    IconDecorationKind::Triangle
6157                                                } else {
6158                                                    IconDecorationKind::X
6159                                                }
6160                                            } else {
6161                                                IconDecorationKind::Dot
6162                                            },
6163                                            bg_color,
6164                                            cx,
6165                                        )
6166                                        .group_name(Some(GROUP_NAME.into()))
6167                                        .knockout_hover_color(bg_hover_color)
6168                                        .color(decoration_color.color(cx))
6169                                        .position(Point {
6170                                            x: px(-2.),
6171                                            y: px(-2.),
6172                                        }),
6173                                    ),
6174                                )
6175                                .into_any_element(),
6176                            )
6177                        } else {
6178                            h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
6179                        }
6180                    } else if let Some((icon_name, color)) =
6181                        entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
6182                    {
6183                        h_flex()
6184                            .size(IconSize::default().rems())
6185                            .child(Icon::new(icon_name).color(color).size(IconSize::Small))
6186                    } else {
6187                        h_flex()
6188                            .size(IconSize::default().rems())
6189                            .invisible()
6190                            .flex_none()
6191                    })
6192                    .child(if show_editor {
6193                        h_flex().h_6().w_full().child(self.filename_editor.clone())
6194                    } else {
6195                        h_flex()
6196                            .h_6()
6197                            .map(|this| match self.state.ancestors.get(&entry_id) {
6198                                Some(folded_ancestors) => {
6199                                    this.children(self.render_folder_elements(
6200                                        folded_ancestors,
6201                                        entry_id,
6202                                        file_name,
6203                                        path_style,
6204                                        is_sticky,
6205                                        kind.is_file(),
6206                                        is_active || is_marked,
6207                                        settings.drag_and_drop,
6208                                        settings.bold_folder_labels,
6209                                        item_colors.drag_over,
6210                                        folded_directory_drag_target,
6211                                        filename_text_color,
6212                                        cx,
6213                                    ))
6214                                }
6215
6216                                None => this.child(
6217                                    Label::new(file_name)
6218                                        .single_line()
6219                                        .color(filename_text_color)
6220                                        .when(
6221                                            settings.bold_folder_labels && kind.is_dir(),
6222                                            |this| this.weight(FontWeight::SEMIBOLD),
6223                                        )
6224                                        .into_any_element(),
6225                                ),
6226                            })
6227                    })
6228                    .on_secondary_mouse_down(cx.listener(
6229                        move |this, event: &MouseDownEvent, window, cx| {
6230                            // Stop propagation to prevent the catch-all context menu for the project
6231                            // panel from being deployed.
6232                            cx.stop_propagation();
6233                            // Some context menu actions apply to all marked entries. If the user
6234                            // right-clicks on an entry that is not marked, they may not realize the
6235                            // action applies to multiple entries. To avoid inadvertent changes, all
6236                            // entries are unmarked.
6237                            if !this.marked_entries.contains(&selection) {
6238                                this.marked_entries.clear();
6239                            }
6240                            this.deploy_context_menu(event.position, entry_id, window, cx);
6241                        },
6242                    ))
6243                    .overflow_x(),
6244            )
6245            .when_some(validation_color_and_message, |this, (color, message)| {
6246                this.relative().child(deferred(
6247                    div()
6248                        .occlude()
6249                        .absolute()
6250                        .top_full()
6251                        .left(px(-1.)) // Used px over rem so that it doesn't change with font size
6252                        .right(px(-0.5))
6253                        .py_1()
6254                        .px_2()
6255                        .border_1()
6256                        .border_color(color)
6257                        .bg(cx.theme().colors().background)
6258                        .child(
6259                            Label::new(message)
6260                                .color(Color::from(color))
6261                                .size(LabelSize::Small),
6262                        ),
6263                ))
6264            })
6265    }
6266
6267    fn render_folder_elements(
6268        &self,
6269        folded_ancestors: &FoldedAncestors,
6270        entry_id: ProjectEntryId,
6271        file_name: String,
6272        path_style: PathStyle,
6273        is_sticky: bool,
6274        is_file: bool,
6275        is_active_or_marked: bool,
6276        drag_and_drop_enabled: bool,
6277        bold_folder_labels: bool,
6278        drag_over_color: Hsla,
6279        folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
6280        filename_text_color: Color,
6281        cx: &Context<Self>,
6282    ) -> impl Iterator<Item = AnyElement> {
6283        let components = Path::new(&file_name)
6284            .components()
6285            .map(|comp| comp.as_os_str().to_string_lossy().into_owned())
6286            .collect::<Vec<_>>();
6287        let active_index = folded_ancestors.active_index();
6288        let components_len = components.len();
6289        let delimiter = SharedString::new(path_style.primary_separator());
6290
6291        let path_component_elements =
6292            components
6293                .into_iter()
6294                .enumerate()
6295                .map(move |(index, component)| {
6296                    div()
6297                        .id(SharedString::from(format!(
6298                            "project_panel_path_component_{}_{index}",
6299                            entry_id.to_usize()
6300                        )))
6301                        .when(index == 0, |this| this.ml_neg_0p5())
6302                        .px_0p5()
6303                        .rounded_xs()
6304                        .hover(|style| style.bg(cx.theme().colors().element_active))
6305                        .when(!is_sticky, |div| {
6306                            div.when(index != components_len - 1, |div| {
6307                                let target_entry_id = folded_ancestors
6308                                    .ancestors
6309                                    .get(components_len - 1 - index)
6310                                    .cloned();
6311                                div.when(drag_and_drop_enabled, |div| {
6312                                    div.on_drag_move(cx.listener(
6313                                        move |this,
6314                                              event: &DragMoveEvent<DraggedSelection>,
6315                                              _,
6316                                              _| {
6317                                            if event.bounds.contains(&event.event.position) {
6318                                                this.folded_directory_drag_target =
6319                                                    Some(FoldedDirectoryDragTarget {
6320                                                        entry_id,
6321                                                        index,
6322                                                        is_delimiter_target: false,
6323                                                    });
6324                                            } else {
6325                                                let is_current_target = this
6326                                                    .folded_directory_drag_target
6327                                                    .as_ref()
6328                                                    .is_some_and(|target| {
6329                                                        target.entry_id == entry_id
6330                                                            && target.index == index
6331                                                            && !target.is_delimiter_target
6332                                                    });
6333                                                if is_current_target {
6334                                                    this.folded_directory_drag_target = None;
6335                                                }
6336                                            }
6337                                        },
6338                                    ))
6339                                    .on_drop(cx.listener(
6340                                        move |this, selections: &DraggedSelection, window, cx| {
6341                                            this.clear_drag_state(cx);
6342                                            if let Some(target_entry_id) = target_entry_id {
6343                                                this.drag_onto(
6344                                                    selections,
6345                                                    target_entry_id,
6346                                                    is_file,
6347                                                    window,
6348                                                    cx,
6349                                                );
6350                                            }
6351                                        },
6352                                    ))
6353                                    .when(
6354                                        folded_directory_drag_target.is_some_and(|target| {
6355                                            target.entry_id == entry_id && target.index == index
6356                                        }),
6357                                        |this| this.bg(drag_over_color),
6358                                    )
6359                                })
6360                            })
6361                        })
6362                        .on_mouse_down(
6363                            MouseButton::Left,
6364                            cx.listener(move |this, _, _, cx| {
6365                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
6366                                    if folds.set_active_index(index) {
6367                                        cx.notify();
6368                                    }
6369                                }
6370                            }),
6371                        )
6372                        .on_mouse_down(
6373                            MouseButton::Right,
6374                            cx.listener(move |this, _, _, cx| {
6375                                if let Some(folds) = this.state.ancestors.get_mut(&entry_id) {
6376                                    if folds.set_active_index(index) {
6377                                        cx.notify();
6378                                    }
6379                                }
6380                            }),
6381                        )
6382                        .child(
6383                            Label::new(component)
6384                                .single_line()
6385                                .color(filename_text_color)
6386                                .when(bold_folder_labels && !is_file, |this| {
6387                                    this.weight(FontWeight::SEMIBOLD)
6388                                })
6389                                .when(index == active_index && is_active_or_marked, |this| {
6390                                    this.underline()
6391                                }),
6392                        )
6393                        .into_any()
6394                });
6395
6396        let mut separator_index = 0;
6397        itertools::intersperse_with(path_component_elements, move || {
6398            separator_index += 1;
6399            self.render_entry_path_separator(
6400                entry_id,
6401                separator_index,
6402                components_len,
6403                is_sticky,
6404                is_file,
6405                drag_and_drop_enabled,
6406                filename_text_color,
6407                &delimiter,
6408                folded_ancestors,
6409                cx,
6410            )
6411            .into_any()
6412        })
6413    }
6414
6415    fn render_entry_path_separator(
6416        &self,
6417        entry_id: ProjectEntryId,
6418        index: usize,
6419        components_len: usize,
6420        is_sticky: bool,
6421        is_file: bool,
6422        drag_and_drop_enabled: bool,
6423        filename_text_color: Color,
6424        delimiter: &SharedString,
6425        folded_ancestors: &FoldedAncestors,
6426        cx: &Context<Self>,
6427    ) -> Div {
6428        let delimiter_target_index = index - 1;
6429        let target_entry_id = folded_ancestors
6430            .ancestors
6431            .get(components_len - 1 - delimiter_target_index)
6432            .cloned();
6433        div()
6434            .when(!is_sticky, |div| {
6435                div.when(drag_and_drop_enabled, |div| {
6436                    div.on_drop(cx.listener(
6437                        move |this, selections: &DraggedSelection, window, cx| {
6438                            this.clear_drag_state(cx);
6439                            if let Some(target_entry_id) = target_entry_id {
6440                                this.drag_onto(selections, target_entry_id, is_file, window, cx);
6441                            }
6442                        },
6443                    ))
6444                    .on_drag_move(cx.listener(
6445                        move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
6446                            if event.bounds.contains(&event.event.position) {
6447                                this.folded_directory_drag_target =
6448                                    Some(FoldedDirectoryDragTarget {
6449                                        entry_id,
6450                                        index: delimiter_target_index,
6451                                        is_delimiter_target: true,
6452                                    });
6453                            } else {
6454                                let is_current_target =
6455                                    this.folded_directory_drag_target.is_some_and(|target| {
6456                                        target.entry_id == entry_id
6457                                            && target.index == delimiter_target_index
6458                                            && target.is_delimiter_target
6459                                    });
6460                                if is_current_target {
6461                                    this.folded_directory_drag_target = None;
6462                                }
6463                            }
6464                        },
6465                    ))
6466                })
6467            })
6468            .child(
6469                Label::new(delimiter.clone())
6470                    .single_line()
6471                    .color(filename_text_color),
6472            )
6473    }
6474
6475    fn details_for_entry(
6476        &self,
6477        entry: &Entry,
6478        worktree_id: WorktreeId,
6479        root_name: &RelPath,
6480        entries_paths: &HashSet<Arc<RelPath>>,
6481        git_status: GitSummary,
6482        sticky: Option<StickyDetails>,
6483        _window: &mut Window,
6484        cx: &mut Context<Self>,
6485    ) -> EntryDetails {
6486        let (show_file_icons, show_folder_icons) = {
6487            let settings = ProjectPanelSettings::get_global(cx);
6488            (settings.file_icons, settings.folder_icons)
6489        };
6490
6491        let expanded_entry_ids = self
6492            .state
6493            .expanded_dir_ids
6494            .get(&worktree_id)
6495            .map(Vec::as_slice)
6496            .unwrap_or(&[]);
6497        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
6498
6499        let icon = match entry.kind {
6500            EntryKind::File => {
6501                if show_file_icons {
6502                    FileIcons::get_icon(entry.path.as_std_path(), cx)
6503                } else {
6504                    None
6505                }
6506            }
6507            _ => {
6508                if show_folder_icons {
6509                    FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx)
6510                } else {
6511                    FileIcons::get_chevron_icon(is_expanded, cx)
6512                }
6513            }
6514        };
6515
6516        let path_style = self.project.read(cx).path_style(cx);
6517        let (depth, difference) =
6518            ProjectPanel::calculate_depth_and_difference(entry, entries_paths);
6519
6520        let filename = if difference > 1 {
6521            entry
6522                .path
6523                .last_n_components(difference)
6524                .map_or(String::new(), |suffix| {
6525                    suffix.display(path_style).to_string()
6526                })
6527        } else {
6528            entry
6529                .path
6530                .file_name()
6531                .map(|name| name.to_string())
6532                .unwrap_or_else(|| root_name.as_unix_str().to_string())
6533        };
6534
6535        let selection = SelectedEntry {
6536            worktree_id,
6537            entry_id: entry.id,
6538        };
6539        let is_marked = self.marked_entries.contains(&selection);
6540        let is_selected = self.selection == Some(selection);
6541
6542        let diagnostic_severity = self
6543            .diagnostics
6544            .get(&(worktree_id, entry.path.clone()))
6545            .cloned();
6546
6547        let diagnostic_count = self
6548            .diagnostic_counts
6549            .get(&(worktree_id, entry.path.clone()))
6550            .copied();
6551
6552        let filename_text_color =
6553            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
6554
6555        let is_cut = self
6556            .clipboard
6557            .as_ref()
6558            .is_some_and(|e| e.is_cut() && e.items().contains(&selection));
6559
6560        EntryDetails {
6561            filename,
6562            icon,
6563            path: entry.path.clone(),
6564            depth,
6565            kind: entry.kind,
6566            is_ignored: entry.is_ignored,
6567            is_expanded,
6568            is_selected,
6569            is_marked,
6570            is_editing: false,
6571            is_processing: false,
6572            is_cut,
6573            sticky,
6574            filename_text_color,
6575            diagnostic_severity,
6576            diagnostic_count,
6577            git_status,
6578            is_private: entry.is_private,
6579            worktree_id,
6580            canonical_path: entry.canonical_path.clone(),
6581        }
6582    }
6583
6584    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
6585        let mut dispatch_context = KeyContext::new_with_defaults();
6586        dispatch_context.add("ProjectPanel");
6587        dispatch_context.add("menu");
6588
6589        let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
6590            "editing"
6591        } else {
6592            "not_editing"
6593        };
6594
6595        dispatch_context.add(identifier);
6596        dispatch_context
6597    }
6598
6599    fn reveal_entry(
6600        &mut self,
6601        project: Entity<Project>,
6602        entry_id: ProjectEntryId,
6603        skip_ignored: bool,
6604        window: &mut Window,
6605        cx: &mut Context<Self>,
6606    ) -> Result<()> {
6607        let worktree = project
6608            .read(cx)
6609            .worktree_for_entry(entry_id, cx)
6610            .context("can't reveal a non-existent entry in the project panel")?;
6611        let worktree = worktree.read(cx);
6612        let worktree_id = worktree.id();
6613        let is_ignored = worktree
6614            .entry_for_id(entry_id)
6615            .is_none_or(|entry| entry.is_ignored && !entry.is_always_included);
6616        if skip_ignored && is_ignored {
6617            if self.index_for_entry(entry_id, worktree_id).is_none() {
6618                anyhow::bail!("can't reveal an ignored entry in the project panel");
6619            }
6620
6621            self.selection = Some(SelectedEntry {
6622                worktree_id,
6623                entry_id,
6624            });
6625            self.marked_entries.clear();
6626            self.marked_entries.push(SelectedEntry {
6627                worktree_id,
6628                entry_id,
6629            });
6630            self.autoscroll(cx);
6631            cx.notify();
6632            return Ok(());
6633        }
6634        let is_active_item_file_diff_view = self
6635            .workspace
6636            .upgrade()
6637            .and_then(|ws| ws.read(cx).active_item(cx))
6638            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
6639            .unwrap_or(false);
6640        if is_active_item_file_diff_view {
6641            return Ok(());
6642        }
6643
6644        self.expand_entry(worktree_id, entry_id, cx);
6645        self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx);
6646        self.marked_entries.clear();
6647        self.marked_entries.push(SelectedEntry {
6648            worktree_id,
6649            entry_id,
6650        });
6651        cx.notify();
6652        Ok(())
6653    }
6654
6655    fn find_active_indent_guide(
6656        &self,
6657        indent_guides: &[IndentGuideLayout],
6658        cx: &App,
6659    ) -> Option<usize> {
6660        let (worktree, entry) = self.selected_entry(cx)?;
6661
6662        // Find the parent entry of the indent guide, this will either be the
6663        // expanded folder we have selected, or the parent of the currently
6664        // selected file/collapsed directory
6665        let mut entry = entry;
6666        loop {
6667            let is_expanded_dir = entry.is_dir()
6668                && self
6669                    .state
6670                    .expanded_dir_ids
6671                    .get(&worktree.id())
6672                    .map(|ids| ids.binary_search(&entry.id).is_ok())
6673                    .unwrap_or(false);
6674            if is_expanded_dir {
6675                break;
6676            }
6677            entry = worktree.entry_for_path(&entry.path.parent()?)?;
6678        }
6679
6680        let (active_indent_range, depth) = {
6681            let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
6682            let child_paths = &self.state.visible_entries[worktree_ix].entries;
6683            let mut child_count = 0;
6684            let depth = entry.path.ancestors().count();
6685            while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
6686                if entry.path.ancestors().count() <= depth {
6687                    break;
6688                }
6689                child_count += 1;
6690            }
6691
6692            let start = ix + 1;
6693            let end = start + child_count;
6694
6695            let visible_worktree = &self.state.visible_entries[worktree_ix];
6696            let visible_worktree_entries = visible_worktree.index.get_or_init(|| {
6697                visible_worktree
6698                    .entries
6699                    .iter()
6700                    .map(|e| e.path.clone())
6701                    .collect()
6702            });
6703
6704            // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
6705            let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
6706            (start..end, depth)
6707        };
6708
6709        let candidates = indent_guides
6710            .iter()
6711            .enumerate()
6712            .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
6713
6714        for (i, indent) in candidates {
6715            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
6716            if active_indent_range.start <= indent.offset.y + indent.length
6717                && indent.offset.y <= active_indent_range.end
6718            {
6719                return Some(i);
6720            }
6721        }
6722        None
6723    }
6724
6725    fn render_sticky_entries(
6726        &self,
6727        child: StickyProjectPanelCandidate,
6728        window: &mut Window,
6729        cx: &mut Context<Self>,
6730    ) -> SmallVec<[AnyElement; 8]> {
6731        let project = self.project.read(cx);
6732
6733        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
6734            return SmallVec::new();
6735        };
6736
6737        let Some(visible) = self
6738            .state
6739            .visible_entries
6740            .iter()
6741            .find(|worktree| worktree.worktree_id == worktree_id)
6742        else {
6743            return SmallVec::new();
6744        };
6745
6746        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
6747            return SmallVec::new();
6748        };
6749        let worktree = worktree.read(cx).snapshot();
6750
6751        let paths = visible
6752            .index
6753            .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect());
6754
6755        let mut sticky_parents = Vec::new();
6756        let mut current_path = entry_ref.path.clone();
6757
6758        'outer: loop {
6759            if let Some(parent_path) = current_path.parent() {
6760                for ancestor_path in parent_path.ancestors() {
6761                    if paths.contains(ancestor_path)
6762                        && let Some(parent_entry) = worktree.entry_for_path(ancestor_path)
6763                    {
6764                        sticky_parents.push(parent_entry.clone());
6765                        current_path = parent_entry.path.clone();
6766                        continue 'outer;
6767                    }
6768                }
6769            }
6770            break 'outer;
6771        }
6772
6773        if sticky_parents.is_empty() {
6774            return SmallVec::new();
6775        }
6776
6777        sticky_parents.reverse();
6778
6779        let panel_settings = ProjectPanelSettings::get_global(cx);
6780        let git_status_enabled = panel_settings.git_status;
6781        let root_name = worktree.root_name();
6782
6783        let git_summaries_by_id = if git_status_enabled {
6784            visible
6785                .entries
6786                .iter()
6787                .map(|e| (e.id, e.git_summary))
6788                .collect::<HashMap<_, _>>()
6789        } else {
6790            Default::default()
6791        };
6792
6793        // already checked if non empty above
6794        let last_item_index = sticky_parents.len() - 1;
6795        let marked_selections: Arc<[SelectedEntry]> = Arc::from(self.marked_entries.clone());
6796        sticky_parents
6797            .iter()
6798            .enumerate()
6799            .map(|(index, entry)| {
6800                let git_status = git_summaries_by_id
6801                    .get(&entry.id)
6802                    .copied()
6803                    .unwrap_or_default();
6804                let sticky_details = Some(StickyDetails {
6805                    sticky_index: index,
6806                });
6807                let details = self.details_for_entry(
6808                    entry,
6809                    worktree_id,
6810                    root_name,
6811                    paths,
6812                    git_status,
6813                    sticky_details,
6814                    window,
6815                    cx,
6816                );
6817                self.render_entry(
6818                    entry.id,
6819                    details,
6820                    Arc::clone(&marked_selections),
6821                    window,
6822                    cx,
6823                )
6824                .when(index == last_item_index, |this| {
6825                    let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
6826                    let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
6827                    let sticky_shadow = div()
6828                        .absolute()
6829                        .left_0()
6830                        .bottom_neg_1p5()
6831                        .h_1p5()
6832                        .w_full()
6833                        .bg(linear_gradient(
6834                            0.,
6835                            linear_color_stop(shadow_color_top, 1.),
6836                            linear_color_stop(shadow_color_bottom, 0.),
6837                        ));
6838                    this.child(sticky_shadow)
6839                })
6840                .into_any()
6841            })
6842            .collect()
6843    }
6844}
6845
6846#[derive(Clone)]
6847struct StickyProjectPanelCandidate {
6848    index: usize,
6849    depth: usize,
6850}
6851
6852impl StickyCandidate for StickyProjectPanelCandidate {
6853    fn depth(&self) -> usize {
6854        self.depth
6855    }
6856}
6857
6858fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
6859    const ICON_SIZE_FACTOR: usize = 2;
6860    let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
6861    if is_symlink {
6862        item_width += ICON_SIZE_FACTOR;
6863    }
6864    item_width
6865}
6866
6867impl Render for ProjectPanel {
6868    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
6869        let has_worktree = !self.state.visible_entries.is_empty();
6870        let project = self.project.read(cx);
6871        let panel_settings = ProjectPanelSettings::get_global(cx);
6872        let indent_size = panel_settings.indent_size;
6873        let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
6874        let horizontal_scroll = panel_settings.scrollbar.horizontal_scroll;
6875        let show_sticky_entries = {
6876            if panel_settings.sticky_scroll {
6877                let is_scrollable = self.scroll_handle.is_scrollable();
6878                let is_scrolled = self.scroll_handle.offset().y < px(0.);
6879                is_scrollable && is_scrolled
6880            } else {
6881                false
6882            }
6883        };
6884
6885        // Trashing, undo, and redo rely on the `TrashProjectEntry` and
6886        // `RestoreProjectEntry` messages, which older collab hosts can't
6887        // decode, with the operation silently never completing. As such, we
6888        // keep all three disabled for collab guests. Trashing is already
6889        // disabled in collab today, so existing users will not lose any
6890        // functionality and the plan is to eventually remove this check, once a
6891        // considerable portion of collab hosts had the chance to upgrade to a
6892        // version that understands these messages.
6893        let is_collab = project.is_via_collab();
6894        let is_local = project.is_local();
6895        let supports_undo = cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>();
6896
6897        if has_worktree {
6898            let item_count = self
6899                .state
6900                .visible_entries
6901                .iter()
6902                .map(|worktree| worktree.entries.len())
6903                .sum();
6904
6905            fn handle_drag_move<T: 'static>(
6906                this: &mut ProjectPanel,
6907                e: &DragMoveEvent<T>,
6908                window: &mut Window,
6909                cx: &mut Context<ProjectPanel>,
6910            ) {
6911                if let Some(previous_position) = this.previous_drag_position {
6912                    // Refresh cursor only when an actual drag happens,
6913                    // because modifiers are not updated when the cursor is not moved.
6914                    if e.event.position != previous_position {
6915                        this.refresh_drag_cursor_style(&e.event.modifiers, window, cx);
6916                    }
6917                }
6918                this.previous_drag_position = Some(e.event.position);
6919
6920                if !e.bounds.contains(&e.event.position) {
6921                    this.clear_drag_state(cx);
6922                    return;
6923                }
6924                this.hover_scroll_task.take();
6925                let panel_height = e.bounds.size.height;
6926                if panel_height <= px(0.) {
6927                    return;
6928                }
6929
6930                let event_offset = e.event.position.y - e.bounds.origin.y;
6931                // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
6932                let hovered_region_offset = event_offset / panel_height;
6933
6934                // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
6935                // These pixels offsets were picked arbitrarily.
6936                let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
6937                    8.
6938                } else if hovered_region_offset <= 0.15 {
6939                    5.
6940                } else if hovered_region_offset >= 0.95 {
6941                    -8.
6942                } else if hovered_region_offset >= 0.85 {
6943                    -5.
6944                } else {
6945                    return;
6946                };
6947                let adjustment = point(px(0.), px(vertical_scroll_offset));
6948                this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
6949                    loop {
6950                        let should_stop_scrolling = this
6951                            .update(cx, |this, cx| {
6952                                this.hover_scroll_task.as_ref()?;
6953                                let handle = this.scroll_handle.0.borrow_mut();
6954                                let offset = handle.base_handle.offset();
6955
6956                                handle.base_handle.set_offset(offset + adjustment);
6957                                cx.notify();
6958                                Some(())
6959                            })
6960                            .ok()
6961                            .flatten()
6962                            .is_some();
6963                        if should_stop_scrolling {
6964                            return;
6965                        }
6966                        cx.background_executor()
6967                            .timer(Duration::from_millis(16))
6968                            .await;
6969                    }
6970                }));
6971            }
6972            h_flex()
6973                .id("project-panel")
6974                .group("project-panel")
6975                .when(panel_settings.drag_and_drop, |this| {
6976                    this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
6977                        .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
6978                        .on_mouse_exit(cx.listener(|this, _: &MouseExitEvent, window, cx| {
6979                            cx.stop_active_drag(window);
6980                            this.clear_drag_state(cx);
6981                        }))
6982                })
6983                .size_full()
6984                .relative()
6985                .on_modifiers_changed(cx.listener(
6986                    |this, event: &ModifiersChangedEvent, window, cx| {
6987                        this.refresh_drag_cursor_style(&event.modifiers, window, cx);
6988                    },
6989                ))
6990                .key_context(self.dispatch_context(window, cx))
6991                .on_action(cx.listener(Self::scroll_up))
6992                .on_action(cx.listener(Self::scroll_down))
6993                .on_action(cx.listener(Self::scroll_cursor_center))
6994                .on_action(cx.listener(Self::scroll_cursor_top))
6995                .on_action(cx.listener(Self::scroll_cursor_bottom))
6996                .on_action(cx.listener(Self::select_next))
6997                .on_action(cx.listener(Self::select_previous))
6998                .on_action(cx.listener(Self::select_first))
6999                .on_action(cx.listener(Self::select_last))
7000                .on_action(cx.listener(Self::select_parent))
7001                .on_action(cx.listener(Self::select_next_git_entry))
7002                .on_action(cx.listener(Self::select_prev_git_entry))
7003                .on_action(cx.listener(Self::select_next_diagnostic))
7004                .on_action(cx.listener(Self::select_prev_diagnostic))
7005                .on_action(cx.listener(Self::select_next_directory))
7006                .on_action(cx.listener(Self::select_prev_directory))
7007                .on_action(cx.listener(Self::expand_selected_entry))
7008                .on_action(cx.listener(Self::collapse_selected_entry))
7009                .on_action(cx.listener(Self::collapse_all_entries))
7010                .on_action(cx.listener(Self::expand_all_entries))
7011                .on_action(cx.listener(Self::collapse_selected_entry_and_children))
7012                .on_action(cx.listener(Self::expand_selected_entry_and_children))
7013                .on_action(cx.listener(Self::open))
7014                .on_action(cx.listener(Self::open_permanent))
7015                .on_action(cx.listener(Self::open_split_vertical))
7016                .on_action(cx.listener(Self::open_split_horizontal))
7017                .on_action(cx.listener(Self::open_markdown_preview))
7018                .on_action(cx.listener(Self::confirm))
7019                .on_action(cx.listener(Self::cancel))
7020                .on_action(cx.listener(Self::copy_path))
7021                .on_action(cx.listener(Self::copy_relative_path))
7022                .on_action(cx.listener(Self::new_search_in_directory))
7023                .on_action(cx.listener(Self::unfold_directory))
7024                .on_action(cx.listener(Self::fold_directory))
7025                .on_action(cx.listener(Self::remove_from_project))
7026                .on_action(cx.listener(Self::compare_marked_files))
7027                .when(!project.is_read_only(cx), |el| {
7028                    el.on_action(cx.listener(Self::new_file))
7029                        .on_action(cx.listener(Self::new_directory))
7030                        .on_action(cx.listener(Self::rename))
7031                        .on_action(cx.listener(Self::delete))
7032                        .on_action(cx.listener(Self::cut))
7033                        .on_action(cx.listener(Self::copy))
7034                        .on_action(cx.listener(Self::paste))
7035                        .on_action(cx.listener(Self::duplicate))
7036                        .on_action(cx.listener(Self::restore_file))
7037                        .on_action(cx.listener(Self::add_to_gitignore))
7038                        .on_action(cx.listener(Self::add_to_git_info_exclude))
7039                        .when(!is_collab, |el| el.on_action(cx.listener(Self::trash)))
7040                        .when(!is_collab && supports_undo, |el| {
7041                            el.on_action(cx.listener(Self::undo))
7042                                .on_action(cx.listener(Self::redo))
7043                        })
7044                })
7045                .when(
7046                    project.is_local() || project.is_via_wsl_with_host_interop(cx),
7047                    |el| {
7048                        el.on_action(cx.listener(Self::reveal_in_finder))
7049                            .on_action(cx.listener(Self::open_system))
7050                            .on_action(cx.listener(Self::open_in_terminal))
7051                    },
7052                )
7053                .when(project.is_via_remote_server(), |el| {
7054                    el.on_action(cx.listener(Self::open_in_terminal))
7055                        .on_action(cx.listener(Self::download_from_remote))
7056                })
7057                .track_focus(&self.focus_handle(cx))
7058                .child(
7059                    v_flex()
7060                        .child(
7061                            uniform_list("entries", item_count, {
7062                                cx.processor(|this, range: Range<usize>, window, cx| {
7063                                    this.rendered_entries_len = range.end - range.start;
7064                                    let mut items = Vec::with_capacity(this.rendered_entries_len);
7065                                    let marked_selections: Arc<[SelectedEntry]> =
7066                                        Arc::from(this.marked_entries.clone());
7067                                    this.for_each_visible_entry(
7068                                        range,
7069                                        window,
7070                                        cx,
7071                                        &mut |id, details, window, cx| {
7072                                            items.push(this.render_entry(
7073                                                id,
7074                                                details,
7075                                                Arc::clone(&marked_selections),
7076                                                window,
7077                                                cx,
7078                                            ));
7079                                        },
7080                                    );
7081                                    items
7082                                })
7083                            })
7084                            .when(show_indent_guides, |list| {
7085                                list.with_decoration(
7086                                    ui::indent_guides(
7087                                        px(indent_size),
7088                                        IndentGuideColors::panel(cx),
7089                                    )
7090                                    .with_compute_indents_fn(
7091                                        cx.entity(),
7092                                        |this, range, window, cx| {
7093                                            let mut items =
7094                                                SmallVec::with_capacity(range.end - range.start);
7095                                            this.iter_visible_entries(
7096                                                range,
7097                                                window,
7098                                                cx,
7099                                                &mut |entry, _, entries, _, _| {
7100                                                    let (depth, _) =
7101                                                        Self::calculate_depth_and_difference(
7102                                                            entry, entries,
7103                                                        );
7104                                                    items.push(depth);
7105                                                },
7106                                            );
7107                                            items
7108                                        },
7109                                    )
7110                                    .on_click(cx.listener(
7111                                        |this,
7112                                         active_indent_guide: &IndentGuideLayout,
7113                                         window,
7114                                         cx| {
7115                                            if window.modifiers().secondary() {
7116                                                let ix = active_indent_guide.offset.y;
7117                                                let Some((target_entry, worktree)) = maybe!({
7118                                                    let (worktree_id, entry) =
7119                                                        this.entry_at_index(ix)?;
7120                                                    let worktree = this
7121                                                        .project
7122                                                        .read(cx)
7123                                                        .worktree_for_id(worktree_id, cx)?;
7124                                                    let target_entry = worktree
7125                                                        .read(cx)
7126                                                        .entry_for_path(&entry.path.parent()?)?;
7127                                                    Some((target_entry, worktree))
7128                                                }) else {
7129                                                    return;
7130                                                };
7131
7132                                                this.collapse_entry(
7133                                                    target_entry.clone(),
7134                                                    worktree,
7135                                                    window,
7136                                                    cx,
7137                                                );
7138                                            }
7139                                        },
7140                                    ))
7141                                    .with_render_fn(
7142                                        cx.entity(),
7143                                        move |this, params, _, cx| {
7144                                            const LEFT_OFFSET: Pixels =
7145                                                ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET;
7146                                            const PADDING_Y: Pixels = px(4.);
7147                                            const HITBOX_OVERDRAW: Pixels = px(3.);
7148
7149                                            let active_indent_guide_index = this
7150                                                .find_active_indent_guide(
7151                                                    &params.indent_guides,
7152                                                    cx,
7153                                                );
7154
7155                                            let indent_size = params.indent_size;
7156                                            let item_height = params.item_height;
7157
7158                                            params
7159                                                .indent_guides
7160                                                .into_iter()
7161                                                .enumerate()
7162                                                .map(|(idx, layout)| {
7163                                                    let offset = if layout.continues_offscreen {
7164                                                        px(0.)
7165                                                    } else {
7166                                                        PADDING_Y
7167                                                    };
7168                                                    let bounds = Bounds::new(
7169                                                        point(
7170                                                            layout.offset.x * indent_size
7171                                                                + LEFT_OFFSET,
7172                                                            layout.offset.y * item_height + offset,
7173                                                        ),
7174                                                        size(
7175                                                            px(1.),
7176                                                            layout.length * item_height
7177                                                                - offset * 2.,
7178                                                        ),
7179                                                    );
7180                                                    ui::RenderedIndentGuide {
7181                                                        bounds,
7182                                                        layout,
7183                                                        is_active: Some(idx)
7184                                                            == active_indent_guide_index,
7185                                                        hitbox: Some(Bounds::new(
7186                                                            point(
7187                                                                bounds.origin.x - HITBOX_OVERDRAW,
7188                                                                bounds.origin.y,
7189                                                            ),
7190                                                            size(
7191                                                                bounds.size.width
7192                                                                    + HITBOX_OVERDRAW * 2.,
7193                                                                bounds.size.height,
7194                                                            ),
7195                                                        )),
7196                                                    }
7197                                                })
7198                                                .collect()
7199                                        },
7200                                    ),
7201                                )
7202                            })
7203                            .when(show_sticky_entries, |list| {
7204                                let sticky_items = ui::sticky_items(
7205                                    cx.entity(),
7206                                    |this, range, window, cx| {
7207                                        let mut items =
7208                                            SmallVec::with_capacity(range.end - range.start);
7209                                        this.iter_visible_entries(
7210                                            range,
7211                                            window,
7212                                            cx,
7213                                            &mut |entry, index, entries, _, _| {
7214                                                let (depth, _) =
7215                                                    Self::calculate_depth_and_difference(
7216                                                        entry, entries,
7217                                                    );
7218                                                let candidate =
7219                                                    StickyProjectPanelCandidate { index, depth };
7220                                                items.push(candidate);
7221                                            },
7222                                        );
7223                                        items
7224                                    },
7225                                    |this, marker_entry, window, cx| {
7226                                        let sticky_entries =
7227                                            this.render_sticky_entries(marker_entry, window, cx);
7228                                        this.sticky_items_count = sticky_entries.len();
7229                                        sticky_entries
7230                                    },
7231                                );
7232                                list.with_decoration(if show_indent_guides {
7233                                    sticky_items.with_decoration(
7234                                        ui::indent_guides(
7235                                            px(indent_size),
7236                                            IndentGuideColors::panel(cx),
7237                                        )
7238                                        .with_render_fn(
7239                                            cx.entity(),
7240                                            move |_, params, _, _| {
7241                                                const LEFT_OFFSET: Pixels =
7242                                                    ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET;
7243
7244                                                let indent_size = params.indent_size;
7245                                                let item_height = params.item_height;
7246
7247                                                params
7248                                                    .indent_guides
7249                                                    .into_iter()
7250                                                    .map(|layout| {
7251                                                        let bounds = Bounds::new(
7252                                                            point(
7253                                                                layout.offset.x * indent_size
7254                                                                    + LEFT_OFFSET,
7255                                                                layout.offset.y * item_height,
7256                                                            ),
7257                                                            size(
7258                                                                px(1.),
7259                                                                layout.length * item_height,
7260                                                            ),
7261                                                        );
7262                                                        ui::RenderedIndentGuide {
7263                                                            bounds,
7264                                                            layout,
7265                                                            is_active: false,
7266                                                            hitbox: None,
7267                                                        }
7268                                                    })
7269                                                    .collect()
7270                                            },
7271                                        ),
7272                                    )
7273                                } else {
7274                                    sticky_items
7275                                })
7276                            })
7277                            .with_sizing_behavior(ListSizingBehavior::Infer)
7278                            .with_horizontal_sizing_behavior(if horizontal_scroll {
7279                                ListHorizontalSizingBehavior::Unconstrained
7280                            } else {
7281                                ListHorizontalSizingBehavior::FitList
7282                            })
7283                            .when(horizontal_scroll, |list| {
7284                                list.with_width_from_item(self.state.max_width_item_index)
7285                            })
7286                            .track_scroll(&self.scroll_handle),
7287                        )
7288                        .child(
7289                            div()
7290                                .id("project-panel-blank-area")
7291                                .block_mouse_except_scroll()
7292                                // `block_mouse_except_scroll` prevents the dock's own
7293                                // focus-follows-mouse hover handler from seeing this area,
7294                                // so handle it here directly.
7295                                .focus_follows_mouse(
7296                                    WorkspaceSettings::get_global(cx).focus_follows_mouse,
7297                                    cx,
7298                                )
7299                                .flex_grow_1()
7300                                .on_scroll_wheel({
7301                                    let scroll_handle = self.scroll_handle.clone();
7302                                    let entity_id = cx.entity().entity_id();
7303                                    move |event, window, cx| {
7304                                        let state = scroll_handle.0.borrow();
7305                                        let base_handle = &state.base_handle;
7306                                        let current_offset = base_handle.offset();
7307                                        let max_offset = base_handle.max_offset();
7308                                        let delta = event.delta.pixel_delta(window.line_height());
7309                                        let new_offset = (current_offset + delta)
7310                                            .clamp(&max_offset.neg(), &Point::default());
7311
7312                                        if new_offset != current_offset {
7313                                            base_handle.set_offset(new_offset);
7314                                            cx.notify(entity_id);
7315                                        }
7316                                    }
7317                                })
7318                                .when(
7319                                    self.drag_target_entry.as_ref().is_some_and(
7320                                        |entry| match entry {
7321                                            DragTarget::Background => true,
7322                                            DragTarget::Entry {
7323                                                highlight_entry_id, ..
7324                                            } => self.state.last_worktree_root_id.is_some_and(
7325                                                |root_id| *highlight_entry_id == root_id,
7326                                            ),
7327                                        },
7328                                    ),
7329                                    |div| div.bg(cx.theme().colors().drop_target_background),
7330                                )
7331                                .on_drag_move::<ExternalPaths>(cx.listener(
7332                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
7333                                        let Some(_last_root_id) = this.state.last_worktree_root_id
7334                                        else {
7335                                            return;
7336                                        };
7337                                        if event.bounds.contains(&event.event.position) {
7338                                            this.drag_target_entry = Some(DragTarget::Background);
7339                                        } else {
7340                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
7341                                                matches!(e, DragTarget::Background)
7342                                            }) {
7343                                                this.drag_target_entry = None;
7344                                            }
7345                                        }
7346                                    },
7347                                ))
7348                                .on_drag_move::<DraggedSelection>(cx.listener(
7349                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
7350                                        let Some(last_root_id) = this.state.last_worktree_root_id
7351                                        else {
7352                                            return;
7353                                        };
7354                                        if event.bounds.contains(&event.event.position) {
7355                                            let drag_state = event.drag(cx);
7356                                            if this.should_highlight_background_for_selection_drag(
7357                                                &drag_state,
7358                                                last_root_id,
7359                                                cx,
7360                                            ) {
7361                                                this.drag_target_entry =
7362                                                    Some(DragTarget::Background);
7363                                            }
7364                                        } else {
7365                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
7366                                                matches!(e, DragTarget::Background)
7367                                            }) {
7368                                                this.drag_target_entry = None;
7369                                            }
7370                                        }
7371                                    },
7372                                ))
7373                                .on_drop(cx.listener(
7374                                    move |this, external_paths: &ExternalPaths, window, cx| {
7375                                        this.clear_drag_state(cx);
7376                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7377                                            this.drop_external_files(
7378                                                external_paths.paths(),
7379                                                entry_id,
7380                                                window,
7381                                                cx,
7382                                            );
7383                                        }
7384                                        cx.stop_propagation();
7385                                    },
7386                                ))
7387                                .on_drop(cx.listener(
7388                                    move |this, selections: &DraggedSelection, window, cx| {
7389                                        this.clear_drag_state(cx);
7390                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7391                                            this.drag_onto(selections, entry_id, false, window, cx);
7392                                        }
7393                                        cx.stop_propagation();
7394                                    },
7395                                ))
7396                                .on_click(cx.listener(|this, event, window, cx| {
7397                                    if matches!(event, gpui::ClickEvent::Keyboard(_)) {
7398                                        return;
7399                                    }
7400                                    cx.stop_propagation();
7401                                    this.selection = None;
7402                                    this.marked_entries.clear();
7403                                    this.focus_handle(cx).focus(window, cx);
7404                                }))
7405                                .on_mouse_down(
7406                                    MouseButton::Right,
7407                                    cx.listener(move |this, event: &MouseDownEvent, window, cx| {
7408                                        // When deploying the context menu anywhere below the last project entry,
7409                                        // act as if the user clicked the root of the last worktree.
7410                                        if let Some(entry_id) = this.state.last_worktree_root_id {
7411                                            this.deploy_context_menu(
7412                                                event.position,
7413                                                entry_id,
7414                                                window,
7415                                                cx,
7416                                            );
7417                                        }
7418                                    }),
7419                                )
7420                                .when(!project.is_read_only(cx), |el| {
7421                                    el.on_click(cx.listener(
7422                                        |this, event: &gpui::ClickEvent, window, cx| {
7423                                            if event.click_count() > 1
7424                                                && let Some(entry_id) =
7425                                                    this.state.last_worktree_root_id
7426                                            {
7427                                                let project = this.project.read(cx);
7428
7429                                                let worktree_id = if let Some(worktree) =
7430                                                    project.worktree_for_entry(entry_id, cx)
7431                                                {
7432                                                    worktree.read(cx).id()
7433                                                } else {
7434                                                    return;
7435                                                };
7436
7437                                                this.selection = Some(SelectedEntry {
7438                                                    worktree_id,
7439                                                    entry_id,
7440                                                });
7441
7442                                                this.new_file(&NewFile, window, cx);
7443                                            }
7444                                        },
7445                                    ))
7446                                }),
7447                        )
7448                        .size_full(),
7449                )
7450                .custom_scrollbars(
7451                    {
7452                        let mut scrollbars =
7453                            Scrollbars::for_settings::<ProjectPanelScrollbarProxy>()
7454                                .tracked_scroll_handle(&self.scroll_handle);
7455                        if horizontal_scroll {
7456                            scrollbars = scrollbars.with_track_along(
7457                                ScrollAxes::Horizontal,
7458                                cx.theme().colors().panel_background,
7459                            );
7460                        }
7461                        scrollbars.notify_content()
7462                    },
7463                    window,
7464                    cx,
7465                )
7466                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
7467                    deferred(
7468                        anchored()
7469                            .position(*position)
7470                            .anchor(gpui::Anchor::TopLeft)
7471                            .child(menu.clone()),
7472                    )
7473                    .with_priority(3)
7474                }))
7475        } else {
7476            let focus_handle = self.focus_handle(cx);
7477            let workspace = self.workspace.clone();
7478            let workspace_clone = self.workspace.clone();
7479
7480            v_flex()
7481                .id("empty-project_panel-wrapper")
7482                .size_full()
7483                .child(
7484                    ProjectEmptyState::new(
7485                        "Project Panel",
7486                        focus_handle.clone(),
7487                        KeyBinding::for_action_in(&workspace::Open::default(), &focus_handle, cx),
7488                    )
7489                    .on_open_project(move |_, window, cx| {
7490                        telemetry::event!("Project Panel Add Project Clicked");
7491                        workspace
7492                            .update(cx, |_, cx| {
7493                                window
7494                                    .dispatch_action(workspace::Open::default().boxed_clone(), cx);
7495                            })
7496                            .log_err();
7497                    })
7498                    .on_clone_repo(move |_, window, cx| {
7499                        telemetry::event!("Project Panel Clone Repo Clicked");
7500                        workspace_clone
7501                            .update(cx, |_, cx| {
7502                                window.dispatch_action(git::Clone.boxed_clone(), cx);
7503                            })
7504                            .log_err();
7505                    }),
7506                )
7507                .when(is_local, |div| {
7508                    div.when(panel_settings.drag_and_drop, |div| {
7509                        div.drag_over::<ExternalPaths>(|style, _, _, cx| {
7510                            style.bg(cx.theme().colors().drop_target_background)
7511                        })
7512                        .on_drop(cx.listener(
7513                            move |this, external_paths: &ExternalPaths, window, cx| {
7514                                this.clear_drag_state(cx);
7515                                if let Some(task) = this
7516                                    .workspace
7517                                    .update(cx, |workspace, cx| {
7518                                        workspace.open_workspace_for_paths(
7519                                            OpenMode::Activate,
7520                                            external_paths.paths().to_owned(),
7521                                            window,
7522                                            cx,
7523                                        )
7524                                    })
7525                                    .log_err()
7526                                {
7527                                    task.detach_and_log_err(cx);
7528                                }
7529                                cx.stop_propagation();
7530                            },
7531                        ))
7532                    })
7533                })
7534        }
7535    }
7536}
7537
7538impl Render for DraggedProjectEntryView {
7539    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7540        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
7541        h_flex()
7542            .font(ui_font)
7543            .pl(self.click_offset.x + px(12.))
7544            .pt(self.click_offset.y + px(12.))
7545            .child(
7546                div()
7547                    .flex()
7548                    .gap_1()
7549                    .items_center()
7550                    .py_1()
7551                    .px_2()
7552                    .rounded_lg()
7553                    .bg(cx.theme().colors().background)
7554                    .map(|this| {
7555                        if self.selections.len() > 1 && self.selections.contains(&self.selection) {
7556                            this.child(Label::new(format!("{} entries", self.selections.len())))
7557                        } else {
7558                            this.child(if let Some(icon) = &self.icon {
7559                                div().child(Icon::from_path(icon.clone()))
7560                            } else {
7561                                div()
7562                            })
7563                            .child(Label::new(self.filename.clone()))
7564                        }
7565                    }),
7566            )
7567    }
7568}
7569
7570impl EventEmitter<Event> for ProjectPanel {}
7571
7572impl EventEmitter<PanelEvent> for ProjectPanel {}
7573
7574impl Panel for ProjectPanel {
7575    fn position(&self, _: &Window, cx: &App) -> DockPosition {
7576        match ProjectPanelSettings::get_global(cx).dock {
7577            DockSide::Left => DockPosition::Left,
7578            DockSide::Right => DockPosition::Right,
7579        }
7580    }
7581
7582    fn position_is_valid(&self, position: DockPosition) -> bool {
7583        matches!(position, DockPosition::Left | DockPosition::Right)
7584    }
7585
7586    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
7587        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
7588            let dock = match position {
7589                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
7590                DockPosition::Right => DockSide::Right,
7591            };
7592            settings.project_panel.get_or_insert_default().dock = Some(dock);
7593        });
7594    }
7595
7596    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
7597        ProjectPanelSettings::get_global(cx).default_width
7598    }
7599
7600    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
7601        ProjectPanelSettings::get_global(cx)
7602            .button
7603            .then_some(IconName::FileTree)
7604    }
7605
7606    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
7607        Some("Project Panel")
7608    }
7609
7610    fn toggle_action(&self) -> Box<dyn Action> {
7611        Box::new(ToggleFocus)
7612    }
7613
7614    fn persistent_name() -> &'static str {
7615        "Project Panel"
7616    }
7617
7618    fn panel_key() -> &'static str {
7619        PROJECT_PANEL_KEY
7620    }
7621
7622    fn starts_open(&self, _: &Window, cx: &App) -> bool {
7623        if !ProjectPanelSettings::get_global(cx).starts_open {
7624            return false;
7625        }
7626
7627        let project = &self.project.read(cx);
7628        project.visible_worktrees(cx).any(|tree| {
7629            tree.read(cx)
7630                .root_entry()
7631                .is_some_and(|entry| entry.is_dir())
7632        })
7633    }
7634
7635    fn activation_priority(&self) -> u32 {
7636        1
7637    }
7638
7639    fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
7640        Some(workspace::HideStatusItem::new(|settings| {
7641            settings.project_panel.get_or_insert_default().button = Some(false);
7642        }))
7643    }
7644}
7645
7646impl ProjectPanel {
7647    pub fn select_path_for_test(&mut self, project_path: ProjectPath, cx: &App) {
7648        let Some(worktree) = self
7649            .project
7650            .read(cx)
7651            .worktree_for_id(project_path.worktree_id, cx)
7652        else {
7653            return;
7654        };
7655        let Some(entry) = worktree.read(cx).entry_for_path(project_path.path.as_ref()) else {
7656            return;
7657        };
7658        self.selection = Some(SelectedEntry {
7659            worktree_id: project_path.worktree_id,
7660            entry_id: entry.id,
7661        });
7662    }
7663}
7664
7665impl Focusable for ProjectPanel {
7666    fn focus_handle(&self, _cx: &App) -> FocusHandle {
7667        self.focus_handle.clone()
7668    }
7669}
7670
7671impl ClipboardEntry {
7672    fn is_cut(&self) -> bool {
7673        matches!(self, Self::Cut { .. })
7674    }
7675
7676    fn items(&self) -> &BTreeSet<SelectedEntry> {
7677        match self {
7678            ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
7679        }
7680    }
7681
7682    fn into_copy_entry(self) -> Self {
7683        match self {
7684            ClipboardEntry::Copied(_) => self,
7685            ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries),
7686        }
7687    }
7688}
7689
7690#[inline]
7691fn cmp_worktree_entries(
7692    a: &Entry,
7693    b: &Entry,
7694    mode: &settings::ProjectPanelSortMode,
7695    order: &settings::ProjectPanelSortOrder,
7696) -> cmp::Ordering {
7697    let a = (&*a.path, a.is_file());
7698    let b = (&*b.path, b.is_file());
7699    util::paths::compare_rel_paths_by(a, b, (*mode).into(), (*order).into())
7700}
7701
7702pub fn sort_worktree_entries(
7703    entries: &mut [impl AsRef<Entry>],
7704    mode: settings::ProjectPanelSortMode,
7705    order: settings::ProjectPanelSortOrder,
7706) {
7707    entries.sort_by(|lhs, rhs| cmp_worktree_entries(lhs.as_ref(), rhs.as_ref(), &mode, &order));
7708}
7709
7710pub fn par_sort_worktree_entries(
7711    entries: &mut Vec<GitEntry>,
7712    mode: settings::ProjectPanelSortMode,
7713    order: settings::ProjectPanelSortOrder,
7714) {
7715    entries.par_sort_by(|lhs, rhs| cmp_worktree_entries(lhs, rhs, &mode, &order));
7716}
7717
7718fn git_status_indicator(git_status: GitSummary) -> Option<(&'static str, Color)> {
7719    if git_status.conflict > 0 {
7720        return Some(("!", Color::Conflict));
7721    }
7722    if git_status.untracked > 0 {
7723        return Some(("U", Color::Created));
7724    }
7725    if git_status.worktree.deleted > 0 {
7726        return Some(("D", Color::Deleted));
7727    }
7728    if git_status.worktree.modified > 0 {
7729        return Some(("M", Color::Modified));
7730    }
7731    if git_status.index.deleted > 0 {
7732        return Some(("D", Color::Deleted));
7733    }
7734    if git_status.index.modified > 0 {
7735        return Some(("M", Color::Modified));
7736    }
7737    if git_status.index.added > 0 {
7738        return Some(("A", Color::Created));
7739    }
7740    None
7741}
7742
7743#[cfg(test)]
7744mod project_panel_tests;
7745mod tests;
7746
Served at tenant.openagents/omega Member data and write actions are omitted.