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