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