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