1mod project_panel_settings;
2mod utils;
3
4use anyhow::{Context as _, Result, anyhow};
5use client::{ErrorCode, ErrorExt};
6use collections::{BTreeSet, HashMap, hash_map};
7use command_palette_hooks::CommandPaletteFilter;
8use db::kvp::KEY_VALUE_STORE;
9use editor::{
10 Editor, EditorEvent, EditorSettings, ShowScrollbar,
11 items::{
12 entry_diagnostic_aware_icon_decoration_and_color,
13 entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
14 },
15 scroll::{Autoscroll, ScrollbarAutoHide},
16};
17use file_icons::FileIcons;
18use git::status::GitSummary;
19use gpui::{
20 Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
21 DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
22 Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
23 MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy,
24 Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions,
25 anchored, deferred, div, impl_actions, point, px, size, uniform_list,
26};
27use indexmap::IndexMap;
28use language::DiagnosticSeverity;
29use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
30use project::{
31 Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
32 ProjectPath, Worktree, WorktreeId,
33 git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
34 relativize_path,
35};
36use project_panel_settings::{
37 ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,
38};
39use schemars::JsonSchema;
40use serde::{Deserialize, Serialize};
41use settings::{Settings, SettingsStore, update_settings_file};
42use smallvec::SmallVec;
43use std::any::TypeId;
44use std::{
45 cell::OnceCell,
46 cmp,
47 collections::HashSet,
48 ffi::OsStr,
49 ops::Range,
50 path::{Path, PathBuf},
51 sync::Arc,
52 time::Duration,
53};
54use theme::ThemeSettings;
55use ui::{
56 Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors,
57 IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar,
58 ScrollbarState, Tooltip, prelude::*, v_flex,
59};
60use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
61use workspace::{
62 DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
63 Workspace,
64 dock::{DockPosition, Panel, PanelEvent},
65 notifications::{DetachAndPromptErr, NotifyTaskExt},
66};
67use worktree::CreatedEntry;
68
69const PROJECT_PANEL_KEY: &str = "ProjectPanel";
70const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
71
72pub struct ProjectPanel {
73 project: Entity<Project>,
74 fs: Arc<dyn Fs>,
75 focus_handle: FocusHandle,
76 scroll_handle: UniformListScrollHandle,
77 // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
78 // hovered over the start/end of a list.
79 hover_scroll_task: Option<Task<()>>,
80 visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
81 /// Maps from leaf project entry ID to the currently selected ancestor.
82 /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
83 /// project entries (and all non-leaf nodes are guaranteed to be directories).
84 ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
85 folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
86 last_worktree_root_id: Option<ProjectEntryId>,
87 last_selection_drag_over_entry: Option<ProjectEntryId>,
88 last_external_paths_drag_over_entry: Option<ProjectEntryId>,
89 expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
90 unfolded_dir_ids: HashSet<ProjectEntryId>,
91 // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
92 selection: Option<SelectedEntry>,
93 marked_entries: BTreeSet<SelectedEntry>,
94 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
95 edit_state: Option<EditState>,
96 filename_editor: Entity<Editor>,
97 clipboard: Option<ClipboardEntry>,
98 _dragged_entry_destination: Option<Arc<Path>>,
99 workspace: WeakEntity<Workspace>,
100 width: Option<Pixels>,
101 pending_serialization: Task<Option<()>>,
102 show_scrollbar: bool,
103 vertical_scrollbar_state: ScrollbarState,
104 horizontal_scrollbar_state: ScrollbarState,
105 hide_scrollbar_task: Option<Task<()>>,
106 diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
107 max_width_item_index: Option<usize>,
108 // We keep track of the mouse down state on entries so we don't flash the UI
109 // in case a user clicks to open a file.
110 mouse_down: bool,
111 hover_expand_task: Option<Task<()>>,
112}
113
114#[derive(Copy, Clone, Debug)]
115struct FoldedDirectoryDragTarget {
116 entry_id: ProjectEntryId,
117 index: usize,
118 /// Whether we are dragging over the delimiter rather than the component itself.
119 is_delimiter_target: bool,
120}
121
122#[derive(Clone, Debug)]
123enum ValidationState {
124 None,
125 Warning(String),
126 Error(String),
127}
128
129#[derive(Clone, Debug)]
130struct EditState {
131 worktree_id: WorktreeId,
132 entry_id: ProjectEntryId,
133 leaf_entry_id: Option<ProjectEntryId>,
134 is_dir: bool,
135 depth: usize,
136 processing_filename: Option<String>,
137 previously_focused: Option<SelectedEntry>,
138 validation_state: ValidationState,
139}
140
141impl EditState {
142 fn is_new_entry(&self) -> bool {
143 self.leaf_entry_id.is_none()
144 }
145}
146
147#[derive(Clone, Debug)]
148enum ClipboardEntry {
149 Copied(BTreeSet<SelectedEntry>),
150 Cut(BTreeSet<SelectedEntry>),
151}
152
153#[derive(Debug, PartialEq, Eq, Clone)]
154struct EntryDetails {
155 filename: String,
156 icon: Option<SharedString>,
157 path: Arc<Path>,
158 depth: usize,
159 kind: EntryKind,
160 is_ignored: bool,
161 is_expanded: bool,
162 is_selected: bool,
163 is_marked: bool,
164 is_editing: bool,
165 is_processing: bool,
166 is_cut: bool,
167 filename_text_color: Color,
168 diagnostic_severity: Option<DiagnosticSeverity>,
169 git_status: GitSummary,
170 is_private: bool,
171 worktree_id: WorktreeId,
172 canonical_path: Option<Arc<Path>>,
173}
174
175#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
176#[serde(deny_unknown_fields)]
177struct Delete {
178 #[serde(default)]
179 pub skip_prompt: bool,
180}
181
182#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
183#[serde(deny_unknown_fields)]
184struct Trash {
185 #[serde(default)]
186 pub skip_prompt: bool,
187}
188
189impl_actions!(project_panel, [Delete, Trash]);
190
191actions!(
192 project_panel,
193 [
194 ExpandSelectedEntry,
195 CollapseSelectedEntry,
196 CollapseAllEntries,
197 NewDirectory,
198 NewFile,
199 Copy,
200 Duplicate,
201 RevealInFileManager,
202 RemoveFromProject,
203 OpenWithSystem,
204 Cut,
205 Paste,
206 Rename,
207 Open,
208 OpenPermanent,
209 ToggleFocus,
210 ToggleHideGitIgnore,
211 NewSearchInDirectory,
212 UnfoldDirectory,
213 FoldDirectory,
214 SelectParent,
215 SelectNextGitEntry,
216 SelectPrevGitEntry,
217 SelectNextDiagnostic,
218 SelectPrevDiagnostic,
219 SelectNextDirectory,
220 SelectPrevDirectory,
221 ]
222);
223
224#[derive(Debug, Default)]
225struct FoldedAncestors {
226 current_ancestor_depth: usize,
227 ancestors: Vec<ProjectEntryId>,
228}
229
230impl FoldedAncestors {
231 fn max_ancestor_depth(&self) -> usize {
232 self.ancestors.len()
233 }
234}
235
236pub fn init_settings(cx: &mut App) {
237 ProjectPanelSettings::register(cx);
238}
239
240pub fn init(cx: &mut App) {
241 init_settings(cx);
242
243 cx.observe_new(|workspace: &mut Workspace, _, _| {
244 workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
245 workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
246 });
247
248 workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
249 let fs = workspace.app_state().fs.clone();
250 update_settings_file::<ProjectPanelSettings>(fs, cx, move |setting, _| {
251 setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false));
252 })
253 });
254 })
255 .detach();
256}
257
258#[derive(Debug)]
259pub enum Event {
260 OpenedEntry {
261 entry_id: ProjectEntryId,
262 focus_opened_item: bool,
263 allow_preview: bool,
264 },
265 SplitEntry {
266 entry_id: ProjectEntryId,
267 },
268 Focus,
269}
270
271#[derive(Serialize, Deserialize)]
272struct SerializedProjectPanel {
273 width: Option<Pixels>,
274}
275
276struct DraggedProjectEntryView {
277 selection: SelectedEntry,
278 details: EntryDetails,
279 click_offset: Point<Pixels>,
280 selections: Arc<BTreeSet<SelectedEntry>>,
281}
282
283struct ItemColors {
284 default: Hsla,
285 hover: Hsla,
286 drag_over: Hsla,
287 marked: Hsla,
288 focused: Hsla,
289}
290
291fn get_item_color(cx: &App) -> ItemColors {
292 let colors = cx.theme().colors();
293
294 ItemColors {
295 default: colors.panel_background,
296 hover: colors.element_hover,
297 marked: colors.element_selected,
298 focused: colors.panel_focused_border,
299 drag_over: colors.drop_target_background,
300 }
301}
302
303impl ProjectPanel {
304 fn new(
305 workspace: &mut Workspace,
306 window: &mut Window,
307 cx: &mut Context<Workspace>,
308 ) -> Entity<Self> {
309 let project = workspace.project().clone();
310 let git_store = project.read(cx).git_store().clone();
311 let project_panel = cx.new(|cx| {
312 let focus_handle = cx.focus_handle();
313 cx.on_focus(&focus_handle, window, Self::focus_in).detach();
314 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
315 this.focus_out(window, cx);
316 this.hide_scrollbar(window, cx);
317 })
318 .detach();
319
320 cx.subscribe(&git_store, |this, _, event, cx| match event {
321 GitStoreEvent::RepositoryUpdated(_, _, _)
322 | GitStoreEvent::RepositoryAdded(_)
323 | GitStoreEvent::RepositoryRemoved(_) => {
324 this.update_visible_entries(None, cx);
325 cx.notify();
326 }
327 _ => {}
328 })
329 .detach();
330
331 cx.subscribe(&project, |this, project, event, cx| match event {
332 project::Event::ActiveEntryChanged(Some(entry_id)) => {
333 if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
334 this.reveal_entry(project.clone(), *entry_id, true, cx).ok();
335 }
336 }
337 project::Event::ActiveEntryChanged(None) => {
338 this.marked_entries.clear();
339 }
340 project::Event::RevealInProjectPanel(entry_id) => {
341 if let Some(()) = this
342 .reveal_entry(project.clone(), *entry_id, false, cx)
343 .log_err()
344 {
345 cx.emit(PanelEvent::Activate);
346 }
347 }
348 project::Event::ActivateProjectPanel => {
349 cx.emit(PanelEvent::Activate);
350 }
351 project::Event::DiskBasedDiagnosticsFinished { .. }
352 | project::Event::DiagnosticsUpdated { .. } => {
353 if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off
354 {
355 this.update_diagnostics(cx);
356 cx.notify();
357 }
358 }
359 project::Event::WorktreeRemoved(id) => {
360 this.expanded_dir_ids.remove(id);
361 this.update_visible_entries(None, cx);
362 cx.notify();
363 }
364 project::Event::WorktreeUpdatedEntries(_, _)
365 | project::Event::WorktreeAdded(_)
366 | project::Event::WorktreeOrderChanged => {
367 this.update_visible_entries(None, cx);
368 cx.notify();
369 }
370 project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
371 if let Some((worktree, expanded_dir_ids)) = project
372 .read(cx)
373 .worktree_for_id(*worktree_id, cx)
374 .zip(this.expanded_dir_ids.get_mut(&worktree_id))
375 {
376 let worktree = worktree.read(cx);
377
378 let Some(entry) = worktree.entry_for_id(*entry_id) else {
379 return;
380 };
381 let include_ignored_dirs = !entry.is_ignored;
382
383 let mut dirs_to_expand = vec![*entry_id];
384 while let Some(current_id) = dirs_to_expand.pop() {
385 let Some(current_entry) = worktree.entry_for_id(current_id) else {
386 continue;
387 };
388 for child in worktree.child_entries(¤t_entry.path) {
389 if !child.is_dir() || (include_ignored_dirs && child.is_ignored) {
390 continue;
391 }
392
393 dirs_to_expand.push(child.id);
394
395 if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
396 expanded_dir_ids.insert(ix, child.id);
397 }
398 this.unfolded_dir_ids.insert(child.id);
399 }
400 }
401 this.update_visible_entries(None, cx);
402 cx.notify();
403 }
404 }
405 _ => {}
406 })
407 .detach();
408
409 let trash_action = [TypeId::of::<Trash>()];
410 let is_remote = project.read(cx).is_via_collab();
411
412 if is_remote {
413 CommandPaletteFilter::update_global(cx, |filter, _cx| {
414 filter.hide_action_types(&trash_action);
415 });
416 }
417
418 let filename_editor = cx.new(|cx| Editor::single_line(window, cx));
419
420 cx.subscribe(
421 &filename_editor,
422 |project_panel, _, editor_event, cx| match editor_event {
423 EditorEvent::BufferEdited => {
424 project_panel.populate_validation_error(cx);
425 project_panel.autoscroll(cx);
426 }
427 EditorEvent::SelectionsChanged { .. } => {
428 project_panel.autoscroll(cx);
429 }
430 EditorEvent::Blurred => {
431 if project_panel
432 .edit_state
433 .as_ref()
434 .map_or(false, |state| state.processing_filename.is_none())
435 {
436 project_panel.edit_state = None;
437 project_panel.update_visible_entries(None, cx);
438 cx.notify();
439 }
440 }
441 _ => {}
442 },
443 )
444 .detach();
445
446 cx.observe_global::<FileIcons>(|_, cx| {
447 cx.notify();
448 })
449 .detach();
450
451 let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
452 cx.observe_global::<SettingsStore>(move |this, cx| {
453 let new_settings = *ProjectPanelSettings::get_global(cx);
454 if project_panel_settings != new_settings {
455 if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
456 this.update_visible_entries(None, cx);
457 }
458 project_panel_settings = new_settings;
459 this.update_diagnostics(cx);
460 cx.notify();
461 }
462 })
463 .detach();
464
465 let scroll_handle = UniformListScrollHandle::new();
466 let mut this = Self {
467 project: project.clone(),
468 hover_scroll_task: None,
469 fs: workspace.app_state().fs.clone(),
470 focus_handle,
471 visible_entries: Default::default(),
472 ancestors: Default::default(),
473 folded_directory_drag_target: None,
474 last_worktree_root_id: Default::default(),
475 last_external_paths_drag_over_entry: None,
476 last_selection_drag_over_entry: None,
477 expanded_dir_ids: Default::default(),
478 unfolded_dir_ids: Default::default(),
479 selection: None,
480 marked_entries: Default::default(),
481 edit_state: None,
482 context_menu: None,
483 filename_editor,
484 clipboard: None,
485 _dragged_entry_destination: None,
486 workspace: workspace.weak_handle(),
487 width: None,
488 pending_serialization: Task::ready(None),
489 show_scrollbar: !Self::should_autohide_scrollbar(cx),
490 hide_scrollbar_task: None,
491 vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
492 .parent_entity(&cx.entity()),
493 horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
494 .parent_entity(&cx.entity()),
495 max_width_item_index: None,
496 diagnostics: Default::default(),
497 scroll_handle,
498 mouse_down: false,
499 hover_expand_task: None,
500 };
501 this.update_visible_entries(None, cx);
502
503 this
504 });
505
506 cx.subscribe_in(&project_panel, window, {
507 let project_panel = project_panel.downgrade();
508 move |workspace, _, event, window, cx| match event {
509 &Event::OpenedEntry {
510 entry_id,
511 focus_opened_item,
512 allow_preview,
513 } => {
514 if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
515 if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
516 let file_path = entry.path.clone();
517 let worktree_id = worktree.read(cx).id();
518 let entry_id = entry.id;
519 let is_via_ssh = project.read(cx).is_via_ssh();
520
521 workspace
522 .open_path_preview(
523 ProjectPath {
524 worktree_id,
525 path: file_path.clone(),
526 },
527 None,
528 focus_opened_item,
529 allow_preview,
530 true,
531 window, cx,
532 )
533 .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| {
534 match e.error_code() {
535 ErrorCode::Disconnected => if is_via_ssh {
536 Some("Disconnected from SSH host".to_string())
537 } else {
538 Some("Disconnected from remote project".to_string())
539 },
540 ErrorCode::UnsharedItem => Some(format!(
541 "{} is not shared by the host. This could be because it has been marked as `private`",
542 file_path.display()
543 )),
544 // See note in worktree.rs where this error originates. Returning Some in this case prevents
545 // the error popup from saying "Try Again", which is a red herring in this case
546 ErrorCode::Internal if e.to_string().contains("File is too large to load") => Some(e.to_string()),
547 _ => None,
548 }
549 });
550
551 if let Some(project_panel) = project_panel.upgrade() {
552 // Always select and mark the entry, regardless of whether it is opened or not.
553 project_panel.update(cx, |project_panel, _| {
554 let entry = SelectedEntry { worktree_id, entry_id };
555 project_panel.marked_entries.clear();
556 project_panel.marked_entries.insert(entry);
557 project_panel.selection = Some(entry);
558 });
559 if !focus_opened_item {
560 let focus_handle = project_panel.read(cx).focus_handle.clone();
561 window.focus(&focus_handle);
562 }
563 }
564 }
565 }
566 }
567 &Event::SplitEntry { entry_id } => {
568 if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
569 if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
570 workspace
571 .split_path(
572 ProjectPath {
573 worktree_id: worktree.read(cx).id(),
574 path: entry.path.clone(),
575 },
576 window, cx,
577 )
578 .detach_and_log_err(cx);
579 }
580 }
581 }
582
583 _ => {}
584 }
585 })
586 .detach();
587
588 project_panel
589 }
590
591 pub async fn load(
592 workspace: WeakEntity<Workspace>,
593 mut cx: AsyncWindowContext,
594 ) -> Result<Entity<Self>> {
595 let serialized_panel = cx
596 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
597 .await
598 .map_err(|e| anyhow!("Failed to load project panel: {}", e))
599 .log_err()
600 .flatten()
601 .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
602 .transpose()
603 .log_err()
604 .flatten();
605
606 workspace.update_in(&mut cx, |workspace, window, cx| {
607 let panel = ProjectPanel::new(workspace, window, cx);
608 if let Some(serialized_panel) = serialized_panel {
609 panel.update(cx, |panel, cx| {
610 panel.width = serialized_panel.width.map(|px| px.round());
611 cx.notify();
612 });
613 }
614 panel
615 })
616 }
617
618 fn update_diagnostics(&mut self, cx: &mut Context<Self>) {
619 let mut diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity> =
620 Default::default();
621 let show_diagnostics_setting = ProjectPanelSettings::get_global(cx).show_diagnostics;
622
623 if show_diagnostics_setting != ShowDiagnostics::Off {
624 self.project
625 .read(cx)
626 .diagnostic_summaries(false, cx)
627 .filter_map(|(path, _, diagnostic_summary)| {
628 if diagnostic_summary.error_count > 0 {
629 Some((path, DiagnosticSeverity::ERROR))
630 } else if show_diagnostics_setting == ShowDiagnostics::All
631 && diagnostic_summary.warning_count > 0
632 {
633 Some((path, DiagnosticSeverity::WARNING))
634 } else {
635 None
636 }
637 })
638 .for_each(|(project_path, diagnostic_severity)| {
639 let mut path_buffer = PathBuf::new();
640 Self::update_strongest_diagnostic_severity(
641 &mut diagnostics,
642 &project_path,
643 path_buffer.clone(),
644 diagnostic_severity,
645 );
646
647 for component in project_path.path.components() {
648 path_buffer.push(component);
649 Self::update_strongest_diagnostic_severity(
650 &mut diagnostics,
651 &project_path,
652 path_buffer.clone(),
653 diagnostic_severity,
654 );
655 }
656 });
657 }
658 self.diagnostics = diagnostics;
659 }
660
661 fn update_strongest_diagnostic_severity(
662 diagnostics: &mut HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
663 project_path: &ProjectPath,
664 path_buffer: PathBuf,
665 diagnostic_severity: DiagnosticSeverity,
666 ) {
667 diagnostics
668 .entry((project_path.worktree_id, path_buffer.clone()))
669 .and_modify(|strongest_diagnostic_severity| {
670 *strongest_diagnostic_severity =
671 cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
672 })
673 .or_insert(diagnostic_severity);
674 }
675
676 fn serialize(&mut self, cx: &mut Context<Self>) {
677 let width = self.width;
678 self.pending_serialization = cx.background_spawn(
679 async move {
680 KEY_VALUE_STORE
681 .write_kvp(
682 PROJECT_PANEL_KEY.into(),
683 serde_json::to_string(&SerializedProjectPanel { width })?,
684 )
685 .await?;
686 anyhow::Ok(())
687 }
688 .log_err(),
689 );
690 }
691
692 fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
693 if !self.focus_handle.contains_focused(window, cx) {
694 cx.emit(Event::Focus);
695 }
696 }
697
698 fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
699 if !self.focus_handle.is_focused(window) {
700 self.confirm(&Confirm, window, cx);
701 }
702 }
703
704 fn deploy_context_menu(
705 &mut self,
706 position: Point<Pixels>,
707 entry_id: ProjectEntryId,
708 window: &mut Window,
709 cx: &mut Context<Self>,
710 ) {
711 let project = self.project.read(cx);
712
713 let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
714 id
715 } else {
716 return;
717 };
718
719 self.selection = Some(SelectedEntry {
720 worktree_id,
721 entry_id,
722 });
723
724 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
725 let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
726 let worktree = worktree.read(cx);
727 let is_root = Some(entry) == worktree.root_entry();
728 let is_dir = entry.is_dir();
729 let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
730 let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
731 let is_read_only = project.is_read_only(cx);
732 let is_remote = project.is_via_collab();
733 let is_local = project.is_local();
734
735 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
736 menu.context(self.focus_handle.clone()).map(|menu| {
737 if is_read_only {
738 menu.when(is_dir, |menu| {
739 menu.action("Search Inside", Box::new(NewSearchInDirectory))
740 })
741 } else {
742 menu.action("New File", Box::new(NewFile))
743 .action("New Folder", Box::new(NewDirectory))
744 .separator()
745 .when(is_local && cfg!(target_os = "macos"), |menu| {
746 menu.action("Reveal in Finder", Box::new(RevealInFileManager))
747 })
748 .when(is_local && cfg!(not(target_os = "macos")), |menu| {
749 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
750 })
751 .when(is_local, |menu| {
752 menu.action("Open in Default App", Box::new(OpenWithSystem))
753 })
754 .action("Open in Terminal", Box::new(OpenInTerminal))
755 .when(is_dir, |menu| {
756 menu.separator()
757 .action("Find in Folder…", Box::new(NewSearchInDirectory))
758 })
759 .when(is_unfoldable, |menu| {
760 menu.action("Unfold Directory", Box::new(UnfoldDirectory))
761 })
762 .when(is_foldable, |menu| {
763 menu.action("Fold Directory", Box::new(FoldDirectory))
764 })
765 .separator()
766 .action("Cut", Box::new(Cut))
767 .action("Copy", Box::new(Copy))
768 .action("Duplicate", Box::new(Duplicate))
769 // TODO: Paste should always be visible, cbut disabled when clipboard is empty
770 .map(|menu| {
771 if self.clipboard.as_ref().is_some() {
772 menu.action("Paste", Box::new(Paste))
773 } else {
774 menu.disabled_action("Paste", Box::new(Paste))
775 }
776 })
777 .separator()
778 .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
779 .action(
780 "Copy Relative Path",
781 Box::new(zed_actions::workspace::CopyRelativePath),
782 )
783 .separator()
784 .when(!is_root || !cfg!(target_os = "windows"), |menu| {
785 menu.action("Rename", Box::new(Rename))
786 })
787 .when(!is_root & !is_remote, |menu| {
788 menu.action("Trash", Box::new(Trash { skip_prompt: false }))
789 })
790 .when(!is_root, |menu| {
791 menu.action("Delete", Box::new(Delete { skip_prompt: false }))
792 })
793 .when(!is_remote & is_root, |menu| {
794 menu.separator()
795 .action(
796 "Add Folder to Project…",
797 Box::new(workspace::AddFolderToProject),
798 )
799 .action("Remove from Project", Box::new(RemoveFromProject))
800 })
801 .when(is_root, |menu| {
802 menu.separator()
803 .action("Collapse All", Box::new(CollapseAllEntries))
804 })
805 }
806 })
807 });
808
809 window.focus(&context_menu.focus_handle(cx));
810 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
811 this.context_menu.take();
812 cx.notify();
813 });
814 self.context_menu = Some((context_menu, position, subscription));
815 }
816
817 cx.notify();
818 }
819
820 fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
821 if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
822 return false;
823 }
824
825 if let Some(parent_path) = entry.path.parent() {
826 let snapshot = worktree.snapshot();
827 let mut child_entries = snapshot.child_entries(parent_path);
828 if let Some(child) = child_entries.next() {
829 if child_entries.next().is_none() {
830 return child.kind.is_dir();
831 }
832 }
833 };
834 false
835 }
836
837 fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
838 if entry.is_dir() {
839 let snapshot = worktree.snapshot();
840
841 let mut child_entries = snapshot.child_entries(&entry.path);
842 if let Some(child) = child_entries.next() {
843 if child_entries.next().is_none() {
844 return child.kind.is_dir();
845 }
846 }
847 }
848 false
849 }
850
851 fn expand_selected_entry(
852 &mut self,
853 _: &ExpandSelectedEntry,
854 window: &mut Window,
855 cx: &mut Context<Self>,
856 ) {
857 if let Some((worktree, entry)) = self.selected_entry(cx) {
858 if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
859 if folded_ancestors.current_ancestor_depth > 0 {
860 folded_ancestors.current_ancestor_depth -= 1;
861 cx.notify();
862 return;
863 }
864 }
865 if entry.is_dir() {
866 let worktree_id = worktree.id();
867 let entry_id = entry.id;
868 let expanded_dir_ids =
869 if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
870 expanded_dir_ids
871 } else {
872 return;
873 };
874
875 match expanded_dir_ids.binary_search(&entry_id) {
876 Ok(_) => self.select_next(&SelectNext, window, cx),
877 Err(ix) => {
878 self.project.update(cx, |project, cx| {
879 project.expand_entry(worktree_id, entry_id, cx);
880 });
881
882 expanded_dir_ids.insert(ix, entry_id);
883 self.update_visible_entries(None, cx);
884 cx.notify();
885 }
886 }
887 }
888 }
889 }
890
891 fn collapse_selected_entry(
892 &mut self,
893 _: &CollapseSelectedEntry,
894 _: &mut Window,
895 cx: &mut Context<Self>,
896 ) {
897 let Some((worktree, entry)) = self.selected_entry_handle(cx) else {
898 return;
899 };
900 self.collapse_entry(entry.clone(), worktree, cx)
901 }
902
903 fn collapse_entry(&mut self, entry: Entry, worktree: Entity<Worktree>, cx: &mut Context<Self>) {
904 let worktree = worktree.read(cx);
905 if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) {
906 if folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() {
907 folded_ancestors.current_ancestor_depth += 1;
908 cx.notify();
909 return;
910 }
911 }
912 let worktree_id = worktree.id();
913 let expanded_dir_ids =
914 if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
915 expanded_dir_ids
916 } else {
917 return;
918 };
919
920 let mut entry = &entry;
921 loop {
922 let entry_id = entry.id;
923 match expanded_dir_ids.binary_search(&entry_id) {
924 Ok(ix) => {
925 expanded_dir_ids.remove(ix);
926 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
927 cx.notify();
928 break;
929 }
930 Err(_) => {
931 if let Some(parent_entry) =
932 entry.path.parent().and_then(|p| worktree.entry_for_path(p))
933 {
934 entry = parent_entry;
935 } else {
936 break;
937 }
938 }
939 }
940 }
941 }
942
943 pub fn collapse_all_entries(
944 &mut self,
945 _: &CollapseAllEntries,
946 _: &mut Window,
947 cx: &mut Context<Self>,
948 ) {
949 // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries
950 // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids).
951 self.expanded_dir_ids
952 .retain(|_, expanded_entries| expanded_entries.is_empty());
953 self.update_visible_entries(None, cx);
954 cx.notify();
955 }
956
957 fn toggle_expanded(
958 &mut self,
959 entry_id: ProjectEntryId,
960 window: &mut Window,
961 cx: &mut Context<Self>,
962 ) {
963 if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
964 if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
965 self.project.update(cx, |project, cx| {
966 match expanded_dir_ids.binary_search(&entry_id) {
967 Ok(ix) => {
968 expanded_dir_ids.remove(ix);
969 }
970 Err(ix) => {
971 project.expand_entry(worktree_id, entry_id, cx);
972 expanded_dir_ids.insert(ix, entry_id);
973 }
974 }
975 });
976 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
977 window.focus(&self.focus_handle);
978 cx.notify();
979 }
980 }
981 }
982
983 fn toggle_expand_all(
984 &mut self,
985 entry_id: ProjectEntryId,
986 window: &mut Window,
987 cx: &mut Context<Self>,
988 ) {
989 if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
990 if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
991 match expanded_dir_ids.binary_search(&entry_id) {
992 Ok(_ix) => {
993 self.collapse_all_for_entry(worktree_id, entry_id, cx);
994 }
995 Err(_ix) => {
996 self.expand_all_for_entry(worktree_id, entry_id, cx);
997 }
998 }
999 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
1000 window.focus(&self.focus_handle);
1001 cx.notify();
1002 }
1003 }
1004 }
1005
1006 fn expand_all_for_entry(
1007 &mut self,
1008 worktree_id: WorktreeId,
1009 entry_id: ProjectEntryId,
1010 cx: &mut Context<Self>,
1011 ) {
1012 self.project.update(cx, |project, cx| {
1013 if let Some((worktree, expanded_dir_ids)) = project
1014 .worktree_for_id(worktree_id, cx)
1015 .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1016 {
1017 if let Some(task) = project.expand_all_for_entry(worktree_id, entry_id, cx) {
1018 task.detach();
1019 }
1020
1021 let worktree = worktree.read(cx);
1022
1023 if let Some(mut entry) = worktree.entry_for_id(entry_id) {
1024 loop {
1025 if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1026 expanded_dir_ids.insert(ix, entry.id);
1027 }
1028
1029 if let Some(parent_entry) =
1030 entry.path.parent().and_then(|p| worktree.entry_for_path(p))
1031 {
1032 entry = parent_entry;
1033 } else {
1034 break;
1035 }
1036 }
1037 }
1038 }
1039 });
1040 }
1041
1042 fn collapse_all_for_entry(
1043 &mut self,
1044 worktree_id: WorktreeId,
1045 entry_id: ProjectEntryId,
1046 cx: &mut Context<Self>,
1047 ) {
1048 self.project.update(cx, |project, cx| {
1049 if let Some((worktree, expanded_dir_ids)) = project
1050 .worktree_for_id(worktree_id, cx)
1051 .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1052 {
1053 let worktree = worktree.read(cx);
1054 let mut dirs_to_collapse = vec![entry_id];
1055 let auto_fold_enabled = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
1056 while let Some(current_id) = dirs_to_collapse.pop() {
1057 let Some(current_entry) = worktree.entry_for_id(current_id) else {
1058 continue;
1059 };
1060 if let Ok(ix) = expanded_dir_ids.binary_search(¤t_id) {
1061 expanded_dir_ids.remove(ix);
1062 }
1063 if auto_fold_enabled {
1064 self.unfolded_dir_ids.remove(¤t_id);
1065 }
1066 for child in worktree.child_entries(¤t_entry.path) {
1067 if child.is_dir() {
1068 dirs_to_collapse.push(child.id);
1069 }
1070 }
1071 }
1072 }
1073 });
1074 }
1075
1076 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1077 if let Some(edit_state) = &self.edit_state {
1078 if edit_state.processing_filename.is_none() {
1079 self.filename_editor.update(cx, |editor, cx| {
1080 editor.move_to_beginning_of_line(
1081 &editor::actions::MoveToBeginningOfLine {
1082 stop_at_soft_wraps: false,
1083 stop_at_indent: false,
1084 },
1085 window,
1086 cx,
1087 );
1088 });
1089 return;
1090 }
1091 }
1092 if let Some(selection) = self.selection {
1093 let (mut worktree_ix, mut entry_ix, _) =
1094 self.index_for_selection(selection).unwrap_or_default();
1095 if entry_ix > 0 {
1096 entry_ix -= 1;
1097 } else if worktree_ix > 0 {
1098 worktree_ix -= 1;
1099 entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
1100 } else {
1101 return;
1102 }
1103
1104 let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix];
1105 let selection = SelectedEntry {
1106 worktree_id: *worktree_id,
1107 entry_id: worktree_entries[entry_ix].id,
1108 };
1109 self.selection = Some(selection);
1110 if window.modifiers().shift {
1111 self.marked_entries.insert(selection);
1112 }
1113 self.autoscroll(cx);
1114 cx.notify();
1115 } else {
1116 self.select_first(&SelectFirst {}, window, cx);
1117 }
1118 }
1119
1120 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
1121 if let Some(task) = self.confirm_edit(window, cx) {
1122 task.detach_and_notify_err(window, cx);
1123 }
1124 }
1125
1126 fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
1127 let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
1128 self.open_internal(true, !preview_tabs_enabled, window, cx);
1129 }
1130
1131 fn open_permanent(&mut self, _: &OpenPermanent, window: &mut Window, cx: &mut Context<Self>) {
1132 self.open_internal(false, true, window, cx);
1133 }
1134
1135 fn open_internal(
1136 &mut self,
1137 allow_preview: bool,
1138 focus_opened_item: bool,
1139 window: &mut Window,
1140 cx: &mut Context<Self>,
1141 ) {
1142 if let Some((_, entry)) = self.selected_entry(cx) {
1143 if entry.is_file() {
1144 self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
1145 cx.notify();
1146 } else {
1147 self.toggle_expanded(entry.id, window, cx);
1148 }
1149 }
1150 }
1151
1152 fn populate_validation_error(&mut self, cx: &mut Context<Self>) {
1153 let edit_state = match self.edit_state.as_mut() {
1154 Some(state) => state,
1155 None => return,
1156 };
1157 let filename = self.filename_editor.read(cx).text(cx);
1158 if !filename.is_empty() {
1159 if let Some(worktree) = self
1160 .project
1161 .read(cx)
1162 .worktree_for_id(edit_state.worktree_id, cx)
1163 {
1164 if let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) {
1165 let mut already_exists = false;
1166 if edit_state.is_new_entry() {
1167 let new_path = entry.path.join(filename.trim_start_matches('/'));
1168 if worktree
1169 .read(cx)
1170 .entry_for_path(new_path.as_path())
1171 .is_some()
1172 {
1173 already_exists = true;
1174 }
1175 } else {
1176 let new_path = if let Some(parent) = entry.path.clone().parent() {
1177 parent.join(&filename)
1178 } else {
1179 filename.clone().into()
1180 };
1181 if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path())
1182 {
1183 if existing.id != entry.id {
1184 already_exists = true;
1185 }
1186 }
1187 };
1188 if already_exists {
1189 edit_state.validation_state = ValidationState::Error(format!(
1190 "File or directory '{}' already exists at location. Please choose a different name.",
1191 filename
1192 ));
1193 cx.notify();
1194 return;
1195 }
1196 }
1197 }
1198 let trimmed_filename = filename.trim();
1199 if trimmed_filename.is_empty() {
1200 edit_state.validation_state =
1201 ValidationState::Error("File or directory name cannot be empty.".to_string());
1202 cx.notify();
1203 return;
1204 }
1205 if trimmed_filename != filename {
1206 edit_state.validation_state = ValidationState::Warning(
1207 "File or directory name contains leading or trailing whitespace.".to_string(),
1208 );
1209 cx.notify();
1210 return;
1211 }
1212 }
1213 edit_state.validation_state = ValidationState::None;
1214 cx.notify();
1215 }
1216
1217 fn confirm_edit(
1218 &mut self,
1219 window: &mut Window,
1220 cx: &mut Context<Self>,
1221 ) -> Option<Task<Result<()>>> {
1222 let edit_state = self.edit_state.as_mut()?;
1223 let worktree_id = edit_state.worktree_id;
1224 let is_new_entry = edit_state.is_new_entry();
1225 let filename = self.filename_editor.read(cx).text(cx);
1226 if filename.trim().is_empty() {
1227 return None;
1228 }
1229 #[cfg(not(target_os = "windows"))]
1230 let filename_indicates_dir = filename.ends_with("/");
1231 // On Windows, path separator could be either `/` or `\`.
1232 #[cfg(target_os = "windows")]
1233 let filename_indicates_dir = filename.ends_with("/") || filename.ends_with("\\");
1234 edit_state.is_dir =
1235 edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir);
1236 let is_dir = edit_state.is_dir;
1237 let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
1238 let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
1239
1240 let edit_task;
1241 let edited_entry_id;
1242 if is_new_entry {
1243 self.selection = Some(SelectedEntry {
1244 worktree_id,
1245 entry_id: NEW_ENTRY_ID,
1246 });
1247 let new_path = entry.path.join(filename.trim_start_matches('/'));
1248 if worktree
1249 .read(cx)
1250 .entry_for_path(new_path.as_path())
1251 .is_some()
1252 {
1253 return None;
1254 }
1255
1256 edited_entry_id = NEW_ENTRY_ID;
1257 edit_task = self.project.update(cx, |project, cx| {
1258 project.create_entry((worktree_id, &new_path), is_dir, cx)
1259 });
1260 } else {
1261 let new_path = if let Some(parent) = entry.path.clone().parent() {
1262 parent.join(&filename)
1263 } else {
1264 filename.clone().into()
1265 };
1266 if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) {
1267 if existing.id == entry.id {
1268 window.focus(&self.focus_handle);
1269 }
1270 return None;
1271 }
1272 edited_entry_id = entry.id;
1273 edit_task = self.project.update(cx, |project, cx| {
1274 project.rename_entry(entry.id, new_path.as_path(), cx)
1275 });
1276 };
1277
1278 window.focus(&self.focus_handle);
1279 edit_state.processing_filename = Some(filename);
1280 cx.notify();
1281
1282 Some(cx.spawn_in(window, async move |project_panel, cx| {
1283 let new_entry = edit_task.await;
1284 project_panel.update(cx, |project_panel, cx| {
1285 project_panel.edit_state = None;
1286 cx.notify();
1287 })?;
1288
1289 match new_entry {
1290 Err(e) => {
1291 project_panel.update( cx, |project_panel, cx| {
1292 project_panel.marked_entries.clear();
1293 project_panel.update_visible_entries(None, cx);
1294 }).ok();
1295 Err(e)?;
1296 }
1297 Ok(CreatedEntry::Included(new_entry)) => {
1298 project_panel.update( cx, |project_panel, cx| {
1299 if let Some(selection) = &mut project_panel.selection {
1300 if selection.entry_id == edited_entry_id {
1301 selection.worktree_id = worktree_id;
1302 selection.entry_id = new_entry.id;
1303 project_panel.marked_entries.clear();
1304 project_panel.expand_to_selection(cx);
1305 }
1306 }
1307 project_panel.update_visible_entries(None, cx);
1308 if is_new_entry && !is_dir {
1309 project_panel.open_entry(new_entry.id, true, false, cx);
1310 }
1311 cx.notify();
1312 })?;
1313 }
1314 Ok(CreatedEntry::Excluded { abs_path }) => {
1315 if let Some(open_task) = project_panel
1316 .update_in( cx, |project_panel, window, cx| {
1317 project_panel.marked_entries.clear();
1318 project_panel.update_visible_entries(None, cx);
1319
1320 if is_dir {
1321 project_panel.project.update(cx, |_, cx| {
1322 cx.emit(project::Event::Toast {
1323 notification_id: "excluded-directory".into(),
1324 message: format!("Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel")
1325 })
1326 });
1327 None
1328 } else {
1329 project_panel
1330 .workspace
1331 .update(cx, |workspace, cx| {
1332 workspace.open_abs_path(abs_path, OpenOptions { visible: Some(OpenVisible::All), ..Default::default() }, window, cx)
1333 })
1334 .ok()
1335 }
1336 })
1337 .ok()
1338 .flatten()
1339 {
1340 let _ = open_task.await?;
1341 }
1342 }
1343 }
1344 Ok(())
1345 }))
1346 }
1347
1348 fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1349 let previous_edit_state = self.edit_state.take();
1350 self.update_visible_entries(None, cx);
1351 self.marked_entries.clear();
1352
1353 if let Some(previously_focused) =
1354 previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
1355 {
1356 self.selection = Some(previously_focused);
1357 self.autoscroll(cx);
1358 }
1359
1360 window.focus(&self.focus_handle);
1361 cx.notify();
1362 }
1363
1364 fn open_entry(
1365 &mut self,
1366 entry_id: ProjectEntryId,
1367 focus_opened_item: bool,
1368 allow_preview: bool,
1369
1370 cx: &mut Context<Self>,
1371 ) {
1372 cx.emit(Event::OpenedEntry {
1373 entry_id,
1374 focus_opened_item,
1375 allow_preview,
1376 });
1377 }
1378
1379 fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut Context<Self>) {
1380 cx.emit(Event::SplitEntry { entry_id });
1381 }
1382
1383 fn new_file(&mut self, _: &NewFile, window: &mut Window, cx: &mut Context<Self>) {
1384 self.add_entry(false, window, cx)
1385 }
1386
1387 fn new_directory(&mut self, _: &NewDirectory, window: &mut Window, cx: &mut Context<Self>) {
1388 self.add_entry(true, window, cx)
1389 }
1390
1391 fn add_entry(&mut self, is_dir: bool, window: &mut Window, cx: &mut Context<Self>) {
1392 let Some((worktree_id, entry_id)) = self
1393 .selection
1394 .map(|entry| (entry.worktree_id, entry.entry_id))
1395 .or_else(|| {
1396 let entry_id = self.last_worktree_root_id?;
1397 let worktree_id = self
1398 .project
1399 .read(cx)
1400 .worktree_for_entry(entry_id, cx)?
1401 .read(cx)
1402 .id();
1403
1404 self.selection = Some(SelectedEntry {
1405 worktree_id,
1406 entry_id,
1407 });
1408
1409 Some((worktree_id, entry_id))
1410 })
1411 else {
1412 return;
1413 };
1414
1415 let directory_id;
1416 let new_entry_id = self.resolve_entry(entry_id);
1417 if let Some((worktree, expanded_dir_ids)) = self
1418 .project
1419 .read(cx)
1420 .worktree_for_id(worktree_id, cx)
1421 .zip(self.expanded_dir_ids.get_mut(&worktree_id))
1422 {
1423 let worktree = worktree.read(cx);
1424 if let Some(mut entry) = worktree.entry_for_id(new_entry_id) {
1425 loop {
1426 if entry.is_dir() {
1427 if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
1428 expanded_dir_ids.insert(ix, entry.id);
1429 }
1430 directory_id = entry.id;
1431 break;
1432 } else {
1433 if let Some(parent_path) = entry.path.parent() {
1434 if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
1435 entry = parent_entry;
1436 continue;
1437 }
1438 }
1439 return;
1440 }
1441 }
1442 } else {
1443 return;
1444 };
1445 } else {
1446 return;
1447 };
1448
1449 self.marked_entries.clear();
1450 self.edit_state = Some(EditState {
1451 worktree_id,
1452 entry_id: directory_id,
1453 leaf_entry_id: None,
1454 is_dir,
1455 processing_filename: None,
1456 previously_focused: self.selection,
1457 depth: 0,
1458 validation_state: ValidationState::None,
1459 });
1460 self.filename_editor.update(cx, |editor, cx| {
1461 editor.clear(window, cx);
1462 window.focus(&editor.focus_handle(cx));
1463 });
1464 self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
1465 self.autoscroll(cx);
1466 cx.notify();
1467 }
1468
1469 fn unflatten_entry_id(&self, leaf_entry_id: ProjectEntryId) -> ProjectEntryId {
1470 if let Some(ancestors) = self.ancestors.get(&leaf_entry_id) {
1471 ancestors
1472 .ancestors
1473 .get(ancestors.current_ancestor_depth)
1474 .copied()
1475 .unwrap_or(leaf_entry_id)
1476 } else {
1477 leaf_entry_id
1478 }
1479 }
1480
1481 fn rename_impl(
1482 &mut self,
1483 selection: Option<Range<usize>>,
1484 window: &mut Window,
1485 cx: &mut Context<Self>,
1486 ) {
1487 if let Some(SelectedEntry {
1488 worktree_id,
1489 entry_id,
1490 }) = self.selection
1491 {
1492 if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
1493 let sub_entry_id = self.unflatten_entry_id(entry_id);
1494 if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
1495 #[cfg(target_os = "windows")]
1496 if Some(entry) == worktree.read(cx).root_entry() {
1497 return;
1498 }
1499 self.edit_state = Some(EditState {
1500 worktree_id,
1501 entry_id: sub_entry_id,
1502 leaf_entry_id: Some(entry_id),
1503 is_dir: entry.is_dir(),
1504 processing_filename: None,
1505 previously_focused: None,
1506 depth: 0,
1507 validation_state: ValidationState::None,
1508 });
1509 let file_name = entry
1510 .path
1511 .file_name()
1512 .map(|s| s.to_string_lossy())
1513 .unwrap_or_default()
1514 .to_string();
1515 let selection = selection.unwrap_or_else(|| {
1516 let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
1517 let selection_end =
1518 file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
1519 0..selection_end
1520 });
1521 self.filename_editor.update(cx, |editor, cx| {
1522 editor.set_text(file_name, window, cx);
1523 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1524 s.select_ranges([selection])
1525 });
1526 window.focus(&editor.focus_handle(cx));
1527 });
1528 self.update_visible_entries(None, cx);
1529 self.autoscroll(cx);
1530 cx.notify();
1531 }
1532 }
1533 }
1534 }
1535
1536 fn rename(&mut self, _: &Rename, window: &mut Window, cx: &mut Context<Self>) {
1537 self.rename_impl(None, window, cx);
1538 }
1539
1540 fn trash(&mut self, action: &Trash, window: &mut Window, cx: &mut Context<Self>) {
1541 self.remove(true, action.skip_prompt, window, cx);
1542 }
1543
1544 fn delete(&mut self, action: &Delete, window: &mut Window, cx: &mut Context<Self>) {
1545 self.remove(false, action.skip_prompt, window, cx);
1546 }
1547
1548 fn remove(
1549 &mut self,
1550 trash: bool,
1551 skip_prompt: bool,
1552 window: &mut Window,
1553 cx: &mut Context<ProjectPanel>,
1554 ) {
1555 maybe!({
1556 let items_to_delete = self.disjoint_entries(cx);
1557 if items_to_delete.is_empty() {
1558 return None;
1559 }
1560 let project = self.project.read(cx);
1561
1562 let mut dirty_buffers = 0;
1563 let file_paths = items_to_delete
1564 .iter()
1565 .filter_map(|selection| {
1566 let project_path = project.path_for_entry(selection.entry_id, cx)?;
1567 dirty_buffers +=
1568 project.dirty_buffers(cx).any(|path| path == project_path) as usize;
1569 Some((
1570 selection.entry_id,
1571 project_path
1572 .path
1573 .file_name()?
1574 .to_string_lossy()
1575 .into_owned(),
1576 ))
1577 })
1578 .collect::<Vec<_>>();
1579 if file_paths.is_empty() {
1580 return None;
1581 }
1582 let answer = if !skip_prompt {
1583 let operation = if trash { "Trash" } else { "Delete" };
1584 let prompt = match file_paths.first() {
1585 Some((_, path)) if file_paths.len() == 1 => {
1586 let unsaved_warning = if dirty_buffers > 0 {
1587 "\n\nIt has unsaved changes, which will be lost."
1588 } else {
1589 ""
1590 };
1591
1592 format!("{operation} {path}?{unsaved_warning}")
1593 }
1594 _ => {
1595 const CUTOFF_POINT: usize = 10;
1596 let names = if file_paths.len() > CUTOFF_POINT {
1597 let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
1598 let mut paths = file_paths
1599 .iter()
1600 .map(|(_, path)| path.clone())
1601 .take(CUTOFF_POINT)
1602 .collect::<Vec<_>>();
1603 paths.truncate(CUTOFF_POINT);
1604 if truncated_path_counts == 1 {
1605 paths.push(".. 1 file not shown".into());
1606 } else {
1607 paths.push(format!(".. {} files not shown", truncated_path_counts));
1608 }
1609 paths
1610 } else {
1611 file_paths.iter().map(|(_, path)| path.clone()).collect()
1612 };
1613 let unsaved_warning = if dirty_buffers == 0 {
1614 String::new()
1615 } else if dirty_buffers == 1 {
1616 "\n\n1 of these has unsaved changes, which will be lost.".to_string()
1617 } else {
1618 format!(
1619 "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
1620 )
1621 };
1622
1623 format!(
1624 "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1625 operation.to_lowercase(),
1626 file_paths.len(),
1627 names.join("\n")
1628 )
1629 }
1630 };
1631 Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
1632 } else {
1633 None
1634 };
1635 let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
1636 cx.spawn_in(window, async move |panel, cx| {
1637 if let Some(answer) = answer {
1638 if answer.await != Ok(0) {
1639 return anyhow::Ok(());
1640 }
1641 }
1642 for (entry_id, _) in file_paths {
1643 panel
1644 .update(cx, |panel, cx| {
1645 panel
1646 .project
1647 .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1648 .context("no such entry")
1649 })??
1650 .await?;
1651 }
1652 panel.update_in(cx, |panel, window, cx| {
1653 if let Some(next_selection) = next_selection {
1654 panel.selection = Some(next_selection);
1655 panel.autoscroll(cx);
1656 } else {
1657 panel.select_last(&SelectLast {}, window, cx);
1658 }
1659 })?;
1660 Ok(())
1661 })
1662 .detach_and_log_err(cx);
1663 Some(())
1664 });
1665 }
1666
1667 fn find_next_selection_after_deletion(
1668 &self,
1669 sanitized_entries: BTreeSet<SelectedEntry>,
1670 cx: &mut Context<Self>,
1671 ) -> Option<SelectedEntry> {
1672 if sanitized_entries.is_empty() {
1673 return None;
1674 }
1675 let project = self.project.read(cx);
1676 let (worktree_id, worktree) = sanitized_entries
1677 .iter()
1678 .map(|entry| entry.worktree_id)
1679 .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
1680 .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
1681 let git_store = project.git_store().read(cx);
1682
1683 let marked_entries_in_worktree = sanitized_entries
1684 .iter()
1685 .filter(|e| e.worktree_id == worktree_id)
1686 .collect::<HashSet<_>>();
1687 let latest_entry = marked_entries_in_worktree
1688 .iter()
1689 .max_by(|a, b| {
1690 match (
1691 worktree.entry_for_id(a.entry_id),
1692 worktree.entry_for_id(b.entry_id),
1693 ) {
1694 (Some(a), Some(b)) => {
1695 compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
1696 }
1697 _ => cmp::Ordering::Equal,
1698 }
1699 })
1700 .and_then(|e| worktree.entry_for_id(e.entry_id))?;
1701
1702 let parent_path = latest_entry.path.parent()?;
1703 let parent_entry = worktree.entry_for_path(parent_path)?;
1704
1705 // Remove all siblings that are being deleted except the last marked entry
1706 let repo_snapshots = git_store.repo_snapshots(cx);
1707 let worktree_snapshot = worktree.snapshot();
1708 let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
1709 let mut siblings: Vec<_> =
1710 ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
1711 .filter(|sibling| {
1712 (sibling.id == latest_entry.id)
1713 || (!marked_entries_in_worktree.contains(&&SelectedEntry {
1714 worktree_id,
1715 entry_id: sibling.id,
1716 }) && (!hide_gitignore || !sibling.is_ignored))
1717 })
1718 .map(|entry| entry.to_owned())
1719 .collect();
1720
1721 project::sort_worktree_entries(&mut siblings);
1722 let sibling_entry_index = siblings
1723 .iter()
1724 .position(|sibling| sibling.id == latest_entry.id)?;
1725
1726 if let Some(next_sibling) = sibling_entry_index
1727 .checked_add(1)
1728 .and_then(|i| siblings.get(i))
1729 {
1730 return Some(SelectedEntry {
1731 worktree_id,
1732 entry_id: next_sibling.id,
1733 });
1734 }
1735 if let Some(prev_sibling) = sibling_entry_index
1736 .checked_sub(1)
1737 .and_then(|i| siblings.get(i))
1738 {
1739 return Some(SelectedEntry {
1740 worktree_id,
1741 entry_id: prev_sibling.id,
1742 });
1743 }
1744 // No neighbour sibling found, fall back to parent
1745 Some(SelectedEntry {
1746 worktree_id,
1747 entry_id: parent_entry.id,
1748 })
1749 }
1750
1751 fn unfold_directory(&mut self, _: &UnfoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1752 if let Some((worktree, entry)) = self.selected_entry(cx) {
1753 self.unfolded_dir_ids.insert(entry.id);
1754
1755 let snapshot = worktree.snapshot();
1756 let mut parent_path = entry.path.parent();
1757 while let Some(path) = parent_path {
1758 if let Some(parent_entry) = worktree.entry_for_path(path) {
1759 let mut children_iter = snapshot.child_entries(path);
1760
1761 if children_iter.by_ref().take(2).count() > 1 {
1762 break;
1763 }
1764
1765 self.unfolded_dir_ids.insert(parent_entry.id);
1766 parent_path = path.parent();
1767 } else {
1768 break;
1769 }
1770 }
1771
1772 self.update_visible_entries(None, cx);
1773 self.autoscroll(cx);
1774 cx.notify();
1775 }
1776 }
1777
1778 fn fold_directory(&mut self, _: &FoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1779 if let Some((worktree, entry)) = self.selected_entry(cx) {
1780 self.unfolded_dir_ids.remove(&entry.id);
1781
1782 let snapshot = worktree.snapshot();
1783 let mut path = &*entry.path;
1784 loop {
1785 let mut child_entries_iter = snapshot.child_entries(path);
1786 if let Some(child) = child_entries_iter.next() {
1787 if child_entries_iter.next().is_none() && child.is_dir() {
1788 self.unfolded_dir_ids.remove(&child.id);
1789 path = &*child.path;
1790 } else {
1791 break;
1792 }
1793 } else {
1794 break;
1795 }
1796 }
1797
1798 self.update_visible_entries(None, cx);
1799 self.autoscroll(cx);
1800 cx.notify();
1801 }
1802 }
1803
1804 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1805 if let Some(edit_state) = &self.edit_state {
1806 if edit_state.processing_filename.is_none() {
1807 self.filename_editor.update(cx, |editor, cx| {
1808 editor.move_to_end_of_line(
1809 &editor::actions::MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 },
1812 window,
1813 cx,
1814 );
1815 });
1816 return;
1817 }
1818 }
1819 if let Some(selection) = self.selection {
1820 let (mut worktree_ix, mut entry_ix, _) =
1821 self.index_for_selection(selection).unwrap_or_default();
1822 if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1823 if entry_ix + 1 < worktree_entries.len() {
1824 entry_ix += 1;
1825 } else {
1826 worktree_ix += 1;
1827 entry_ix = 0;
1828 }
1829 }
1830
1831 if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1832 {
1833 if let Some(entry) = worktree_entries.get(entry_ix) {
1834 let selection = SelectedEntry {
1835 worktree_id: *worktree_id,
1836 entry_id: entry.id,
1837 };
1838 self.selection = Some(selection);
1839 if window.modifiers().shift {
1840 self.marked_entries.insert(selection);
1841 }
1842
1843 self.autoscroll(cx);
1844 cx.notify();
1845 }
1846 }
1847 } else {
1848 self.select_first(&SelectFirst {}, window, cx);
1849 }
1850 }
1851
1852 fn select_prev_diagnostic(
1853 &mut self,
1854 _: &SelectPrevDiagnostic,
1855 _: &mut Window,
1856 cx: &mut Context<Self>,
1857 ) {
1858 let selection = self.find_entry(
1859 self.selection.as_ref(),
1860 true,
1861 |entry, worktree_id| {
1862 (self.selection.is_none()
1863 || self.selection.is_some_and(|selection| {
1864 if selection.worktree_id == worktree_id {
1865 selection.entry_id != entry.id
1866 } else {
1867 true
1868 }
1869 }))
1870 && entry.is_file()
1871 && self
1872 .diagnostics
1873 .contains_key(&(worktree_id, entry.path.to_path_buf()))
1874 },
1875 cx,
1876 );
1877
1878 if let Some(selection) = selection {
1879 self.selection = Some(selection);
1880 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1881 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1882 self.autoscroll(cx);
1883 cx.notify();
1884 }
1885 }
1886
1887 fn select_next_diagnostic(
1888 &mut self,
1889 _: &SelectNextDiagnostic,
1890 _: &mut Window,
1891 cx: &mut Context<Self>,
1892 ) {
1893 let selection = self.find_entry(
1894 self.selection.as_ref(),
1895 false,
1896 |entry, worktree_id| {
1897 (self.selection.is_none()
1898 || self.selection.is_some_and(|selection| {
1899 if selection.worktree_id == worktree_id {
1900 selection.entry_id != entry.id
1901 } else {
1902 true
1903 }
1904 }))
1905 && entry.is_file()
1906 && self
1907 .diagnostics
1908 .contains_key(&(worktree_id, entry.path.to_path_buf()))
1909 },
1910 cx,
1911 );
1912
1913 if let Some(selection) = selection {
1914 self.selection = Some(selection);
1915 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1916 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1917 self.autoscroll(cx);
1918 cx.notify();
1919 }
1920 }
1921
1922 fn select_prev_git_entry(
1923 &mut self,
1924 _: &SelectPrevGitEntry,
1925 _: &mut Window,
1926 cx: &mut Context<Self>,
1927 ) {
1928 let selection = self.find_entry(
1929 self.selection.as_ref(),
1930 true,
1931 |entry, worktree_id| {
1932 (self.selection.is_none()
1933 || self.selection.is_some_and(|selection| {
1934 if selection.worktree_id == worktree_id {
1935 selection.entry_id != entry.id
1936 } else {
1937 true
1938 }
1939 }))
1940 && entry.is_file()
1941 && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1942 },
1943 cx,
1944 );
1945
1946 if let Some(selection) = selection {
1947 self.selection = Some(selection);
1948 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1949 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1950 self.autoscroll(cx);
1951 cx.notify();
1952 }
1953 }
1954
1955 fn select_prev_directory(
1956 &mut self,
1957 _: &SelectPrevDirectory,
1958 _: &mut Window,
1959 cx: &mut Context<Self>,
1960 ) {
1961 let selection = self.find_visible_entry(
1962 self.selection.as_ref(),
1963 true,
1964 |entry, worktree_id| {
1965 (self.selection.is_none()
1966 || self.selection.is_some_and(|selection| {
1967 if selection.worktree_id == worktree_id {
1968 selection.entry_id != entry.id
1969 } else {
1970 true
1971 }
1972 }))
1973 && entry.is_dir()
1974 },
1975 cx,
1976 );
1977
1978 if let Some(selection) = selection {
1979 self.selection = Some(selection);
1980 self.autoscroll(cx);
1981 cx.notify();
1982 }
1983 }
1984
1985 fn select_next_directory(
1986 &mut self,
1987 _: &SelectNextDirectory,
1988 _: &mut Window,
1989 cx: &mut Context<Self>,
1990 ) {
1991 let selection = self.find_visible_entry(
1992 self.selection.as_ref(),
1993 false,
1994 |entry, worktree_id| {
1995 (self.selection.is_none()
1996 || self.selection.is_some_and(|selection| {
1997 if selection.worktree_id == worktree_id {
1998 selection.entry_id != entry.id
1999 } else {
2000 true
2001 }
2002 }))
2003 && entry.is_dir()
2004 },
2005 cx,
2006 );
2007
2008 if let Some(selection) = selection {
2009 self.selection = Some(selection);
2010 self.autoscroll(cx);
2011 cx.notify();
2012 }
2013 }
2014
2015 fn select_next_git_entry(
2016 &mut self,
2017 _: &SelectNextGitEntry,
2018 _: &mut Window,
2019 cx: &mut Context<Self>,
2020 ) {
2021 let selection = self.find_entry(
2022 self.selection.as_ref(),
2023 false,
2024 |entry, worktree_id| {
2025 (self.selection.is_none()
2026 || self.selection.is_some_and(|selection| {
2027 if selection.worktree_id == worktree_id {
2028 selection.entry_id != entry.id
2029 } else {
2030 true
2031 }
2032 }))
2033 && entry.is_file()
2034 && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
2035 },
2036 cx,
2037 );
2038
2039 if let Some(selection) = selection {
2040 self.selection = Some(selection);
2041 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
2042 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
2043 self.autoscroll(cx);
2044 cx.notify();
2045 }
2046 }
2047
2048 fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
2049 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2050 if let Some(parent) = entry.path.parent() {
2051 let worktree = worktree.read(cx);
2052 if let Some(parent_entry) = worktree.entry_for_path(parent) {
2053 self.selection = Some(SelectedEntry {
2054 worktree_id: worktree.id(),
2055 entry_id: parent_entry.id,
2056 });
2057 self.autoscroll(cx);
2058 cx.notify();
2059 }
2060 }
2061 } else {
2062 self.select_first(&SelectFirst {}, window, cx);
2063 }
2064 }
2065
2066 fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
2067 let worktree = self
2068 .visible_entries
2069 .first()
2070 .and_then(|(worktree_id, _, _)| {
2071 self.project.read(cx).worktree_for_id(*worktree_id, cx)
2072 });
2073 if let Some(worktree) = worktree {
2074 let worktree = worktree.read(cx);
2075 let worktree_id = worktree.id();
2076 if let Some(root_entry) = worktree.root_entry() {
2077 let selection = SelectedEntry {
2078 worktree_id,
2079 entry_id: root_entry.id,
2080 };
2081 self.selection = Some(selection);
2082 if window.modifiers().shift {
2083 self.marked_entries.insert(selection);
2084 }
2085 self.autoscroll(cx);
2086 cx.notify();
2087 }
2088 }
2089 }
2090
2091 fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
2092 if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() {
2093 let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
2094 if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) {
2095 let worktree = worktree.read(cx);
2096 if let Some(entry) = worktree.entry_for_id(entry.id) {
2097 let selection = SelectedEntry {
2098 worktree_id: *worktree_id,
2099 entry_id: entry.id,
2100 };
2101 self.selection = Some(selection);
2102 self.autoscroll(cx);
2103 cx.notify();
2104 }
2105 }
2106 }
2107 }
2108
2109 fn autoscroll(&mut self, cx: &mut Context<Self>) {
2110 if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
2111 self.scroll_handle
2112 .scroll_to_item(index, ScrollStrategy::Center);
2113 cx.notify();
2114 }
2115 }
2116
2117 fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
2118 let entries = self.disjoint_entries(cx);
2119 if !entries.is_empty() {
2120 self.clipboard = Some(ClipboardEntry::Cut(entries));
2121 cx.notify();
2122 }
2123 }
2124
2125 fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2126 let entries = self.disjoint_entries(cx);
2127 if !entries.is_empty() {
2128 self.clipboard = Some(ClipboardEntry::Copied(entries));
2129 cx.notify();
2130 }
2131 }
2132
2133 fn create_paste_path(
2134 &self,
2135 source: &SelectedEntry,
2136 (worktree, target_entry): (Entity<Worktree>, &Entry),
2137 cx: &App,
2138 ) -> Option<(PathBuf, Option<Range<usize>>)> {
2139 let mut new_path = target_entry.path.to_path_buf();
2140 // If we're pasting into a file, or a directory into itself, go up one level.
2141 if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2142 new_path.pop();
2143 }
2144 let clipboard_entry_file_name = self
2145 .project
2146 .read(cx)
2147 .path_for_entry(source.entry_id, cx)?
2148 .path
2149 .file_name()?
2150 .to_os_string();
2151 new_path.push(&clipboard_entry_file_name);
2152 let extension = new_path.extension().map(|e| e.to_os_string());
2153 let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
2154 let file_name_len = file_name_without_extension.to_string_lossy().len();
2155 let mut disambiguation_range = None;
2156 let mut ix = 0;
2157 {
2158 let worktree = worktree.read(cx);
2159 while worktree.entry_for_path(&new_path).is_some() {
2160 new_path.pop();
2161
2162 let mut new_file_name = file_name_without_extension.to_os_string();
2163
2164 let disambiguation = " copy";
2165 let mut disambiguation_len = disambiguation.len();
2166
2167 new_file_name.push(disambiguation);
2168
2169 if ix > 0 {
2170 let extra_disambiguation = format!(" {}", ix);
2171 disambiguation_len += extra_disambiguation.len();
2172
2173 new_file_name.push(extra_disambiguation);
2174 }
2175 if let Some(extension) = extension.as_ref() {
2176 new_file_name.push(".");
2177 new_file_name.push(extension);
2178 }
2179
2180 new_path.push(new_file_name);
2181 disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2182 ix += 1;
2183 }
2184 }
2185 Some((new_path, disambiguation_range))
2186 }
2187
2188 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2189 maybe!({
2190 let (worktree, entry) = self.selected_entry_handle(cx)?;
2191 let entry = entry.clone();
2192 let worktree_id = worktree.read(cx).id();
2193 let clipboard_entries = self
2194 .clipboard
2195 .as_ref()
2196 .filter(|clipboard| !clipboard.items().is_empty())?;
2197 enum PasteTask {
2198 Rename(Task<Result<CreatedEntry>>),
2199 Copy(Task<Result<Option<Entry>>>),
2200 }
2201 let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
2202 IndexMap::default();
2203 let mut disambiguation_range = None;
2204 let clip_is_cut = clipboard_entries.is_cut();
2205 for clipboard_entry in clipboard_entries.items() {
2206 let (new_path, new_disambiguation_range) =
2207 self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2208 let clip_entry_id = clipboard_entry.entry_id;
2209 let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
2210 let relative_worktree_source_path = if !is_same_worktree {
2211 let target_base_path = worktree.read(cx).abs_path();
2212 let clipboard_project_path =
2213 self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
2214 let clipboard_abs_path = self
2215 .project
2216 .read(cx)
2217 .absolute_path(&clipboard_project_path, cx)?;
2218 Some(relativize_path(
2219 &target_base_path,
2220 clipboard_abs_path.as_path(),
2221 ))
2222 } else {
2223 None
2224 };
2225 let task = if clip_is_cut && is_same_worktree {
2226 let task = self.project.update(cx, |project, cx| {
2227 project.rename_entry(clip_entry_id, new_path, cx)
2228 });
2229 PasteTask::Rename(task)
2230 } else {
2231 let entry_id = if is_same_worktree {
2232 clip_entry_id
2233 } else {
2234 entry.id
2235 };
2236 let task = self.project.update(cx, |project, cx| {
2237 project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
2238 });
2239 PasteTask::Copy(task)
2240 };
2241 let needs_delete = !is_same_worktree && clip_is_cut;
2242 paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
2243 disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2244 }
2245
2246 let item_count = paste_entry_tasks.len();
2247
2248 cx.spawn_in(window, async move |project_panel, cx| {
2249 let mut last_succeed = None;
2250 let mut need_delete_ids = Vec::new();
2251 for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
2252 match task {
2253 PasteTask::Rename(task) => {
2254 if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
2255 last_succeed = Some(entry);
2256 }
2257 }
2258 PasteTask::Copy(task) => {
2259 if let Some(Some(entry)) = task.await.log_err() {
2260 last_succeed = Some(entry);
2261 if need_delete {
2262 need_delete_ids.push(entry_id);
2263 }
2264 }
2265 }
2266 }
2267 }
2268 // remove entry for cut in difference worktree
2269 for entry_id in need_delete_ids {
2270 project_panel
2271 .update(cx, |project_panel, cx| {
2272 project_panel
2273 .project
2274 .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
2275 .ok_or_else(|| anyhow!("no such entry"))
2276 })??
2277 .await?;
2278 }
2279 // update selection
2280 if let Some(entry) = last_succeed {
2281 project_panel
2282 .update_in(cx, |project_panel, window, cx| {
2283 project_panel.selection = Some(SelectedEntry {
2284 worktree_id,
2285 entry_id: entry.id,
2286 });
2287
2288 if item_count == 1 {
2289 // open entry if not dir, and only focus if rename is not pending
2290 if !entry.is_dir() {
2291 project_panel.open_entry(
2292 entry.id,
2293 disambiguation_range.is_none(),
2294 false,
2295 cx,
2296 );
2297 }
2298
2299 // if only one entry was pasted and it was disambiguated, open the rename editor
2300 if disambiguation_range.is_some() {
2301 cx.defer_in(window, |this, window, cx| {
2302 this.rename_impl(disambiguation_range, window, cx);
2303 });
2304 }
2305 }
2306 })
2307 .ok();
2308 }
2309
2310 anyhow::Ok(())
2311 })
2312 .detach_and_log_err(cx);
2313
2314 self.expand_entry(worktree_id, entry.id, cx);
2315 Some(())
2316 });
2317 }
2318
2319 fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2320 self.copy(&Copy {}, window, cx);
2321 self.paste(&Paste {}, window, cx);
2322 }
2323
2324 fn copy_path(
2325 &mut self,
2326 _: &zed_actions::workspace::CopyPath,
2327 _: &mut Window,
2328 cx: &mut Context<Self>,
2329 ) {
2330 let abs_file_paths = {
2331 let project = self.project.read(cx);
2332 self.effective_entries()
2333 .into_iter()
2334 .filter_map(|entry| {
2335 let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2336 Some(
2337 project
2338 .worktree_for_id(entry.worktree_id, cx)?
2339 .read(cx)
2340 .abs_path()
2341 .join(entry_path)
2342 .to_string_lossy()
2343 .to_string(),
2344 )
2345 })
2346 .collect::<Vec<_>>()
2347 };
2348 if !abs_file_paths.is_empty() {
2349 cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2350 }
2351 }
2352
2353 fn copy_relative_path(
2354 &mut self,
2355 _: &zed_actions::workspace::CopyRelativePath,
2356 _: &mut Window,
2357 cx: &mut Context<Self>,
2358 ) {
2359 let file_paths = {
2360 let project = self.project.read(cx);
2361 self.effective_entries()
2362 .into_iter()
2363 .filter_map(|entry| {
2364 Some(
2365 project
2366 .path_for_entry(entry.entry_id, cx)?
2367 .path
2368 .to_string_lossy()
2369 .to_string(),
2370 )
2371 })
2372 .collect::<Vec<_>>()
2373 };
2374 if !file_paths.is_empty() {
2375 cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2376 }
2377 }
2378
2379 fn reveal_in_finder(
2380 &mut self,
2381 _: &RevealInFileManager,
2382 _: &mut Window,
2383 cx: &mut Context<Self>,
2384 ) {
2385 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2386 cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2387 }
2388 }
2389
2390 fn remove_from_project(
2391 &mut self,
2392 _: &RemoveFromProject,
2393 _window: &mut Window,
2394 cx: &mut Context<Self>,
2395 ) {
2396 for entry in self.effective_entries().iter() {
2397 let worktree_id = entry.worktree_id;
2398 self.project
2399 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2400 }
2401 }
2402
2403 fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2404 if let Some((worktree, entry)) = self.selected_entry(cx) {
2405 let abs_path = worktree.abs_path().join(&entry.path);
2406 cx.open_with_system(&abs_path);
2407 }
2408 }
2409
2410 fn open_in_terminal(
2411 &mut self,
2412 _: &OpenInTerminal,
2413 window: &mut Window,
2414 cx: &mut Context<Self>,
2415 ) {
2416 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2417 let abs_path = match &entry.canonical_path {
2418 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2419 None => worktree.read(cx).absolutize(&entry.path).ok(),
2420 };
2421
2422 let working_directory = if entry.is_dir() {
2423 abs_path
2424 } else {
2425 abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2426 };
2427 if let Some(working_directory) = working_directory {
2428 window.dispatch_action(
2429 workspace::OpenTerminal { working_directory }.boxed_clone(),
2430 cx,
2431 )
2432 }
2433 }
2434 }
2435
2436 pub fn new_search_in_directory(
2437 &mut self,
2438 _: &NewSearchInDirectory,
2439 window: &mut Window,
2440 cx: &mut Context<Self>,
2441 ) {
2442 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2443 let dir_path = if entry.is_dir() {
2444 entry.path.clone()
2445 } else {
2446 // entry is a file, use its parent directory
2447 match entry.path.parent() {
2448 Some(parent) => Arc::from(parent),
2449 None => {
2450 // File at root, open search with empty filter
2451 self.workspace
2452 .update(cx, |workspace, cx| {
2453 search::ProjectSearchView::new_search_in_directory(
2454 workspace,
2455 Path::new(""),
2456 window,
2457 cx,
2458 );
2459 })
2460 .ok();
2461 return;
2462 }
2463 }
2464 };
2465
2466 let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2467 let dir_path = if include_root {
2468 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2469 full_path.push(&dir_path);
2470 Arc::from(full_path)
2471 } else {
2472 dir_path
2473 };
2474
2475 self.workspace
2476 .update(cx, |workspace, cx| {
2477 search::ProjectSearchView::new_search_in_directory(
2478 workspace, &dir_path, window, cx,
2479 );
2480 })
2481 .ok();
2482 }
2483 }
2484
2485 fn move_entry(
2486 &mut self,
2487 entry_to_move: ProjectEntryId,
2488 destination: ProjectEntryId,
2489 destination_is_file: bool,
2490 cx: &mut Context<Self>,
2491 ) {
2492 if self
2493 .project
2494 .read(cx)
2495 .entry_is_worktree_root(entry_to_move, cx)
2496 {
2497 self.move_worktree_root(entry_to_move, destination, cx)
2498 } else {
2499 self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2500 }
2501 }
2502
2503 fn move_worktree_root(
2504 &mut self,
2505 entry_to_move: ProjectEntryId,
2506 destination: ProjectEntryId,
2507 cx: &mut Context<Self>,
2508 ) {
2509 self.project.update(cx, |project, cx| {
2510 let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2511 return;
2512 };
2513 let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2514 return;
2515 };
2516
2517 let worktree_id = worktree_to_move.read(cx).id();
2518 let destination_id = destination_worktree.read(cx).id();
2519
2520 project
2521 .move_worktree(worktree_id, destination_id, cx)
2522 .log_err();
2523 });
2524 }
2525
2526 fn move_worktree_entry(
2527 &mut self,
2528 entry_to_move: ProjectEntryId,
2529 destination: ProjectEntryId,
2530 destination_is_file: bool,
2531 cx: &mut Context<Self>,
2532 ) {
2533 if entry_to_move == destination {
2534 return;
2535 }
2536
2537 let destination_worktree = self.project.update(cx, |project, cx| {
2538 let entry_path = project.path_for_entry(entry_to_move, cx)?;
2539 let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2540
2541 let mut destination_path = destination_entry_path.as_ref();
2542 if destination_is_file {
2543 destination_path = destination_path.parent()?;
2544 }
2545
2546 let mut new_path = destination_path.to_path_buf();
2547 new_path.push(entry_path.path.file_name()?);
2548 if new_path != entry_path.path.as_ref() {
2549 let task = project.rename_entry(entry_to_move, new_path, cx);
2550 cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2551 }
2552
2553 project.worktree_id_for_entry(destination, cx)
2554 });
2555
2556 if let Some(destination_worktree) = destination_worktree {
2557 self.expand_entry(destination_worktree, destination, cx);
2558 }
2559 }
2560
2561 fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2562 let mut entry_index = 0;
2563 let mut visible_entries_index = 0;
2564 for (worktree_index, (worktree_id, worktree_entries, _)) in
2565 self.visible_entries.iter().enumerate()
2566 {
2567 if *worktree_id == selection.worktree_id {
2568 for entry in worktree_entries {
2569 if entry.id == selection.entry_id {
2570 return Some((worktree_index, entry_index, visible_entries_index));
2571 } else {
2572 visible_entries_index += 1;
2573 entry_index += 1;
2574 }
2575 }
2576 break;
2577 } else {
2578 visible_entries_index += worktree_entries.len();
2579 }
2580 }
2581 None
2582 }
2583
2584 fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2585 let marked_entries = self.effective_entries();
2586 let mut sanitized_entries = BTreeSet::new();
2587 if marked_entries.is_empty() {
2588 return sanitized_entries;
2589 }
2590
2591 let project = self.project.read(cx);
2592 let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2593 .into_iter()
2594 .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2595 .fold(HashMap::default(), |mut map, entry| {
2596 map.entry(entry.worktree_id).or_default().push(entry);
2597 map
2598 });
2599
2600 for (worktree_id, marked_entries) in marked_entries_by_worktree {
2601 if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2602 let worktree = worktree.read(cx);
2603 let marked_dir_paths = marked_entries
2604 .iter()
2605 .filter_map(|entry| {
2606 worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2607 if entry.is_dir() {
2608 Some(entry.path.as_ref())
2609 } else {
2610 None
2611 }
2612 })
2613 })
2614 .collect::<BTreeSet<_>>();
2615
2616 sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2617 let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2618 return false;
2619 };
2620 let entry_path = entry_info.path.as_ref();
2621 let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2622 entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2623 });
2624 !inside_marked_dir
2625 }));
2626 }
2627 }
2628
2629 sanitized_entries
2630 }
2631
2632 fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2633 if let Some(selection) = self.selection {
2634 let selection = SelectedEntry {
2635 entry_id: self.resolve_entry(selection.entry_id),
2636 worktree_id: selection.worktree_id,
2637 };
2638
2639 // Default to using just the selected item when nothing is marked.
2640 if self.marked_entries.is_empty() {
2641 return BTreeSet::from([selection]);
2642 }
2643
2644 // Allow operating on the selected item even when something else is marked,
2645 // making it easier to perform one-off actions without clearing a mark.
2646 if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2647 return BTreeSet::from([selection]);
2648 }
2649 }
2650
2651 // Return only marked entries since we've already handled special cases where
2652 // only selection should take precedence. At this point, marked entries may or
2653 // may not include the current selection, which is intentional.
2654 self.marked_entries
2655 .iter()
2656 .map(|entry| SelectedEntry {
2657 entry_id: self.resolve_entry(entry.entry_id),
2658 worktree_id: entry.worktree_id,
2659 })
2660 .collect::<BTreeSet<_>>()
2661 }
2662
2663 /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2664 /// has no ancestors, the project entry ID that's passed in is returned as-is.
2665 fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2666 self.ancestors
2667 .get(&id)
2668 .and_then(|ancestors| {
2669 if ancestors.current_ancestor_depth == 0 {
2670 return None;
2671 }
2672 ancestors.ancestors.get(ancestors.current_ancestor_depth)
2673 })
2674 .copied()
2675 .unwrap_or(id)
2676 }
2677
2678 pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2679 let (worktree, entry) = self.selected_entry_handle(cx)?;
2680 Some((worktree.read(cx), entry))
2681 }
2682
2683 /// Compared to selected_entry, this function resolves to the currently
2684 /// selected subentry if dir auto-folding is enabled.
2685 fn selected_sub_entry<'a>(
2686 &self,
2687 cx: &'a App,
2688 ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2689 let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2690
2691 let resolved_id = self.resolve_entry(entry.id);
2692 if resolved_id != entry.id {
2693 let worktree = worktree.read(cx);
2694 entry = worktree.entry_for_id(resolved_id)?;
2695 }
2696 Some((worktree, entry))
2697 }
2698 fn selected_entry_handle<'a>(
2699 &self,
2700 cx: &'a App,
2701 ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2702 let selection = self.selection?;
2703 let project = self.project.read(cx);
2704 let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2705 let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2706 Some((worktree, entry))
2707 }
2708
2709 fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2710 let (worktree, entry) = self.selected_entry(cx)?;
2711 let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2712
2713 for path in entry.path.ancestors() {
2714 let Some(entry) = worktree.entry_for_path(path) else {
2715 continue;
2716 };
2717 if entry.is_dir() {
2718 if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2719 expanded_dir_ids.insert(idx, entry.id);
2720 }
2721 }
2722 }
2723
2724 Some(())
2725 }
2726
2727 fn update_visible_entries(
2728 &mut self,
2729 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2730 cx: &mut Context<Self>,
2731 ) {
2732 let settings = ProjectPanelSettings::get_global(cx);
2733 let auto_collapse_dirs = settings.auto_fold_dirs;
2734 let hide_gitignore = settings.hide_gitignore;
2735 let project = self.project.read(cx);
2736 let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2737 self.last_worktree_root_id = project
2738 .visible_worktrees(cx)
2739 .next_back()
2740 .and_then(|worktree| worktree.read(cx).root_entry())
2741 .map(|entry| entry.id);
2742
2743 let old_ancestors = std::mem::take(&mut self.ancestors);
2744 self.visible_entries.clear();
2745 let mut max_width_item = None;
2746 for worktree in project.visible_worktrees(cx) {
2747 let worktree_snapshot = worktree.read(cx).snapshot();
2748 let worktree_id = worktree_snapshot.id();
2749
2750 let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2751 hash_map::Entry::Occupied(e) => e.into_mut(),
2752 hash_map::Entry::Vacant(e) => {
2753 // The first time a worktree's root entry becomes available,
2754 // mark that root entry as expanded.
2755 if let Some(entry) = worktree_snapshot.root_entry() {
2756 e.insert(vec![entry.id]).as_slice()
2757 } else {
2758 &[]
2759 }
2760 }
2761 };
2762
2763 let mut new_entry_parent_id = None;
2764 let mut new_entry_kind = EntryKind::Dir;
2765 if let Some(edit_state) = &self.edit_state {
2766 if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2767 new_entry_parent_id = Some(edit_state.entry_id);
2768 new_entry_kind = if edit_state.is_dir {
2769 EntryKind::Dir
2770 } else {
2771 EntryKind::File
2772 };
2773 }
2774 }
2775
2776 let mut visible_worktree_entries = Vec::new();
2777 let mut entry_iter =
2778 GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2779 let mut auto_folded_ancestors = vec![];
2780 while let Some(entry) = entry_iter.entry() {
2781 if auto_collapse_dirs && entry.kind.is_dir() {
2782 auto_folded_ancestors.push(entry.id);
2783 if !self.unfolded_dir_ids.contains(&entry.id) {
2784 if let Some(root_path) = worktree_snapshot.root_entry() {
2785 let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2786 if let Some(child) = child_entries.next() {
2787 if entry.path != root_path.path
2788 && child_entries.next().is_none()
2789 && child.kind.is_dir()
2790 {
2791 entry_iter.advance();
2792
2793 continue;
2794 }
2795 }
2796 }
2797 }
2798 let depth = old_ancestors
2799 .get(&entry.id)
2800 .map(|ancestor| ancestor.current_ancestor_depth)
2801 .unwrap_or_default()
2802 .min(auto_folded_ancestors.len());
2803 if let Some(edit_state) = &mut self.edit_state {
2804 if edit_state.entry_id == entry.id {
2805 edit_state.depth = depth;
2806 }
2807 }
2808 let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2809 if ancestors.len() > 1 {
2810 ancestors.reverse();
2811 self.ancestors.insert(
2812 entry.id,
2813 FoldedAncestors {
2814 current_ancestor_depth: depth,
2815 ancestors,
2816 },
2817 );
2818 }
2819 }
2820 auto_folded_ancestors.clear();
2821 if !hide_gitignore || !entry.is_ignored {
2822 visible_worktree_entries.push(entry.to_owned());
2823 }
2824 let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2825 entry.id == new_entry_id || {
2826 self.ancestors.get(&entry.id).map_or(false, |entries| {
2827 entries
2828 .ancestors
2829 .iter()
2830 .any(|entry_id| *entry_id == new_entry_id)
2831 })
2832 }
2833 } else {
2834 false
2835 };
2836 if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2837 visible_worktree_entries.push(GitEntry {
2838 entry: Entry {
2839 id: NEW_ENTRY_ID,
2840 kind: new_entry_kind,
2841 path: entry.path.join("\0").into(),
2842 inode: 0,
2843 mtime: entry.mtime,
2844 size: entry.size,
2845 is_ignored: entry.is_ignored,
2846 is_external: false,
2847 is_private: false,
2848 is_always_included: entry.is_always_included,
2849 canonical_path: entry.canonical_path.clone(),
2850 char_bag: entry.char_bag,
2851 is_fifo: entry.is_fifo,
2852 },
2853 git_summary: entry.git_summary,
2854 });
2855 }
2856 let worktree_abs_path = worktree.read(cx).abs_path();
2857 let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2858 let Some(path_name) = worktree_abs_path
2859 .file_name()
2860 .with_context(|| {
2861 format!("Worktree abs path has no file name, root entry: {entry:?}")
2862 })
2863 .log_err()
2864 else {
2865 continue;
2866 };
2867 let path = ArcCow::Borrowed(Path::new(path_name));
2868 let depth = 0;
2869 (depth, path)
2870 } else if entry.is_file() {
2871 let Some(path_name) = entry
2872 .path
2873 .file_name()
2874 .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2875 .log_err()
2876 else {
2877 continue;
2878 };
2879 let path = ArcCow::Borrowed(Path::new(path_name));
2880 let depth = entry.path.ancestors().count() - 1;
2881 (depth, path)
2882 } else {
2883 let path = self
2884 .ancestors
2885 .get(&entry.id)
2886 .and_then(|ancestors| {
2887 let outermost_ancestor = ancestors.ancestors.last()?;
2888 let root_folded_entry = worktree
2889 .read(cx)
2890 .entry_for_id(*outermost_ancestor)?
2891 .path
2892 .as_ref();
2893 entry
2894 .path
2895 .strip_prefix(root_folded_entry)
2896 .ok()
2897 .and_then(|suffix| {
2898 let full_path = Path::new(root_folded_entry.file_name()?);
2899 Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2900 })
2901 })
2902 .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2903 .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2904 let depth = path.components().count();
2905 (depth, path)
2906 };
2907 let width_estimate = item_width_estimate(
2908 depth,
2909 path.to_string_lossy().chars().count(),
2910 entry.canonical_path.is_some(),
2911 );
2912
2913 match max_width_item.as_mut() {
2914 Some((id, worktree_id, width)) => {
2915 if *width < width_estimate {
2916 *id = entry.id;
2917 *worktree_id = worktree.read(cx).id();
2918 *width = width_estimate;
2919 }
2920 }
2921 None => {
2922 max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2923 }
2924 }
2925
2926 if expanded_dir_ids.binary_search(&entry.id).is_err()
2927 && entry_iter.advance_to_sibling()
2928 {
2929 continue;
2930 }
2931 entry_iter.advance();
2932 }
2933
2934 project::sort_worktree_entries(&mut visible_worktree_entries);
2935
2936 self.visible_entries
2937 .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2938 }
2939
2940 if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2941 let mut visited_worktrees_length = 0;
2942 let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2943 if worktree_id == *id {
2944 entries
2945 .iter()
2946 .position(|entry| entry.id == project_entry_id)
2947 } else {
2948 visited_worktrees_length += entries.len();
2949 None
2950 }
2951 });
2952 if let Some(index) = index {
2953 self.max_width_item_index = Some(visited_worktrees_length + index);
2954 }
2955 }
2956 if let Some((worktree_id, entry_id)) = new_selected_entry {
2957 self.selection = Some(SelectedEntry {
2958 worktree_id,
2959 entry_id,
2960 });
2961 }
2962 }
2963
2964 fn expand_entry(
2965 &mut self,
2966 worktree_id: WorktreeId,
2967 entry_id: ProjectEntryId,
2968 cx: &mut Context<Self>,
2969 ) {
2970 self.project.update(cx, |project, cx| {
2971 if let Some((worktree, expanded_dir_ids)) = project
2972 .worktree_for_id(worktree_id, cx)
2973 .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2974 {
2975 project.expand_entry(worktree_id, entry_id, cx);
2976 let worktree = worktree.read(cx);
2977
2978 if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2979 loop {
2980 if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2981 expanded_dir_ids.insert(ix, entry.id);
2982 }
2983
2984 if let Some(parent_entry) =
2985 entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2986 {
2987 entry = parent_entry;
2988 } else {
2989 break;
2990 }
2991 }
2992 }
2993 }
2994 });
2995 }
2996
2997 fn drop_external_files(
2998 &mut self,
2999 paths: &[PathBuf],
3000 entry_id: ProjectEntryId,
3001 window: &mut Window,
3002 cx: &mut Context<Self>,
3003 ) {
3004 let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
3005
3006 let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
3007
3008 let Some((target_directory, worktree, fs)) = maybe!({
3009 let project = self.project.read(cx);
3010 let fs = project.fs().clone();
3011 let worktree = project.worktree_for_entry(entry_id, cx)?;
3012 let entry = worktree.read(cx).entry_for_id(entry_id)?;
3013 let path = entry.path.clone();
3014 let target_directory = if entry.is_dir() {
3015 path.to_path_buf()
3016 } else {
3017 path.parent()?.to_path_buf()
3018 };
3019 Some((target_directory, worktree, fs))
3020 }) else {
3021 return;
3022 };
3023
3024 let mut paths_to_replace = Vec::new();
3025 for path in &paths {
3026 if let Some(name) = path.file_name() {
3027 let mut target_path = target_directory.clone();
3028 target_path.push(name);
3029 if target_path.exists() {
3030 paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
3031 }
3032 }
3033 }
3034
3035 cx.spawn_in(window, async move |this, cx| {
3036 async move {
3037 for (filename, original_path) in &paths_to_replace {
3038 let answer = cx.update(|window, cx| {
3039 window
3040 .prompt(
3041 PromptLevel::Info,
3042 format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
3043 None,
3044 &["Replace", "Cancel"],
3045 cx,
3046 )
3047 })?.await?;
3048
3049 if answer == 1 {
3050 if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
3051 paths.remove(item_idx);
3052 }
3053 }
3054 }
3055
3056 if paths.is_empty() {
3057 return Ok(());
3058 }
3059
3060 let task = worktree.update( cx, |worktree, cx| {
3061 worktree.copy_external_entries(target_directory.into(), paths, fs, cx)
3062 })?;
3063
3064 let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
3065 this.update(cx, |this, cx| {
3066 if open_file_after_drop && !opened_entries.is_empty() {
3067 this.open_entry(opened_entries[0], true, false, cx);
3068 }
3069 })
3070 }
3071 .log_err().await
3072 })
3073 .detach();
3074 }
3075
3076 fn drag_onto(
3077 &mut self,
3078 selections: &DraggedSelection,
3079 target_entry_id: ProjectEntryId,
3080 is_file: bool,
3081 window: &mut Window,
3082 cx: &mut Context<Self>,
3083 ) {
3084 let should_copy = window.modifiers().alt;
3085 if should_copy {
3086 let _ = maybe!({
3087 let project = self.project.read(cx);
3088 let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
3089 let worktree_id = target_worktree.read(cx).id();
3090 let target_entry = target_worktree
3091 .read(cx)
3092 .entry_for_id(target_entry_id)?
3093 .clone();
3094
3095 let mut copy_tasks = Vec::new();
3096 let mut disambiguation_range = None;
3097 for selection in selections.items() {
3098 let (new_path, new_disambiguation_range) = self.create_paste_path(
3099 selection,
3100 (target_worktree.clone(), &target_entry),
3101 cx,
3102 )?;
3103
3104 let task = self.project.update(cx, |project, cx| {
3105 project.copy_entry(selection.entry_id, None, new_path, cx)
3106 });
3107 copy_tasks.push(task);
3108 disambiguation_range = new_disambiguation_range.or(disambiguation_range);
3109 }
3110
3111 let item_count = copy_tasks.len();
3112
3113 cx.spawn_in(window, async move |project_panel, cx| {
3114 let mut last_succeed = None;
3115 for task in copy_tasks.into_iter() {
3116 if let Some(Some(entry)) = task.await.log_err() {
3117 last_succeed = Some(entry.id);
3118 }
3119 }
3120 // update selection
3121 if let Some(entry_id) = last_succeed {
3122 project_panel
3123 .update_in(cx, |project_panel, window, cx| {
3124 project_panel.selection = Some(SelectedEntry {
3125 worktree_id,
3126 entry_id,
3127 });
3128
3129 // if only one entry was dragged and it was disambiguated, open the rename editor
3130 if item_count == 1 && disambiguation_range.is_some() {
3131 project_panel.rename_impl(disambiguation_range, window, cx);
3132 }
3133 })
3134 .ok();
3135 }
3136 })
3137 .detach();
3138 Some(())
3139 });
3140 } else {
3141 for selection in selections.items() {
3142 self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3143 }
3144 }
3145 }
3146
3147 fn index_for_entry(
3148 &self,
3149 entry_id: ProjectEntryId,
3150 worktree_id: WorktreeId,
3151 ) -> Option<(usize, usize, usize)> {
3152 let mut worktree_ix = 0;
3153 let mut total_ix = 0;
3154 for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3155 if worktree_id != *current_worktree_id {
3156 total_ix += visible_worktree_entries.len();
3157 worktree_ix += 1;
3158 continue;
3159 }
3160
3161 return visible_worktree_entries
3162 .iter()
3163 .enumerate()
3164 .find(|(_, entry)| entry.id == entry_id)
3165 .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3166 }
3167 None
3168 }
3169
3170 fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3171 let mut offset = 0;
3172 for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3173 if visible_worktree_entries.len() > offset + index {
3174 return visible_worktree_entries
3175 .get(index)
3176 .map(|entry| (*worktree_id, entry.to_ref()));
3177 }
3178 offset += visible_worktree_entries.len();
3179 }
3180 None
3181 }
3182
3183 fn iter_visible_entries(
3184 &self,
3185 range: Range<usize>,
3186 window: &mut Window,
3187 cx: &mut Context<ProjectPanel>,
3188 mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3189 ) {
3190 let mut ix = 0;
3191 for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3192 if ix >= range.end {
3193 return;
3194 }
3195
3196 if ix + visible_worktree_entries.len() <= range.start {
3197 ix += visible_worktree_entries.len();
3198 continue;
3199 }
3200
3201 let end_ix = range.end.min(ix + visible_worktree_entries.len());
3202 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3203 let entries = entries_paths.get_or_init(|| {
3204 visible_worktree_entries
3205 .iter()
3206 .map(|e| (e.path.clone()))
3207 .collect()
3208 });
3209 for entry in visible_worktree_entries[entry_range].iter() {
3210 callback(&entry, entries, window, cx);
3211 }
3212 ix = end_ix;
3213 }
3214 }
3215
3216 fn for_each_visible_entry(
3217 &self,
3218 range: Range<usize>,
3219 window: &mut Window,
3220 cx: &mut Context<ProjectPanel>,
3221 mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3222 ) {
3223 let mut ix = 0;
3224 for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3225 if ix >= range.end {
3226 return;
3227 }
3228
3229 if ix + visible_worktree_entries.len() <= range.start {
3230 ix += visible_worktree_entries.len();
3231 continue;
3232 }
3233
3234 let end_ix = range.end.min(ix + visible_worktree_entries.len());
3235 let (git_status_setting, show_file_icons, show_folder_icons) = {
3236 let settings = ProjectPanelSettings::get_global(cx);
3237 (
3238 settings.git_status,
3239 settings.file_icons,
3240 settings.folder_icons,
3241 )
3242 };
3243 if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3244 let snapshot = worktree.read(cx).snapshot();
3245 let root_name = OsStr::new(snapshot.root_name());
3246 let expanded_entry_ids = self
3247 .expanded_dir_ids
3248 .get(&snapshot.id())
3249 .map(Vec::as_slice)
3250 .unwrap_or(&[]);
3251
3252 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3253 let entries = entries_paths.get_or_init(|| {
3254 visible_worktree_entries
3255 .iter()
3256 .map(|e| (e.path.clone()))
3257 .collect()
3258 });
3259 for entry in visible_worktree_entries[entry_range].iter() {
3260 let status = git_status_setting
3261 .then_some(entry.git_summary)
3262 .unwrap_or_default();
3263 let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3264 let icon = match entry.kind {
3265 EntryKind::File => {
3266 if show_file_icons {
3267 FileIcons::get_icon(&entry.path, cx)
3268 } else {
3269 None
3270 }
3271 }
3272 _ => {
3273 if show_folder_icons {
3274 FileIcons::get_folder_icon(is_expanded, cx)
3275 } else {
3276 FileIcons::get_chevron_icon(is_expanded, cx)
3277 }
3278 }
3279 };
3280
3281 let (depth, difference) =
3282 ProjectPanel::calculate_depth_and_difference(&entry, entries);
3283
3284 let filename = match difference {
3285 diff if diff > 1 => entry
3286 .path
3287 .iter()
3288 .skip(entry.path.components().count() - diff)
3289 .collect::<PathBuf>()
3290 .to_str()
3291 .unwrap_or_default()
3292 .to_string(),
3293 _ => entry
3294 .path
3295 .file_name()
3296 .map(|name| name.to_string_lossy().into_owned())
3297 .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3298 };
3299 let selection = SelectedEntry {
3300 worktree_id: snapshot.id(),
3301 entry_id: entry.id,
3302 };
3303
3304 let is_marked = self.marked_entries.contains(&selection);
3305
3306 let diagnostic_severity = self
3307 .diagnostics
3308 .get(&(*worktree_id, entry.path.to_path_buf()))
3309 .cloned();
3310
3311 let filename_text_color =
3312 entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3313
3314 let mut details = EntryDetails {
3315 filename,
3316 icon,
3317 path: entry.path.clone(),
3318 depth,
3319 kind: entry.kind,
3320 is_ignored: entry.is_ignored,
3321 is_expanded,
3322 is_selected: self.selection == Some(selection),
3323 is_marked,
3324 is_editing: false,
3325 is_processing: false,
3326 is_cut: self
3327 .clipboard
3328 .as_ref()
3329 .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3330 filename_text_color,
3331 diagnostic_severity,
3332 git_status: status,
3333 is_private: entry.is_private,
3334 worktree_id: *worktree_id,
3335 canonical_path: entry.canonical_path.clone(),
3336 };
3337
3338 if let Some(edit_state) = &self.edit_state {
3339 let is_edited_entry = if edit_state.is_new_entry() {
3340 entry.id == NEW_ENTRY_ID
3341 } else {
3342 entry.id == edit_state.entry_id
3343 || self
3344 .ancestors
3345 .get(&entry.id)
3346 .is_some_and(|auto_folded_dirs| {
3347 auto_folded_dirs
3348 .ancestors
3349 .iter()
3350 .any(|entry_id| *entry_id == edit_state.entry_id)
3351 })
3352 };
3353
3354 if is_edited_entry {
3355 if let Some(processing_filename) = &edit_state.processing_filename {
3356 details.is_processing = true;
3357 if let Some(ancestors) = edit_state
3358 .leaf_entry_id
3359 .and_then(|entry| self.ancestors.get(&entry))
3360 {
3361 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;
3362 let all_components = ancestors.ancestors.len();
3363
3364 let prefix_components = all_components - position;
3365 let suffix_components = position.checked_sub(1);
3366 let mut previous_components =
3367 Path::new(&details.filename).components();
3368 let mut new_path = previous_components
3369 .by_ref()
3370 .take(prefix_components)
3371 .collect::<PathBuf>();
3372 if let Some(last_component) =
3373 Path::new(processing_filename).components().next_back()
3374 {
3375 new_path.push(last_component);
3376 previous_components.next();
3377 }
3378
3379 if let Some(_) = suffix_components {
3380 new_path.push(previous_components);
3381 }
3382 if let Some(str) = new_path.to_str() {
3383 details.filename.clear();
3384 details.filename.push_str(str);
3385 }
3386 } else {
3387 details.filename.clear();
3388 details.filename.push_str(processing_filename);
3389 }
3390 } else {
3391 if edit_state.is_new_entry() {
3392 details.filename.clear();
3393 }
3394 details.is_editing = true;
3395 }
3396 }
3397 }
3398
3399 callback(entry.id, details, window, cx);
3400 }
3401 }
3402 ix = end_ix;
3403 }
3404 }
3405
3406 fn find_entry_in_worktree(
3407 &self,
3408 worktree_id: WorktreeId,
3409 reverse_search: bool,
3410 only_visible_entries: bool,
3411 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3412 cx: &mut Context<Self>,
3413 ) -> Option<GitEntry> {
3414 if only_visible_entries {
3415 let entries = self
3416 .visible_entries
3417 .iter()
3418 .find_map(|(tree_id, entries, _)| {
3419 if worktree_id == *tree_id {
3420 Some(entries)
3421 } else {
3422 None
3423 }
3424 })?
3425 .clone();
3426
3427 return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3428 .find(|ele| predicate(ele.to_ref(), worktree_id))
3429 .cloned();
3430 }
3431
3432 let repo_snapshots = self
3433 .project
3434 .read(cx)
3435 .git_store()
3436 .read(cx)
3437 .repo_snapshots(cx);
3438 let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3439 worktree.update(cx, |tree, _| {
3440 utils::ReversibleIterable::new(
3441 GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3442 reverse_search,
3443 )
3444 .find_single_ended(|ele| predicate(*ele, worktree_id))
3445 .map(|ele| ele.to_owned())
3446 })
3447 }
3448
3449 fn find_entry(
3450 &self,
3451 start: Option<&SelectedEntry>,
3452 reverse_search: bool,
3453 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3454 cx: &mut Context<Self>,
3455 ) -> Option<SelectedEntry> {
3456 let mut worktree_ids: Vec<_> = self
3457 .visible_entries
3458 .iter()
3459 .map(|(worktree_id, _, _)| *worktree_id)
3460 .collect();
3461 let repo_snapshots = self
3462 .project
3463 .read(cx)
3464 .git_store()
3465 .read(cx)
3466 .repo_snapshots(cx);
3467
3468 let mut last_found: Option<SelectedEntry> = None;
3469
3470 if let Some(start) = start {
3471 let worktree = self
3472 .project
3473 .read(cx)
3474 .worktree_for_id(start.worktree_id, cx)?;
3475
3476 let search = worktree.update(cx, |tree, _| {
3477 let entry = tree.entry_for_id(start.entry_id)?;
3478 let root_entry = tree.root_entry()?;
3479 let tree_id = tree.id();
3480
3481 let mut first_iter = GitTraversal::new(
3482 &repo_snapshots,
3483 tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3484 );
3485
3486 if reverse_search {
3487 first_iter.next();
3488 }
3489
3490 let first = first_iter
3491 .enumerate()
3492 .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3493 .map(|(_, entry)| entry)
3494 .find(|ele| predicate(*ele, tree_id))
3495 .map(|ele| ele.to_owned());
3496
3497 let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3498
3499 let second = if reverse_search {
3500 second_iter
3501 .take_until(|ele| ele.id == start.entry_id)
3502 .filter(|ele| predicate(*ele, tree_id))
3503 .last()
3504 .map(|ele| ele.to_owned())
3505 } else {
3506 second_iter
3507 .take_while(|ele| ele.id != start.entry_id)
3508 .filter(|ele| predicate(*ele, tree_id))
3509 .last()
3510 .map(|ele| ele.to_owned())
3511 };
3512
3513 if reverse_search {
3514 Some((second, first))
3515 } else {
3516 Some((first, second))
3517 }
3518 });
3519
3520 if let Some((first, second)) = search {
3521 let first = first.map(|entry| SelectedEntry {
3522 worktree_id: start.worktree_id,
3523 entry_id: entry.id,
3524 });
3525
3526 let second = second.map(|entry| SelectedEntry {
3527 worktree_id: start.worktree_id,
3528 entry_id: entry.id,
3529 });
3530
3531 if first.is_some() {
3532 return first;
3533 }
3534 last_found = second;
3535
3536 let idx = worktree_ids
3537 .iter()
3538 .enumerate()
3539 .find(|(_, ele)| **ele == start.worktree_id)
3540 .map(|(idx, _)| idx);
3541
3542 if let Some(idx) = idx {
3543 worktree_ids.rotate_left(idx + 1usize);
3544 worktree_ids.pop();
3545 }
3546 }
3547 }
3548
3549 for tree_id in worktree_ids.into_iter() {
3550 if let Some(found) =
3551 self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3552 {
3553 return Some(SelectedEntry {
3554 worktree_id: tree_id,
3555 entry_id: found.id,
3556 });
3557 }
3558 }
3559
3560 last_found
3561 }
3562
3563 fn find_visible_entry(
3564 &self,
3565 start: Option<&SelectedEntry>,
3566 reverse_search: bool,
3567 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3568 cx: &mut Context<Self>,
3569 ) -> Option<SelectedEntry> {
3570 let mut worktree_ids: Vec<_> = self
3571 .visible_entries
3572 .iter()
3573 .map(|(worktree_id, _, _)| *worktree_id)
3574 .collect();
3575
3576 let mut last_found: Option<SelectedEntry> = None;
3577
3578 if let Some(start) = start {
3579 let entries = self
3580 .visible_entries
3581 .iter()
3582 .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3583 .map(|(_, entries, _)| entries)?;
3584
3585 let mut start_idx = entries
3586 .iter()
3587 .enumerate()
3588 .find(|(_, ele)| ele.id == start.entry_id)
3589 .map(|(idx, _)| idx)?;
3590
3591 if reverse_search {
3592 start_idx = start_idx.saturating_add(1usize);
3593 }
3594
3595 let (left, right) = entries.split_at_checked(start_idx)?;
3596
3597 let (first_iter, second_iter) = if reverse_search {
3598 (
3599 utils::ReversibleIterable::new(left.iter(), reverse_search),
3600 utils::ReversibleIterable::new(right.iter(), reverse_search),
3601 )
3602 } else {
3603 (
3604 utils::ReversibleIterable::new(right.iter(), reverse_search),
3605 utils::ReversibleIterable::new(left.iter(), reverse_search),
3606 )
3607 };
3608
3609 let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3610 let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3611
3612 if first_search.is_some() {
3613 return first_search.map(|entry| SelectedEntry {
3614 worktree_id: start.worktree_id,
3615 entry_id: entry.id,
3616 });
3617 }
3618
3619 last_found = second_search.map(|entry| SelectedEntry {
3620 worktree_id: start.worktree_id,
3621 entry_id: entry.id,
3622 });
3623
3624 let idx = worktree_ids
3625 .iter()
3626 .enumerate()
3627 .find(|(_, ele)| **ele == start.worktree_id)
3628 .map(|(idx, _)| idx);
3629
3630 if let Some(idx) = idx {
3631 worktree_ids.rotate_left(idx + 1usize);
3632 worktree_ids.pop();
3633 }
3634 }
3635
3636 for tree_id in worktree_ids.into_iter() {
3637 if let Some(found) =
3638 self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3639 {
3640 return Some(SelectedEntry {
3641 worktree_id: tree_id,
3642 entry_id: found.id,
3643 });
3644 }
3645 }
3646
3647 last_found
3648 }
3649
3650 fn calculate_depth_and_difference(
3651 entry: &Entry,
3652 visible_worktree_entries: &HashSet<Arc<Path>>,
3653 ) -> (usize, usize) {
3654 let (depth, difference) = entry
3655 .path
3656 .ancestors()
3657 .skip(1) // Skip the entry itself
3658 .find_map(|ancestor| {
3659 if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3660 let entry_path_components_count = entry.path.components().count();
3661 let parent_path_components_count = parent_entry.components().count();
3662 let difference = entry_path_components_count - parent_path_components_count;
3663 let depth = parent_entry
3664 .ancestors()
3665 .skip(1)
3666 .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3667 .count();
3668 Some((depth + 1, difference))
3669 } else {
3670 None
3671 }
3672 })
3673 .unwrap_or((0, 0));
3674
3675 (depth, difference)
3676 }
3677
3678 fn render_entry(
3679 &self,
3680 entry_id: ProjectEntryId,
3681 details: EntryDetails,
3682 window: &mut Window,
3683 cx: &mut Context<Self>,
3684 ) -> Stateful<Div> {
3685 const GROUP_NAME: &str = "project_entry";
3686
3687 let kind = details.kind;
3688 let settings = ProjectPanelSettings::get_global(cx);
3689 let show_editor = details.is_editing && !details.is_processing;
3690
3691 let selection = SelectedEntry {
3692 worktree_id: details.worktree_id,
3693 entry_id,
3694 };
3695
3696 let is_marked = self.marked_entries.contains(&selection);
3697 let is_active = self
3698 .selection
3699 .map_or(false, |selection| selection.entry_id == entry_id);
3700
3701 let file_name = details.filename.clone();
3702
3703 let mut icon = details.icon.clone();
3704 if settings.file_icons && show_editor && details.kind.is_file() {
3705 let filename = self.filename_editor.read(cx).text(cx);
3706 if filename.len() > 2 {
3707 icon = FileIcons::get_icon(Path::new(&filename), cx);
3708 }
3709 }
3710
3711 let filename_text_color = details.filename_text_color;
3712 let diagnostic_severity = details.diagnostic_severity;
3713 let item_colors = get_item_color(cx);
3714
3715 let canonical_path = details
3716 .canonical_path
3717 .as_ref()
3718 .map(|f| f.to_string_lossy().to_string());
3719 let path = details.path.clone();
3720
3721 let depth = details.depth;
3722 let worktree_id = details.worktree_id;
3723 let selections = Arc::new(self.marked_entries.clone());
3724
3725 let dragged_selection = DraggedSelection {
3726 active_selection: selection,
3727 marked_selections: selections,
3728 };
3729
3730 let bg_color = if is_marked {
3731 item_colors.marked
3732 } else {
3733 item_colors.default
3734 };
3735
3736 let bg_hover_color = if is_marked {
3737 item_colors.marked
3738 } else {
3739 item_colors.hover
3740 };
3741
3742 let validation_color_and_message = if show_editor {
3743 match self
3744 .edit_state
3745 .as_ref()
3746 .map_or(ValidationState::None, |e| e.validation_state.clone())
3747 {
3748 ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
3749 ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
3750 ValidationState::None => None,
3751 }
3752 } else {
3753 None
3754 };
3755
3756 let border_color =
3757 if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3758 match validation_color_and_message {
3759 Some((color, _)) => color,
3760 None => item_colors.focused,
3761 }
3762 } else {
3763 bg_color
3764 };
3765
3766 let border_hover_color =
3767 if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3768 match validation_color_and_message {
3769 Some((color, _)) => color,
3770 None => item_colors.focused,
3771 }
3772 } else {
3773 bg_hover_color
3774 };
3775
3776 let folded_directory_drag_target = self.folded_directory_drag_target;
3777
3778 div()
3779 .id(entry_id.to_proto() as usize)
3780 .group(GROUP_NAME)
3781 .cursor_pointer()
3782 .rounded_none()
3783 .bg(bg_color)
3784 .border_1()
3785 .border_r_2()
3786 .border_color(border_color)
3787 .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3788 .on_drag_move::<ExternalPaths>(cx.listener(
3789 move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3790 if event.bounds.contains(&event.event.position) {
3791 if this.last_external_paths_drag_over_entry == Some(entry_id) {
3792 return;
3793 }
3794 this.last_external_paths_drag_over_entry = Some(entry_id);
3795 this.marked_entries.clear();
3796
3797 let Some((worktree, path, entry)) = maybe!({
3798 let worktree = this
3799 .project
3800 .read(cx)
3801 .worktree_for_id(selection.worktree_id, cx)?;
3802 let worktree = worktree.read(cx);
3803 let entry = worktree.entry_for_path(&path)?;
3804 let path = if entry.is_dir() {
3805 path.as_ref()
3806 } else {
3807 path.parent()?
3808 };
3809 Some((worktree, path, entry))
3810 }) else {
3811 return;
3812 };
3813
3814 this.marked_entries.insert(SelectedEntry {
3815 entry_id: entry.id,
3816 worktree_id: worktree.id(),
3817 });
3818
3819 for entry in worktree.child_entries(path) {
3820 this.marked_entries.insert(SelectedEntry {
3821 entry_id: entry.id,
3822 worktree_id: worktree.id(),
3823 });
3824 }
3825
3826 cx.notify();
3827 }
3828 },
3829 ))
3830 .on_drop(cx.listener(
3831 move |this, external_paths: &ExternalPaths, window, cx| {
3832 this.hover_scroll_task.take();
3833 this.last_external_paths_drag_over_entry = None;
3834 this.marked_entries.clear();
3835 this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3836 cx.stop_propagation();
3837 },
3838 ))
3839 .on_drag_move::<DraggedSelection>(cx.listener(
3840 move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3841 if event.bounds.contains(&event.event.position) {
3842 if this.last_selection_drag_over_entry == Some(entry_id) {
3843 return;
3844 }
3845 this.last_selection_drag_over_entry = Some(entry_id);
3846 this.hover_expand_task.take();
3847
3848 if !kind.is_dir()
3849 || this
3850 .expanded_dir_ids
3851 .get(&details.worktree_id)
3852 .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3853 {
3854 return;
3855 }
3856
3857 let bounds = event.bounds;
3858 this.hover_expand_task =
3859 Some(cx.spawn_in(window, async move |this, cx| {
3860 cx.background_executor()
3861 .timer(Duration::from_millis(500))
3862 .await;
3863 this.update_in(cx, |this, window, cx| {
3864 this.hover_expand_task.take();
3865 if this.last_selection_drag_over_entry == Some(entry_id)
3866 && bounds.contains(&window.mouse_position())
3867 {
3868 this.expand_entry(worktree_id, entry_id, cx);
3869 this.update_visible_entries(
3870 Some((worktree_id, entry_id)),
3871 cx,
3872 );
3873 cx.notify();
3874 }
3875 })
3876 .ok();
3877 }));
3878 }
3879 },
3880 ))
3881 .on_drag(
3882 dragged_selection,
3883 move |selection, click_offset, _window, cx| {
3884 cx.new(|_| DraggedProjectEntryView {
3885 details: details.clone(),
3886 click_offset,
3887 selection: selection.active_selection,
3888 selections: selection.marked_selections.clone(),
3889 })
3890 },
3891 )
3892 .drag_over::<DraggedSelection>(move |style, _, _, _| {
3893 if folded_directory_drag_target.is_some() {
3894 return style;
3895 }
3896 style.bg(item_colors.drag_over)
3897 })
3898 .on_drop(
3899 cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3900 this.hover_scroll_task.take();
3901 this.hover_expand_task.take();
3902 if folded_directory_drag_target.is_some() {
3903 return;
3904 }
3905 this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3906 }),
3907 )
3908 .on_mouse_down(
3909 MouseButton::Left,
3910 cx.listener(move |this, _, _, cx| {
3911 this.mouse_down = true;
3912 cx.propagate();
3913 }),
3914 )
3915 .on_click(
3916 cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3917 if event.down.button == MouseButton::Right
3918 || event.down.first_mouse
3919 || show_editor
3920 {
3921 return;
3922 }
3923 if event.down.button == MouseButton::Left {
3924 this.mouse_down = false;
3925 }
3926 cx.stop_propagation();
3927
3928 if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3929 let current_selection = this.index_for_selection(selection);
3930 let clicked_entry = SelectedEntry {
3931 entry_id,
3932 worktree_id,
3933 };
3934 let target_selection = this.index_for_selection(clicked_entry);
3935 if let Some(((_, _, source_index), (_, _, target_index))) =
3936 current_selection.zip(target_selection)
3937 {
3938 let range_start = source_index.min(target_index);
3939 let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3940 let mut new_selections = BTreeSet::new();
3941 this.for_each_visible_entry(
3942 range_start..range_end,
3943 window,
3944 cx,
3945 |entry_id, details, _, _| {
3946 new_selections.insert(SelectedEntry {
3947 entry_id,
3948 worktree_id: details.worktree_id,
3949 });
3950 },
3951 );
3952
3953 this.marked_entries = this
3954 .marked_entries
3955 .union(&new_selections)
3956 .cloned()
3957 .collect();
3958
3959 this.selection = Some(clicked_entry);
3960 this.marked_entries.insert(clicked_entry);
3961 }
3962 } else if event.modifiers().secondary() {
3963 if event.down.click_count > 1 {
3964 this.split_entry(entry_id, cx);
3965 } else {
3966 this.selection = Some(selection);
3967 if !this.marked_entries.insert(selection) {
3968 this.marked_entries.remove(&selection);
3969 }
3970 }
3971 } else if kind.is_dir() {
3972 this.marked_entries.clear();
3973 if event.modifiers().alt {
3974 this.toggle_expand_all(entry_id, window, cx);
3975 } else {
3976 this.toggle_expanded(entry_id, window, cx);
3977 }
3978 } else {
3979 let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3980 let click_count = event.up.click_count;
3981 let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3982 let allow_preview = preview_tabs_enabled && click_count == 1;
3983 this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3984 }
3985 }),
3986 )
3987 .child(
3988 ListItem::new(entry_id.to_proto() as usize)
3989 .indent_level(depth)
3990 .indent_step_size(px(settings.indent_size))
3991 .spacing(match settings.entry_spacing {
3992 project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3993 project_panel_settings::EntrySpacing::Standard => {
3994 ListItemSpacing::ExtraDense
3995 }
3996 })
3997 .selectable(false)
3998 .when_some(canonical_path, |this, path| {
3999 this.end_slot::<AnyElement>(
4000 div()
4001 .id("symlink_icon")
4002 .pr_3()
4003 .tooltip(move |window, cx| {
4004 Tooltip::with_meta(
4005 path.to_string(),
4006 None,
4007 "Symbolic Link",
4008 window,
4009 cx,
4010 )
4011 })
4012 .child(
4013 Icon::new(IconName::ArrowUpRight)
4014 .size(IconSize::Indicator)
4015 .color(filename_text_color),
4016 )
4017 .into_any_element(),
4018 )
4019 })
4020 .child(if let Some(icon) = &icon {
4021 if let Some((_, decoration_color)) =
4022 entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
4023 {
4024 let is_warning = diagnostic_severity
4025 .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
4026 .unwrap_or(false);
4027 div().child(
4028 DecoratedIcon::new(
4029 Icon::from_path(icon.clone()).color(Color::Muted),
4030 Some(
4031 IconDecoration::new(
4032 if kind.is_file() {
4033 if is_warning {
4034 IconDecorationKind::Triangle
4035 } else {
4036 IconDecorationKind::X
4037 }
4038 } else {
4039 IconDecorationKind::Dot
4040 },
4041 bg_color,
4042 cx,
4043 )
4044 .group_name(Some(GROUP_NAME.into()))
4045 .knockout_hover_color(bg_hover_color)
4046 .color(decoration_color.color(cx))
4047 .position(Point {
4048 x: px(-2.),
4049 y: px(-2.),
4050 }),
4051 ),
4052 )
4053 .into_any_element(),
4054 )
4055 } else {
4056 h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
4057 }
4058 } else {
4059 if let Some((icon_name, color)) =
4060 entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
4061 {
4062 h_flex()
4063 .size(IconSize::default().rems())
4064 .child(Icon::new(icon_name).color(color).size(IconSize::Small))
4065 } else {
4066 h_flex()
4067 .size(IconSize::default().rems())
4068 .invisible()
4069 .flex_none()
4070 }
4071 })
4072 .child(
4073 if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
4074 h_flex().h_6().w_full().child(editor.clone())
4075 } else {
4076 h_flex().h_6().map(|mut this| {
4077 if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
4078 let components = Path::new(&file_name)
4079 .components()
4080 .map(|comp| {
4081 let comp_str =
4082 comp.as_os_str().to_string_lossy().into_owned();
4083 comp_str
4084 })
4085 .collect::<Vec<_>>();
4086
4087 let components_len = components.len();
4088 let active_index = components_len
4089 - 1
4090 - folded_ancestors.current_ancestor_depth;
4091 const DELIMITER: SharedString =
4092 SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
4093 for (index, component) in components.into_iter().enumerate() {
4094 if index != 0 {
4095 let delimiter_target_index = index - 1;
4096 let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
4097 this = this.child(
4098 div()
4099 .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
4100 this.hover_scroll_task.take();
4101 this.folded_directory_drag_target = None;
4102 if let Some(target_entry_id) = target_entry_id {
4103 this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4104 }
4105 }))
4106 .on_drag_move(cx.listener(
4107 move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4108 if event.bounds.contains(&event.event.position) {
4109 this.folded_directory_drag_target = Some(
4110 FoldedDirectoryDragTarget {
4111 entry_id,
4112 index: delimiter_target_index,
4113 is_delimiter_target: true,
4114 }
4115 );
4116 } else {
4117 let is_current_target = this.folded_directory_drag_target
4118 .map_or(false, |target|
4119 target.entry_id == entry_id &&
4120 target.index == delimiter_target_index &&
4121 target.is_delimiter_target
4122 );
4123 if is_current_target {
4124 this.folded_directory_drag_target = None;
4125 }
4126 }
4127
4128 },
4129 ))
4130 .child(
4131 Label::new(DELIMITER.clone())
4132 .single_line()
4133 .color(filename_text_color)
4134 )
4135 );
4136 }
4137 let id = SharedString::from(format!(
4138 "project_panel_path_component_{}_{index}",
4139 entry_id.to_usize()
4140 ));
4141 let label = div()
4142 .id(id)
4143 .on_click(cx.listener(move |this, _, _, cx| {
4144 if index != active_index {
4145 if let Some(folds) =
4146 this.ancestors.get_mut(&entry_id)
4147 {
4148 folds.current_ancestor_depth =
4149 components_len - 1 - index;
4150 cx.notify();
4151 }
4152 }
4153 }))
4154 .when(index != components_len - 1, |div|{
4155 let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4156 div
4157 .on_drag_move(cx.listener(
4158 move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4159 if event.bounds.contains(&event.event.position) {
4160 this.folded_directory_drag_target = Some(
4161 FoldedDirectoryDragTarget {
4162 entry_id,
4163 index,
4164 is_delimiter_target: false,
4165 }
4166 );
4167 } else {
4168 let is_current_target = this.folded_directory_drag_target
4169 .as_ref()
4170 .map_or(false, |target|
4171 target.entry_id == entry_id &&
4172 target.index == index &&
4173 !target.is_delimiter_target
4174 );
4175 if is_current_target {
4176 this.folded_directory_drag_target = None;
4177 }
4178 }
4179 },
4180 ))
4181 .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4182 this.hover_scroll_task.take();
4183 this.folded_directory_drag_target = None;
4184 if let Some(target_entry_id) = target_entry_id {
4185 this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4186 }
4187 }))
4188 .when(folded_directory_drag_target.map_or(false, |target|
4189 target.entry_id == entry_id &&
4190 target.index == index
4191 ), |this| {
4192 this.bg(item_colors.drag_over)
4193 })
4194 })
4195 .child(
4196 Label::new(component)
4197 .single_line()
4198 .color(filename_text_color)
4199 .when(
4200 index == active_index
4201 && (is_active || is_marked),
4202 |this| this.underline(),
4203 ),
4204 );
4205
4206 this = this.child(label);
4207 }
4208
4209 this
4210 } else {
4211 this.child(
4212 Label::new(file_name)
4213 .single_line()
4214 .color(filename_text_color),
4215 )
4216 }
4217 })
4218 }
4219 .ml_1(),
4220 )
4221 .on_secondary_mouse_down(cx.listener(
4222 move |this, event: &MouseDownEvent, window, cx| {
4223 // Stop propagation to prevent the catch-all context menu for the project
4224 // panel from being deployed.
4225 cx.stop_propagation();
4226 // Some context menu actions apply to all marked entries. If the user
4227 // right-clicks on an entry that is not marked, they may not realize the
4228 // action applies to multiple entries. To avoid inadvertent changes, all
4229 // entries are unmarked.
4230 if !this.marked_entries.contains(&selection) {
4231 this.marked_entries.clear();
4232 }
4233 this.deploy_context_menu(event.position, entry_id, window, cx);
4234 },
4235 ))
4236 .overflow_x(),
4237 )
4238 .when_some(
4239 validation_color_and_message,
4240 |this, (color, message)| {
4241 this
4242 .relative()
4243 .child(
4244 deferred(
4245 div()
4246 .occlude()
4247 .absolute()
4248 .top_full()
4249 .left(px(-1.)) // Used px over rem so that it doesn't change with font size
4250 .right(px(-0.5))
4251 .py_1()
4252 .px_2()
4253 .border_1()
4254 .border_color(color)
4255 .bg(cx.theme().colors().background)
4256 .child(
4257 Label::new(message)
4258 .color(Color::from(color))
4259 .size(LabelSize::Small)
4260 )
4261 )
4262 )
4263 }
4264 )
4265 }
4266
4267 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4268 if !Self::should_show_scrollbar(cx)
4269 || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4270 {
4271 return None;
4272 }
4273 Some(
4274 div()
4275 .occlude()
4276 .id("project-panel-vertical-scroll")
4277 .on_mouse_move(cx.listener(|_, _, _, cx| {
4278 cx.notify();
4279 cx.stop_propagation()
4280 }))
4281 .on_hover(|_, _, cx| {
4282 cx.stop_propagation();
4283 })
4284 .on_any_mouse_down(|_, _, cx| {
4285 cx.stop_propagation();
4286 })
4287 .on_mouse_up(
4288 MouseButton::Left,
4289 cx.listener(|this, _, window, cx| {
4290 if !this.vertical_scrollbar_state.is_dragging()
4291 && !this.focus_handle.contains_focused(window, cx)
4292 {
4293 this.hide_scrollbar(window, cx);
4294 cx.notify();
4295 }
4296
4297 cx.stop_propagation();
4298 }),
4299 )
4300 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4301 cx.notify();
4302 }))
4303 .h_full()
4304 .absolute()
4305 .right_1()
4306 .top_1()
4307 .bottom_1()
4308 .w(px(12.))
4309 .cursor_default()
4310 .children(Scrollbar::vertical(
4311 // percentage as f32..end_offset as f32,
4312 self.vertical_scrollbar_state.clone(),
4313 )),
4314 )
4315 }
4316
4317 fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4318 if !Self::should_show_scrollbar(cx)
4319 || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4320 {
4321 return None;
4322 }
4323
4324 let scroll_handle = self.scroll_handle.0.borrow();
4325 let longest_item_width = scroll_handle
4326 .last_item_size
4327 .filter(|size| size.contents.width > size.item.width)?
4328 .contents
4329 .width
4330 .0 as f64;
4331 if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4332 return None;
4333 }
4334
4335 Some(
4336 div()
4337 .occlude()
4338 .id("project-panel-horizontal-scroll")
4339 .on_mouse_move(cx.listener(|_, _, _, cx| {
4340 cx.notify();
4341 cx.stop_propagation()
4342 }))
4343 .on_hover(|_, _, cx| {
4344 cx.stop_propagation();
4345 })
4346 .on_any_mouse_down(|_, _, cx| {
4347 cx.stop_propagation();
4348 })
4349 .on_mouse_up(
4350 MouseButton::Left,
4351 cx.listener(|this, _, window, cx| {
4352 if !this.horizontal_scrollbar_state.is_dragging()
4353 && !this.focus_handle.contains_focused(window, cx)
4354 {
4355 this.hide_scrollbar(window, cx);
4356 cx.notify();
4357 }
4358
4359 cx.stop_propagation();
4360 }),
4361 )
4362 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4363 cx.notify();
4364 }))
4365 .w_full()
4366 .absolute()
4367 .right_1()
4368 .left_1()
4369 .bottom_1()
4370 .h(px(12.))
4371 .cursor_default()
4372 .when(self.width.is_some(), |this| {
4373 this.children(Scrollbar::horizontal(
4374 self.horizontal_scrollbar_state.clone(),
4375 ))
4376 }),
4377 )
4378 }
4379
4380 fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4381 let mut dispatch_context = KeyContext::new_with_defaults();
4382 dispatch_context.add("ProjectPanel");
4383 dispatch_context.add("menu");
4384
4385 let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4386 "editing"
4387 } else {
4388 "not_editing"
4389 };
4390
4391 dispatch_context.add(identifier);
4392 dispatch_context
4393 }
4394
4395 fn should_show_scrollbar(cx: &App) -> bool {
4396 let show = ProjectPanelSettings::get_global(cx)
4397 .scrollbar
4398 .show
4399 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4400 match show {
4401 ShowScrollbar::Auto => true,
4402 ShowScrollbar::System => true,
4403 ShowScrollbar::Always => true,
4404 ShowScrollbar::Never => false,
4405 }
4406 }
4407
4408 fn should_autohide_scrollbar(cx: &App) -> bool {
4409 let show = ProjectPanelSettings::get_global(cx)
4410 .scrollbar
4411 .show
4412 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4413 match show {
4414 ShowScrollbar::Auto => true,
4415 ShowScrollbar::System => cx
4416 .try_global::<ScrollbarAutoHide>()
4417 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4418 ShowScrollbar::Always => false,
4419 ShowScrollbar::Never => true,
4420 }
4421 }
4422
4423 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4424 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4425 if !Self::should_autohide_scrollbar(cx) {
4426 return;
4427 }
4428 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4429 cx.background_executor()
4430 .timer(SCROLLBAR_SHOW_INTERVAL)
4431 .await;
4432 panel
4433 .update(cx, |panel, cx| {
4434 panel.show_scrollbar = false;
4435 cx.notify();
4436 })
4437 .log_err();
4438 }))
4439 }
4440
4441 fn reveal_entry(
4442 &mut self,
4443 project: Entity<Project>,
4444 entry_id: ProjectEntryId,
4445 skip_ignored: bool,
4446 cx: &mut Context<Self>,
4447 ) -> Result<()> {
4448 if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4449 let worktree = worktree.read(cx);
4450 if skip_ignored
4451 && worktree
4452 .entry_for_id(entry_id)
4453 .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4454 {
4455 return Err(anyhow!(
4456 "can't reveal an ignored entry in the project panel"
4457 ));
4458 }
4459
4460 let worktree_id = worktree.id();
4461 self.expand_entry(worktree_id, entry_id, cx);
4462 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4463 self.marked_entries.clear();
4464 self.marked_entries.insert(SelectedEntry {
4465 worktree_id,
4466 entry_id,
4467 });
4468 self.autoscroll(cx);
4469 cx.notify();
4470 Ok(())
4471 } else {
4472 Err(anyhow!(
4473 "can't reveal a non-existent entry in the project panel"
4474 ))
4475 }
4476 }
4477
4478 fn find_active_indent_guide(
4479 &self,
4480 indent_guides: &[IndentGuideLayout],
4481 cx: &App,
4482 ) -> Option<usize> {
4483 let (worktree, entry) = self.selected_entry(cx)?;
4484
4485 // Find the parent entry of the indent guide, this will either be the
4486 // expanded folder we have selected, or the parent of the currently
4487 // selected file/collapsed directory
4488 let mut entry = entry;
4489 loop {
4490 let is_expanded_dir = entry.is_dir()
4491 && self
4492 .expanded_dir_ids
4493 .get(&worktree.id())
4494 .map(|ids| ids.binary_search(&entry.id).is_ok())
4495 .unwrap_or(false);
4496 if is_expanded_dir {
4497 break;
4498 }
4499 entry = worktree.entry_for_path(&entry.path.parent()?)?;
4500 }
4501
4502 let (active_indent_range, depth) = {
4503 let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4504 let child_paths = &self.visible_entries[worktree_ix].1;
4505 let mut child_count = 0;
4506 let depth = entry.path.ancestors().count();
4507 while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4508 if entry.path.ancestors().count() <= depth {
4509 break;
4510 }
4511 child_count += 1;
4512 }
4513
4514 let start = ix + 1;
4515 let end = start + child_count;
4516
4517 let (_, entries, paths) = &self.visible_entries[worktree_ix];
4518 let visible_worktree_entries =
4519 paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4520
4521 // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4522 let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4523 (start..end, depth)
4524 };
4525
4526 let candidates = indent_guides
4527 .iter()
4528 .enumerate()
4529 .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4530
4531 for (i, indent) in candidates {
4532 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4533 if active_indent_range.start <= indent.offset.y + indent.length
4534 && indent.offset.y <= active_indent_range.end
4535 {
4536 return Some(i);
4537 }
4538 }
4539 None
4540 }
4541}
4542
4543fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4544 const ICON_SIZE_FACTOR: usize = 2;
4545 let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4546 if is_symlink {
4547 item_width += ICON_SIZE_FACTOR;
4548 }
4549 item_width
4550}
4551
4552impl Render for ProjectPanel {
4553 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4554 let has_worktree = !self.visible_entries.is_empty();
4555 let project = self.project.read(cx);
4556 let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4557 let show_indent_guides =
4558 ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4559 let is_local = project.is_local();
4560
4561 if has_worktree {
4562 let item_count = self
4563 .visible_entries
4564 .iter()
4565 .map(|(_, worktree_entries, _)| worktree_entries.len())
4566 .sum();
4567
4568 fn handle_drag_move_scroll<T: 'static>(
4569 this: &mut ProjectPanel,
4570 e: &DragMoveEvent<T>,
4571 window: &mut Window,
4572 cx: &mut Context<ProjectPanel>,
4573 ) {
4574 if !e.bounds.contains(&e.event.position) {
4575 return;
4576 }
4577 this.hover_scroll_task.take();
4578 let panel_height = e.bounds.size.height;
4579 if panel_height <= px(0.) {
4580 return;
4581 }
4582
4583 let event_offset = e.event.position.y - e.bounds.origin.y;
4584 // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4585 let hovered_region_offset = event_offset / panel_height;
4586
4587 // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4588 // These pixels offsets were picked arbitrarily.
4589 let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4590 8.
4591 } else if hovered_region_offset <= 0.15 {
4592 5.
4593 } else if hovered_region_offset >= 0.95 {
4594 -8.
4595 } else if hovered_region_offset >= 0.85 {
4596 -5.
4597 } else {
4598 return;
4599 };
4600 let adjustment = point(px(0.), px(vertical_scroll_offset));
4601 this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| {
4602 loop {
4603 let should_stop_scrolling = this
4604 .update(cx, |this, cx| {
4605 this.hover_scroll_task.as_ref()?;
4606 let handle = this.scroll_handle.0.borrow_mut();
4607 let offset = handle.base_handle.offset();
4608
4609 handle.base_handle.set_offset(offset + adjustment);
4610 cx.notify();
4611 Some(())
4612 })
4613 .ok()
4614 .flatten()
4615 .is_some();
4616 if should_stop_scrolling {
4617 return;
4618 }
4619 cx.background_executor()
4620 .timer(Duration::from_millis(16))
4621 .await;
4622 }
4623 }));
4624 }
4625 h_flex()
4626 .id("project-panel")
4627 .group("project-panel")
4628 .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4629 .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4630 .size_full()
4631 .relative()
4632 .on_hover(cx.listener(|this, hovered, window, cx| {
4633 if *hovered {
4634 this.show_scrollbar = true;
4635 this.hide_scrollbar_task.take();
4636 cx.notify();
4637 } else if !this.focus_handle.contains_focused(window, cx) {
4638 this.hide_scrollbar(window, cx);
4639 }
4640 }))
4641 .on_click(cx.listener(|this, _event, _, cx| {
4642 cx.stop_propagation();
4643 this.selection = None;
4644 this.marked_entries.clear();
4645 }))
4646 .key_context(self.dispatch_context(window, cx))
4647 .on_action(cx.listener(Self::select_next))
4648 .on_action(cx.listener(Self::select_previous))
4649 .on_action(cx.listener(Self::select_first))
4650 .on_action(cx.listener(Self::select_last))
4651 .on_action(cx.listener(Self::select_parent))
4652 .on_action(cx.listener(Self::select_next_git_entry))
4653 .on_action(cx.listener(Self::select_prev_git_entry))
4654 .on_action(cx.listener(Self::select_next_diagnostic))
4655 .on_action(cx.listener(Self::select_prev_diagnostic))
4656 .on_action(cx.listener(Self::select_next_directory))
4657 .on_action(cx.listener(Self::select_prev_directory))
4658 .on_action(cx.listener(Self::expand_selected_entry))
4659 .on_action(cx.listener(Self::collapse_selected_entry))
4660 .on_action(cx.listener(Self::collapse_all_entries))
4661 .on_action(cx.listener(Self::open))
4662 .on_action(cx.listener(Self::open_permanent))
4663 .on_action(cx.listener(Self::confirm))
4664 .on_action(cx.listener(Self::cancel))
4665 .on_action(cx.listener(Self::copy_path))
4666 .on_action(cx.listener(Self::copy_relative_path))
4667 .on_action(cx.listener(Self::new_search_in_directory))
4668 .on_action(cx.listener(Self::unfold_directory))
4669 .on_action(cx.listener(Self::fold_directory))
4670 .on_action(cx.listener(Self::remove_from_project))
4671 .when(!project.is_read_only(cx), |el| {
4672 el.on_action(cx.listener(Self::new_file))
4673 .on_action(cx.listener(Self::new_directory))
4674 .on_action(cx.listener(Self::rename))
4675 .on_action(cx.listener(Self::delete))
4676 .on_action(cx.listener(Self::trash))
4677 .on_action(cx.listener(Self::cut))
4678 .on_action(cx.listener(Self::copy))
4679 .on_action(cx.listener(Self::paste))
4680 .on_action(cx.listener(Self::duplicate))
4681 .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4682 if event.up.click_count > 1 {
4683 if let Some(entry_id) = this.last_worktree_root_id {
4684 let project = this.project.read(cx);
4685
4686 let worktree_id = if let Some(worktree) =
4687 project.worktree_for_entry(entry_id, cx)
4688 {
4689 worktree.read(cx).id()
4690 } else {
4691 return;
4692 };
4693
4694 this.selection = Some(SelectedEntry {
4695 worktree_id,
4696 entry_id,
4697 });
4698
4699 this.new_file(&NewFile, window, cx);
4700 }
4701 }
4702 }))
4703 })
4704 .when(project.is_local(), |el| {
4705 el.on_action(cx.listener(Self::reveal_in_finder))
4706 .on_action(cx.listener(Self::open_system))
4707 .on_action(cx.listener(Self::open_in_terminal))
4708 })
4709 .when(project.is_via_ssh(), |el| {
4710 el.on_action(cx.listener(Self::open_in_terminal))
4711 })
4712 .on_mouse_down(
4713 MouseButton::Right,
4714 cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4715 // When deploying the context menu anywhere below the last project entry,
4716 // act as if the user clicked the root of the last worktree.
4717 if let Some(entry_id) = this.last_worktree_root_id {
4718 this.deploy_context_menu(event.position, entry_id, window, cx);
4719 }
4720 }),
4721 )
4722 .track_focus(&self.focus_handle(cx))
4723 .child(
4724 uniform_list(cx.entity().clone(), "entries", item_count, {
4725 |this, range, window, cx| {
4726 let mut items = Vec::with_capacity(range.end - range.start);
4727 this.for_each_visible_entry(
4728 range,
4729 window,
4730 cx,
4731 |id, details, window, cx| {
4732 items.push(this.render_entry(id, details, window, cx));
4733 },
4734 );
4735 items
4736 }
4737 })
4738 .when(show_indent_guides, |list| {
4739 list.with_decoration(
4740 ui::indent_guides(
4741 cx.entity().clone(),
4742 px(indent_size),
4743 IndentGuideColors::panel(cx),
4744 |this, range, window, cx| {
4745 let mut items =
4746 SmallVec::with_capacity(range.end - range.start);
4747 this.iter_visible_entries(
4748 range,
4749 window,
4750 cx,
4751 |entry, entries, _, _| {
4752 let (depth, _) = Self::calculate_depth_and_difference(
4753 entry, entries,
4754 );
4755 items.push(depth);
4756 },
4757 );
4758 items
4759 },
4760 )
4761 .on_click(cx.listener(
4762 |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4763 if window.modifiers().secondary() {
4764 let ix = active_indent_guide.offset.y;
4765 let Some((target_entry, worktree)) = maybe!({
4766 let (worktree_id, entry) = this.entry_at_index(ix)?;
4767 let worktree = this
4768 .project
4769 .read(cx)
4770 .worktree_for_id(worktree_id, cx)?;
4771 let target_entry = worktree
4772 .read(cx)
4773 .entry_for_path(&entry.path.parent()?)?;
4774 Some((target_entry, worktree))
4775 }) else {
4776 return;
4777 };
4778
4779 this.collapse_entry(target_entry.clone(), worktree, cx);
4780 }
4781 },
4782 ))
4783 .with_render_fn(
4784 cx.entity().clone(),
4785 move |this, params, _, cx| {
4786 const LEFT_OFFSET: Pixels = px(14.);
4787 const PADDING_Y: Pixels = px(4.);
4788 const HITBOX_OVERDRAW: Pixels = px(3.);
4789
4790 let active_indent_guide_index =
4791 this.find_active_indent_guide(¶ms.indent_guides, cx);
4792
4793 let indent_size = params.indent_size;
4794 let item_height = params.item_height;
4795
4796 params
4797 .indent_guides
4798 .into_iter()
4799 .enumerate()
4800 .map(|(idx, layout)| {
4801 let offset = if layout.continues_offscreen {
4802 px(0.)
4803 } else {
4804 PADDING_Y
4805 };
4806 let bounds = Bounds::new(
4807 point(
4808 layout.offset.x * indent_size + LEFT_OFFSET,
4809 layout.offset.y * item_height + offset,
4810 ),
4811 size(
4812 px(1.),
4813 layout.length * item_height - offset * 2.,
4814 ),
4815 );
4816 ui::RenderedIndentGuide {
4817 bounds,
4818 layout,
4819 is_active: Some(idx) == active_indent_guide_index,
4820 hitbox: Some(Bounds::new(
4821 point(
4822 bounds.origin.x - HITBOX_OVERDRAW,
4823 bounds.origin.y,
4824 ),
4825 size(
4826 bounds.size.width + HITBOX_OVERDRAW * 2.,
4827 bounds.size.height,
4828 ),
4829 )),
4830 }
4831 })
4832 .collect()
4833 },
4834 ),
4835 )
4836 })
4837 .size_full()
4838 .with_sizing_behavior(ListSizingBehavior::Infer)
4839 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4840 .with_width_from_item(self.max_width_item_index)
4841 .track_scroll(self.scroll_handle.clone()),
4842 )
4843 .children(self.render_vertical_scrollbar(cx))
4844 .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4845 this.pb_4().child(scrollbar)
4846 })
4847 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4848 deferred(
4849 anchored()
4850 .position(*position)
4851 .anchor(gpui::Corner::TopLeft)
4852 .child(menu.clone()),
4853 )
4854 .with_priority(1)
4855 }))
4856 } else {
4857 v_flex()
4858 .id("empty-project_panel")
4859 .size_full()
4860 .p_4()
4861 .track_focus(&self.focus_handle(cx))
4862 .child(
4863 Button::new("open_project", "Open a project")
4864 .full_width()
4865 .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4866 .on_click(cx.listener(|this, _, window, cx| {
4867 this.workspace
4868 .update(cx, |_, cx| {
4869 window.dispatch_action(Box::new(workspace::Open), cx)
4870 })
4871 .log_err();
4872 })),
4873 )
4874 .when(is_local, |div| {
4875 div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4876 style.bg(cx.theme().colors().drop_target_background)
4877 })
4878 .on_drop(cx.listener(
4879 move |this, external_paths: &ExternalPaths, window, cx| {
4880 this.last_external_paths_drag_over_entry = None;
4881 this.marked_entries.clear();
4882 this.hover_scroll_task.take();
4883 if let Some(task) = this
4884 .workspace
4885 .update(cx, |workspace, cx| {
4886 workspace.open_workspace_for_paths(
4887 true,
4888 external_paths.paths().to_owned(),
4889 window,
4890 cx,
4891 )
4892 })
4893 .log_err()
4894 {
4895 task.detach_and_log_err(cx);
4896 }
4897 cx.stop_propagation();
4898 },
4899 ))
4900 })
4901 }
4902 }
4903}
4904
4905impl Render for DraggedProjectEntryView {
4906 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4907 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4908 h_flex()
4909 .font(ui_font)
4910 .pl(self.click_offset.x + px(12.))
4911 .pt(self.click_offset.y + px(12.))
4912 .child(
4913 div()
4914 .flex()
4915 .gap_1()
4916 .items_center()
4917 .py_1()
4918 .px_2()
4919 .rounded_lg()
4920 .bg(cx.theme().colors().background)
4921 .map(|this| {
4922 if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4923 this.child(Label::new(format!("{} entries", self.selections.len())))
4924 } else {
4925 this.child(if let Some(icon) = &self.details.icon {
4926 div().child(Icon::from_path(icon.clone()))
4927 } else {
4928 div()
4929 })
4930 .child(Label::new(self.details.filename.clone()))
4931 }
4932 }),
4933 )
4934 }
4935}
4936
4937impl EventEmitter<Event> for ProjectPanel {}
4938
4939impl EventEmitter<PanelEvent> for ProjectPanel {}
4940
4941impl Panel for ProjectPanel {
4942 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4943 match ProjectPanelSettings::get_global(cx).dock {
4944 ProjectPanelDockPosition::Left => DockPosition::Left,
4945 ProjectPanelDockPosition::Right => DockPosition::Right,
4946 }
4947 }
4948
4949 fn position_is_valid(&self, position: DockPosition) -> bool {
4950 matches!(position, DockPosition::Left | DockPosition::Right)
4951 }
4952
4953 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4954 settings::update_settings_file::<ProjectPanelSettings>(
4955 self.fs.clone(),
4956 cx,
4957 move |settings, _| {
4958 let dock = match position {
4959 DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4960 DockPosition::Right => ProjectPanelDockPosition::Right,
4961 };
4962 settings.dock = Some(dock);
4963 },
4964 );
4965 }
4966
4967 fn size(&self, _: &Window, cx: &App) -> Pixels {
4968 self.width
4969 .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4970 }
4971
4972 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4973 self.width = size;
4974 self.serialize(cx);
4975 cx.notify();
4976 }
4977
4978 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4979 ProjectPanelSettings::get_global(cx)
4980 .button
4981 .then_some(IconName::FileTree)
4982 }
4983
4984 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4985 Some("Project Panel")
4986 }
4987
4988 fn toggle_action(&self) -> Box<dyn Action> {
4989 Box::new(ToggleFocus)
4990 }
4991
4992 fn persistent_name() -> &'static str {
4993 "Project Panel"
4994 }
4995
4996 fn starts_open(&self, _: &Window, cx: &App) -> bool {
4997 let project = &self.project.read(cx);
4998 project.visible_worktrees(cx).any(|tree| {
4999 tree.read(cx)
5000 .root_entry()
5001 .map_or(false, |entry| entry.is_dir())
5002 })
5003 }
5004
5005 fn activation_priority(&self) -> u32 {
5006 0
5007 }
5008}
5009
5010impl Focusable for ProjectPanel {
5011 fn focus_handle(&self, _cx: &App) -> FocusHandle {
5012 self.focus_handle.clone()
5013 }
5014}
5015
5016impl ClipboardEntry {
5017 fn is_cut(&self) -> bool {
5018 matches!(self, Self::Cut { .. })
5019 }
5020
5021 fn items(&self) -> &BTreeSet<SelectedEntry> {
5022 match self {
5023 ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
5024 }
5025 }
5026}
5027
5028#[cfg(test)]
5029mod project_panel_tests;