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