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!(
1497 "\n\n{dirty_buffers} of these have unsaved changes, which will be lost."
1498 )
1499 };
1500
1501 format!(
1502 "Do you want to {} the following {} files?\n{}{unsaved_warning}",
1503 operation.to_lowercase(),
1504 file_paths.len(),
1505 names.join("\n")
1506 )
1507 }
1508 };
1509 Some(window.prompt(PromptLevel::Info, &prompt, None, &[operation, "Cancel"], cx))
1510 } else {
1511 None
1512 };
1513 let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
1514 cx.spawn_in(window, async move |panel, cx| {
1515 if let Some(answer) = answer {
1516 if answer.await != Ok(0) {
1517 return anyhow::Ok(());
1518 }
1519 }
1520 for (entry_id, _) in file_paths {
1521 panel
1522 .update(cx, |panel, cx| {
1523 panel
1524 .project
1525 .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
1526 .context("no such entry")
1527 })??
1528 .await?;
1529 }
1530 panel.update_in(cx, |panel, window, cx| {
1531 if let Some(next_selection) = next_selection {
1532 panel.selection = Some(next_selection);
1533 panel.autoscroll(cx);
1534 } else {
1535 panel.select_last(&SelectLast {}, window, cx);
1536 }
1537 })?;
1538 Ok(())
1539 })
1540 .detach_and_log_err(cx);
1541 Some(())
1542 });
1543 }
1544
1545 fn find_next_selection_after_deletion(
1546 &self,
1547 sanitized_entries: BTreeSet<SelectedEntry>,
1548 cx: &mut Context<Self>,
1549 ) -> Option<SelectedEntry> {
1550 if sanitized_entries.is_empty() {
1551 return None;
1552 }
1553 let project = self.project.read(cx);
1554 let (worktree_id, worktree) = sanitized_entries
1555 .iter()
1556 .map(|entry| entry.worktree_id)
1557 .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
1558 .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
1559 let git_store = project.git_store().read(cx);
1560
1561 let marked_entries_in_worktree = sanitized_entries
1562 .iter()
1563 .filter(|e| e.worktree_id == worktree_id)
1564 .collect::<HashSet<_>>();
1565 let latest_entry = marked_entries_in_worktree
1566 .iter()
1567 .max_by(|a, b| {
1568 match (
1569 worktree.entry_for_id(a.entry_id),
1570 worktree.entry_for_id(b.entry_id),
1571 ) {
1572 (Some(a), Some(b)) => {
1573 compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
1574 }
1575 _ => cmp::Ordering::Equal,
1576 }
1577 })
1578 .and_then(|e| worktree.entry_for_id(e.entry_id))?;
1579
1580 let parent_path = latest_entry.path.parent()?;
1581 let parent_entry = worktree.entry_for_path(parent_path)?;
1582
1583 // Remove all siblings that are being deleted except the last marked entry
1584 let repo_snapshots = git_store.repo_snapshots(cx);
1585 let worktree_snapshot = worktree.snapshot();
1586 let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
1587 let mut siblings: Vec<_> =
1588 ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
1589 .filter(|sibling| {
1590 (sibling.id == latest_entry.id)
1591 || (!marked_entries_in_worktree.contains(&&SelectedEntry {
1592 worktree_id,
1593 entry_id: sibling.id,
1594 }) && (!hide_gitignore || !sibling.is_ignored))
1595 })
1596 .map(|entry| entry.to_owned())
1597 .collect();
1598
1599 project::sort_worktree_entries(&mut siblings);
1600 let sibling_entry_index = siblings
1601 .iter()
1602 .position(|sibling| sibling.id == latest_entry.id)?;
1603
1604 if let Some(next_sibling) = sibling_entry_index
1605 .checked_add(1)
1606 .and_then(|i| siblings.get(i))
1607 {
1608 return Some(SelectedEntry {
1609 worktree_id,
1610 entry_id: next_sibling.id,
1611 });
1612 }
1613 if let Some(prev_sibling) = sibling_entry_index
1614 .checked_sub(1)
1615 .and_then(|i| siblings.get(i))
1616 {
1617 return Some(SelectedEntry {
1618 worktree_id,
1619 entry_id: prev_sibling.id,
1620 });
1621 }
1622 // No neighbour sibling found, fall back to parent
1623 Some(SelectedEntry {
1624 worktree_id,
1625 entry_id: parent_entry.id,
1626 })
1627 }
1628
1629 fn unfold_directory(&mut self, _: &UnfoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1630 if let Some((worktree, entry)) = self.selected_entry(cx) {
1631 self.unfolded_dir_ids.insert(entry.id);
1632
1633 let snapshot = worktree.snapshot();
1634 let mut parent_path = entry.path.parent();
1635 while let Some(path) = parent_path {
1636 if let Some(parent_entry) = worktree.entry_for_path(path) {
1637 let mut children_iter = snapshot.child_entries(path);
1638
1639 if children_iter.by_ref().take(2).count() > 1 {
1640 break;
1641 }
1642
1643 self.unfolded_dir_ids.insert(parent_entry.id);
1644 parent_path = path.parent();
1645 } else {
1646 break;
1647 }
1648 }
1649
1650 self.update_visible_entries(None, cx);
1651 self.autoscroll(cx);
1652 cx.notify();
1653 }
1654 }
1655
1656 fn fold_directory(&mut self, _: &FoldDirectory, _: &mut Window, cx: &mut Context<Self>) {
1657 if let Some((worktree, entry)) = self.selected_entry(cx) {
1658 self.unfolded_dir_ids.remove(&entry.id);
1659
1660 let snapshot = worktree.snapshot();
1661 let mut path = &*entry.path;
1662 loop {
1663 let mut child_entries_iter = snapshot.child_entries(path);
1664 if let Some(child) = child_entries_iter.next() {
1665 if child_entries_iter.next().is_none() && child.is_dir() {
1666 self.unfolded_dir_ids.remove(&child.id);
1667 path = &*child.path;
1668 } else {
1669 break;
1670 }
1671 } else {
1672 break;
1673 }
1674 }
1675
1676 self.update_visible_entries(None, cx);
1677 self.autoscroll(cx);
1678 cx.notify();
1679 }
1680 }
1681
1682 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1683 if let Some(edit_state) = &self.edit_state {
1684 if edit_state.processing_filename.is_none() {
1685 self.filename_editor.update(cx, |editor, cx| {
1686 editor.move_to_end_of_line(
1687 &editor::actions::MoveToEndOfLine {
1688 stop_at_soft_wraps: false,
1689 },
1690 window,
1691 cx,
1692 );
1693 });
1694 return;
1695 }
1696 }
1697 if let Some(selection) = self.selection {
1698 let (mut worktree_ix, mut entry_ix, _) =
1699 self.index_for_selection(selection).unwrap_or_default();
1700 if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) {
1701 if entry_ix + 1 < worktree_entries.len() {
1702 entry_ix += 1;
1703 } else {
1704 worktree_ix += 1;
1705 entry_ix = 0;
1706 }
1707 }
1708
1709 if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix)
1710 {
1711 if let Some(entry) = worktree_entries.get(entry_ix) {
1712 let selection = SelectedEntry {
1713 worktree_id: *worktree_id,
1714 entry_id: entry.id,
1715 };
1716 self.selection = Some(selection);
1717 if window.modifiers().shift {
1718 self.marked_entries.insert(selection);
1719 }
1720
1721 self.autoscroll(cx);
1722 cx.notify();
1723 }
1724 }
1725 } else {
1726 self.select_first(&SelectFirst {}, window, cx);
1727 }
1728 }
1729
1730 fn select_prev_diagnostic(
1731 &mut self,
1732 _: &SelectPrevDiagnostic,
1733 _: &mut Window,
1734 cx: &mut Context<Self>,
1735 ) {
1736 let selection = self.find_entry(
1737 self.selection.as_ref(),
1738 true,
1739 |entry, worktree_id| {
1740 (self.selection.is_none()
1741 || self.selection.is_some_and(|selection| {
1742 if selection.worktree_id == worktree_id {
1743 selection.entry_id != entry.id
1744 } else {
1745 true
1746 }
1747 }))
1748 && entry.is_file()
1749 && self
1750 .diagnostics
1751 .contains_key(&(worktree_id, entry.path.to_path_buf()))
1752 },
1753 cx,
1754 );
1755
1756 if let Some(selection) = selection {
1757 self.selection = Some(selection);
1758 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1759 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1760 self.autoscroll(cx);
1761 cx.notify();
1762 }
1763 }
1764
1765 fn select_next_diagnostic(
1766 &mut self,
1767 _: &SelectNextDiagnostic,
1768 _: &mut Window,
1769 cx: &mut Context<Self>,
1770 ) {
1771 let selection = self.find_entry(
1772 self.selection.as_ref(),
1773 false,
1774 |entry, worktree_id| {
1775 (self.selection.is_none()
1776 || self.selection.is_some_and(|selection| {
1777 if selection.worktree_id == worktree_id {
1778 selection.entry_id != entry.id
1779 } else {
1780 true
1781 }
1782 }))
1783 && entry.is_file()
1784 && self
1785 .diagnostics
1786 .contains_key(&(worktree_id, entry.path.to_path_buf()))
1787 },
1788 cx,
1789 );
1790
1791 if let Some(selection) = selection {
1792 self.selection = Some(selection);
1793 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1794 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1795 self.autoscroll(cx);
1796 cx.notify();
1797 }
1798 }
1799
1800 fn select_prev_git_entry(
1801 &mut self,
1802 _: &SelectPrevGitEntry,
1803 _: &mut Window,
1804 cx: &mut Context<Self>,
1805 ) {
1806 let selection = self.find_entry(
1807 self.selection.as_ref(),
1808 true,
1809 |entry, worktree_id| {
1810 (self.selection.is_none()
1811 || self.selection.is_some_and(|selection| {
1812 if selection.worktree_id == worktree_id {
1813 selection.entry_id != entry.id
1814 } else {
1815 true
1816 }
1817 }))
1818 && entry.is_file()
1819 && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1820 },
1821 cx,
1822 );
1823
1824 if let Some(selection) = selection {
1825 self.selection = Some(selection);
1826 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1827 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1828 self.autoscroll(cx);
1829 cx.notify();
1830 }
1831 }
1832
1833 fn select_prev_directory(
1834 &mut self,
1835 _: &SelectPrevDirectory,
1836 _: &mut Window,
1837 cx: &mut Context<Self>,
1838 ) {
1839 let selection = self.find_visible_entry(
1840 self.selection.as_ref(),
1841 true,
1842 |entry, worktree_id| {
1843 (self.selection.is_none()
1844 || self.selection.is_some_and(|selection| {
1845 if selection.worktree_id == worktree_id {
1846 selection.entry_id != entry.id
1847 } else {
1848 true
1849 }
1850 }))
1851 && entry.is_dir()
1852 },
1853 cx,
1854 );
1855
1856 if let Some(selection) = selection {
1857 self.selection = Some(selection);
1858 self.autoscroll(cx);
1859 cx.notify();
1860 }
1861 }
1862
1863 fn select_next_directory(
1864 &mut self,
1865 _: &SelectNextDirectory,
1866 _: &mut Window,
1867 cx: &mut Context<Self>,
1868 ) {
1869 let selection = self.find_visible_entry(
1870 self.selection.as_ref(),
1871 false,
1872 |entry, worktree_id| {
1873 (self.selection.is_none()
1874 || self.selection.is_some_and(|selection| {
1875 if selection.worktree_id == worktree_id {
1876 selection.entry_id != entry.id
1877 } else {
1878 true
1879 }
1880 }))
1881 && entry.is_dir()
1882 },
1883 cx,
1884 );
1885
1886 if let Some(selection) = selection {
1887 self.selection = Some(selection);
1888 self.autoscroll(cx);
1889 cx.notify();
1890 }
1891 }
1892
1893 fn select_next_git_entry(
1894 &mut self,
1895 _: &SelectNextGitEntry,
1896 _: &mut Window,
1897 cx: &mut Context<Self>,
1898 ) {
1899 let selection = self.find_entry(
1900 self.selection.as_ref(),
1901 false,
1902 |entry, worktree_id| {
1903 (self.selection.is_none()
1904 || self.selection.is_some_and(|selection| {
1905 if selection.worktree_id == worktree_id {
1906 selection.entry_id != entry.id
1907 } else {
1908 true
1909 }
1910 }))
1911 && entry.is_file()
1912 && entry.git_summary.index.modified + entry.git_summary.worktree.modified > 0
1913 },
1914 cx,
1915 );
1916
1917 if let Some(selection) = selection {
1918 self.selection = Some(selection);
1919 self.expand_entry(selection.worktree_id, selection.entry_id, cx);
1920 self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx);
1921 self.autoscroll(cx);
1922 cx.notify();
1923 }
1924 }
1925
1926 fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1927 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
1928 if let Some(parent) = entry.path.parent() {
1929 let worktree = worktree.read(cx);
1930 if let Some(parent_entry) = worktree.entry_for_path(parent) {
1931 self.selection = Some(SelectedEntry {
1932 worktree_id: worktree.id(),
1933 entry_id: parent_entry.id,
1934 });
1935 self.autoscroll(cx);
1936 cx.notify();
1937 }
1938 }
1939 } else {
1940 self.select_first(&SelectFirst {}, window, cx);
1941 }
1942 }
1943
1944 fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1945 let worktree = self
1946 .visible_entries
1947 .first()
1948 .and_then(|(worktree_id, _, _)| {
1949 self.project.read(cx).worktree_for_id(*worktree_id, cx)
1950 });
1951 if let Some(worktree) = worktree {
1952 let worktree = worktree.read(cx);
1953 let worktree_id = worktree.id();
1954 if let Some(root_entry) = worktree.root_entry() {
1955 let selection = SelectedEntry {
1956 worktree_id,
1957 entry_id: root_entry.id,
1958 };
1959 self.selection = Some(selection);
1960 if window.modifiers().shift {
1961 self.marked_entries.insert(selection);
1962 }
1963 self.autoscroll(cx);
1964 cx.notify();
1965 }
1966 }
1967 }
1968
1969 fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
1970 if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() {
1971 let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx);
1972 if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) {
1973 let worktree = worktree.read(cx);
1974 if let Some(entry) = worktree.entry_for_id(entry.id) {
1975 let selection = SelectedEntry {
1976 worktree_id: *worktree_id,
1977 entry_id: entry.id,
1978 };
1979 self.selection = Some(selection);
1980 self.autoscroll(cx);
1981 cx.notify();
1982 }
1983 }
1984 }
1985 }
1986
1987 fn autoscroll(&mut self, cx: &mut Context<Self>) {
1988 if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
1989 self.scroll_handle
1990 .scroll_to_item(index, ScrollStrategy::Center);
1991 cx.notify();
1992 }
1993 }
1994
1995 fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
1996 let entries = self.disjoint_entries(cx);
1997 if !entries.is_empty() {
1998 self.clipboard = Some(ClipboardEntry::Cut(entries));
1999 cx.notify();
2000 }
2001 }
2002
2003 fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2004 let entries = self.disjoint_entries(cx);
2005 if !entries.is_empty() {
2006 self.clipboard = Some(ClipboardEntry::Copied(entries));
2007 cx.notify();
2008 }
2009 }
2010
2011 fn create_paste_path(
2012 &self,
2013 source: &SelectedEntry,
2014 (worktree, target_entry): (Entity<Worktree>, &Entry),
2015 cx: &App,
2016 ) -> Option<(PathBuf, Option<Range<usize>>)> {
2017 let mut new_path = target_entry.path.to_path_buf();
2018 // If we're pasting into a file, or a directory into itself, go up one level.
2019 if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
2020 new_path.pop();
2021 }
2022 let clipboard_entry_file_name = self
2023 .project
2024 .read(cx)
2025 .path_for_entry(source.entry_id, cx)?
2026 .path
2027 .file_name()?
2028 .to_os_string();
2029 new_path.push(&clipboard_entry_file_name);
2030 let extension = new_path.extension().map(|e| e.to_os_string());
2031 let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
2032 let file_name_len = file_name_without_extension.to_string_lossy().len();
2033 let mut disambiguation_range = None;
2034 let mut ix = 0;
2035 {
2036 let worktree = worktree.read(cx);
2037 while worktree.entry_for_path(&new_path).is_some() {
2038 new_path.pop();
2039
2040 let mut new_file_name = file_name_without_extension.to_os_string();
2041
2042 let disambiguation = " copy";
2043 let mut disambiguation_len = disambiguation.len();
2044
2045 new_file_name.push(disambiguation);
2046
2047 if ix > 0 {
2048 let extra_disambiguation = format!(" {}", ix);
2049 disambiguation_len += extra_disambiguation.len();
2050
2051 new_file_name.push(extra_disambiguation);
2052 }
2053 if let Some(extension) = extension.as_ref() {
2054 new_file_name.push(".");
2055 new_file_name.push(extension);
2056 }
2057
2058 new_path.push(new_file_name);
2059 disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
2060 ix += 1;
2061 }
2062 }
2063 Some((new_path, disambiguation_range))
2064 }
2065
2066 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
2067 maybe!({
2068 let (worktree, entry) = self.selected_entry_handle(cx)?;
2069 let entry = entry.clone();
2070 let worktree_id = worktree.read(cx).id();
2071 let clipboard_entries = self
2072 .clipboard
2073 .as_ref()
2074 .filter(|clipboard| !clipboard.items().is_empty())?;
2075 enum PasteTask {
2076 Rename(Task<Result<CreatedEntry>>),
2077 Copy(Task<Result<Option<Entry>>>),
2078 }
2079 let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
2080 IndexMap::default();
2081 let mut disambiguation_range = None;
2082 let clip_is_cut = clipboard_entries.is_cut();
2083 for clipboard_entry in clipboard_entries.items() {
2084 let (new_path, new_disambiguation_range) =
2085 self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
2086 let clip_entry_id = clipboard_entry.entry_id;
2087 let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
2088 let relative_worktree_source_path = if !is_same_worktree {
2089 let target_base_path = worktree.read(cx).abs_path();
2090 let clipboard_project_path =
2091 self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
2092 let clipboard_abs_path = self
2093 .project
2094 .read(cx)
2095 .absolute_path(&clipboard_project_path, cx)?;
2096 Some(relativize_path(
2097 &target_base_path,
2098 clipboard_abs_path.as_path(),
2099 ))
2100 } else {
2101 None
2102 };
2103 let task = if clip_is_cut && is_same_worktree {
2104 let task = self.project.update(cx, |project, cx| {
2105 project.rename_entry(clip_entry_id, new_path, cx)
2106 });
2107 PasteTask::Rename(task)
2108 } else {
2109 let entry_id = if is_same_worktree {
2110 clip_entry_id
2111 } else {
2112 entry.id
2113 };
2114 let task = self.project.update(cx, |project, cx| {
2115 project.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
2116 });
2117 PasteTask::Copy(task)
2118 };
2119 let needs_delete = !is_same_worktree && clip_is_cut;
2120 paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
2121 disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2122 }
2123
2124 let item_count = paste_entry_tasks.len();
2125
2126 cx.spawn_in(window, async move |project_panel, cx| {
2127 let mut last_succeed = None;
2128 let mut need_delete_ids = Vec::new();
2129 for ((entry_id, need_delete), task) in paste_entry_tasks.into_iter() {
2130 match task {
2131 PasteTask::Rename(task) => {
2132 if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
2133 last_succeed = Some(entry);
2134 }
2135 }
2136 PasteTask::Copy(task) => {
2137 if let Some(Some(entry)) = task.await.log_err() {
2138 last_succeed = Some(entry);
2139 if need_delete {
2140 need_delete_ids.push(entry_id);
2141 }
2142 }
2143 }
2144 }
2145 }
2146 // remove entry for cut in difference worktree
2147 for entry_id in need_delete_ids {
2148 project_panel
2149 .update(cx, |project_panel, cx| {
2150 project_panel
2151 .project
2152 .update(cx, |project, cx| project.delete_entry(entry_id, true, cx))
2153 .ok_or_else(|| anyhow!("no such entry"))
2154 })??
2155 .await?;
2156 }
2157 // update selection
2158 if let Some(entry) = last_succeed {
2159 project_panel
2160 .update_in(cx, |project_panel, window, cx| {
2161 project_panel.selection = Some(SelectedEntry {
2162 worktree_id,
2163 entry_id: entry.id,
2164 });
2165
2166 if item_count == 1 {
2167 // open entry if not dir, and only focus if rename is not pending
2168 if !entry.is_dir() {
2169 project_panel.open_entry(
2170 entry.id,
2171 disambiguation_range.is_none(),
2172 false,
2173 cx,
2174 );
2175 }
2176
2177 // if only one entry was pasted and it was disambiguated, open the rename editor
2178 if disambiguation_range.is_some() {
2179 cx.defer_in(window, |this, window, cx| {
2180 this.rename_impl(disambiguation_range, window, cx);
2181 });
2182 }
2183 }
2184 })
2185 .ok();
2186 }
2187
2188 anyhow::Ok(())
2189 })
2190 .detach_and_log_err(cx);
2191
2192 self.expand_entry(worktree_id, entry.id, cx);
2193 Some(())
2194 });
2195 }
2196
2197 fn duplicate(&mut self, _: &Duplicate, window: &mut Window, cx: &mut Context<Self>) {
2198 self.copy(&Copy {}, window, cx);
2199 self.paste(&Paste {}, window, cx);
2200 }
2201
2202 fn copy_path(
2203 &mut self,
2204 _: &zed_actions::workspace::CopyPath,
2205 _: &mut Window,
2206 cx: &mut Context<Self>,
2207 ) {
2208 let abs_file_paths = {
2209 let project = self.project.read(cx);
2210 self.effective_entries()
2211 .into_iter()
2212 .filter_map(|entry| {
2213 let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
2214 Some(
2215 project
2216 .worktree_for_id(entry.worktree_id, cx)?
2217 .read(cx)
2218 .abs_path()
2219 .join(entry_path)
2220 .to_string_lossy()
2221 .to_string(),
2222 )
2223 })
2224 .collect::<Vec<_>>()
2225 };
2226 if !abs_file_paths.is_empty() {
2227 cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
2228 }
2229 }
2230
2231 fn copy_relative_path(
2232 &mut self,
2233 _: &zed_actions::workspace::CopyRelativePath,
2234 _: &mut Window,
2235 cx: &mut Context<Self>,
2236 ) {
2237 let file_paths = {
2238 let project = self.project.read(cx);
2239 self.effective_entries()
2240 .into_iter()
2241 .filter_map(|entry| {
2242 Some(
2243 project
2244 .path_for_entry(entry.entry_id, cx)?
2245 .path
2246 .to_string_lossy()
2247 .to_string(),
2248 )
2249 })
2250 .collect::<Vec<_>>()
2251 };
2252 if !file_paths.is_empty() {
2253 cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
2254 }
2255 }
2256
2257 fn reveal_in_finder(
2258 &mut self,
2259 _: &RevealInFileManager,
2260 _: &mut Window,
2261 cx: &mut Context<Self>,
2262 ) {
2263 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2264 cx.reveal_path(&worktree.read(cx).abs_path().join(&entry.path));
2265 }
2266 }
2267
2268 fn remove_from_project(
2269 &mut self,
2270 _: &RemoveFromProject,
2271 _window: &mut Window,
2272 cx: &mut Context<Self>,
2273 ) {
2274 for entry in self.effective_entries().iter() {
2275 let worktree_id = entry.worktree_id;
2276 self.project
2277 .update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
2278 }
2279 }
2280
2281 fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
2282 if let Some((worktree, entry)) = self.selected_entry(cx) {
2283 let abs_path = worktree.abs_path().join(&entry.path);
2284 cx.open_with_system(&abs_path);
2285 }
2286 }
2287
2288 fn open_in_terminal(
2289 &mut self,
2290 _: &OpenInTerminal,
2291 window: &mut Window,
2292 cx: &mut Context<Self>,
2293 ) {
2294 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2295 let abs_path = match &entry.canonical_path {
2296 Some(canonical_path) => Some(canonical_path.to_path_buf()),
2297 None => worktree.read(cx).absolutize(&entry.path).ok(),
2298 };
2299
2300 let working_directory = if entry.is_dir() {
2301 abs_path
2302 } else {
2303 abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
2304 };
2305 if let Some(working_directory) = working_directory {
2306 window.dispatch_action(
2307 workspace::OpenTerminal { working_directory }.boxed_clone(),
2308 cx,
2309 )
2310 }
2311 }
2312 }
2313
2314 pub fn new_search_in_directory(
2315 &mut self,
2316 _: &NewSearchInDirectory,
2317 window: &mut Window,
2318 cx: &mut Context<Self>,
2319 ) {
2320 if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
2321 let dir_path = if entry.is_dir() {
2322 entry.path.clone()
2323 } else {
2324 // entry is a file, use its parent directory
2325 match entry.path.parent() {
2326 Some(parent) => Arc::from(parent),
2327 None => {
2328 // File at root, open search with empty filter
2329 self.workspace
2330 .update(cx, |workspace, cx| {
2331 search::ProjectSearchView::new_search_in_directory(
2332 workspace,
2333 Path::new(""),
2334 window,
2335 cx,
2336 );
2337 })
2338 .ok();
2339 return;
2340 }
2341 }
2342 };
2343
2344 let include_root = self.project.read(cx).visible_worktrees(cx).count() > 1;
2345 let dir_path = if include_root {
2346 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
2347 full_path.push(&dir_path);
2348 Arc::from(full_path)
2349 } else {
2350 dir_path
2351 };
2352
2353 self.workspace
2354 .update(cx, |workspace, cx| {
2355 search::ProjectSearchView::new_search_in_directory(
2356 workspace, &dir_path, window, cx,
2357 );
2358 })
2359 .ok();
2360 }
2361 }
2362
2363 fn move_entry(
2364 &mut self,
2365 entry_to_move: ProjectEntryId,
2366 destination: ProjectEntryId,
2367 destination_is_file: bool,
2368 cx: &mut Context<Self>,
2369 ) {
2370 if self
2371 .project
2372 .read(cx)
2373 .entry_is_worktree_root(entry_to_move, cx)
2374 {
2375 self.move_worktree_root(entry_to_move, destination, cx)
2376 } else {
2377 self.move_worktree_entry(entry_to_move, destination, destination_is_file, cx)
2378 }
2379 }
2380
2381 fn move_worktree_root(
2382 &mut self,
2383 entry_to_move: ProjectEntryId,
2384 destination: ProjectEntryId,
2385 cx: &mut Context<Self>,
2386 ) {
2387 self.project.update(cx, |project, cx| {
2388 let Some(worktree_to_move) = project.worktree_for_entry(entry_to_move, cx) else {
2389 return;
2390 };
2391 let Some(destination_worktree) = project.worktree_for_entry(destination, cx) else {
2392 return;
2393 };
2394
2395 let worktree_id = worktree_to_move.read(cx).id();
2396 let destination_id = destination_worktree.read(cx).id();
2397
2398 project
2399 .move_worktree(worktree_id, destination_id, cx)
2400 .log_err();
2401 });
2402 }
2403
2404 fn move_worktree_entry(
2405 &mut self,
2406 entry_to_move: ProjectEntryId,
2407 destination: ProjectEntryId,
2408 destination_is_file: bool,
2409 cx: &mut Context<Self>,
2410 ) {
2411 if entry_to_move == destination {
2412 return;
2413 }
2414
2415 let destination_worktree = self.project.update(cx, |project, cx| {
2416 let entry_path = project.path_for_entry(entry_to_move, cx)?;
2417 let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
2418
2419 let mut destination_path = destination_entry_path.as_ref();
2420 if destination_is_file {
2421 destination_path = destination_path.parent()?;
2422 }
2423
2424 let mut new_path = destination_path.to_path_buf();
2425 new_path.push(entry_path.path.file_name()?);
2426 if new_path != entry_path.path.as_ref() {
2427 let task = project.rename_entry(entry_to_move, new_path, cx);
2428 cx.foreground_executor().spawn(task).detach_and_log_err(cx);
2429 }
2430
2431 project.worktree_id_for_entry(destination, cx)
2432 });
2433
2434 if let Some(destination_worktree) = destination_worktree {
2435 self.expand_entry(destination_worktree, destination, cx);
2436 }
2437 }
2438
2439 fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
2440 let mut entry_index = 0;
2441 let mut visible_entries_index = 0;
2442 for (worktree_index, (worktree_id, worktree_entries, _)) in
2443 self.visible_entries.iter().enumerate()
2444 {
2445 if *worktree_id == selection.worktree_id {
2446 for entry in worktree_entries {
2447 if entry.id == selection.entry_id {
2448 return Some((worktree_index, entry_index, visible_entries_index));
2449 } else {
2450 visible_entries_index += 1;
2451 entry_index += 1;
2452 }
2453 }
2454 break;
2455 } else {
2456 visible_entries_index += worktree_entries.len();
2457 }
2458 }
2459 None
2460 }
2461
2462 fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
2463 let marked_entries = self.effective_entries();
2464 let mut sanitized_entries = BTreeSet::new();
2465 if marked_entries.is_empty() {
2466 return sanitized_entries;
2467 }
2468
2469 let project = self.project.read(cx);
2470 let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
2471 .into_iter()
2472 .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
2473 .fold(HashMap::default(), |mut map, entry| {
2474 map.entry(entry.worktree_id).or_default().push(entry);
2475 map
2476 });
2477
2478 for (worktree_id, marked_entries) in marked_entries_by_worktree {
2479 if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
2480 let worktree = worktree.read(cx);
2481 let marked_dir_paths = marked_entries
2482 .iter()
2483 .filter_map(|entry| {
2484 worktree.entry_for_id(entry.entry_id).and_then(|entry| {
2485 if entry.is_dir() {
2486 Some(entry.path.as_ref())
2487 } else {
2488 None
2489 }
2490 })
2491 })
2492 .collect::<BTreeSet<_>>();
2493
2494 sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
2495 let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
2496 return false;
2497 };
2498 let entry_path = entry_info.path.as_ref();
2499 let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
2500 entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
2501 });
2502 !inside_marked_dir
2503 }));
2504 }
2505 }
2506
2507 sanitized_entries
2508 }
2509
2510 fn effective_entries(&self) -> BTreeSet<SelectedEntry> {
2511 if let Some(selection) = self.selection {
2512 let selection = SelectedEntry {
2513 entry_id: self.resolve_entry(selection.entry_id),
2514 worktree_id: selection.worktree_id,
2515 };
2516
2517 // Default to using just the selected item when nothing is marked.
2518 if self.marked_entries.is_empty() {
2519 return BTreeSet::from([selection]);
2520 }
2521
2522 // Allow operating on the selected item even when something else is marked,
2523 // making it easier to perform one-off actions without clearing a mark.
2524 if self.marked_entries.len() == 1 && !self.marked_entries.contains(&selection) {
2525 return BTreeSet::from([selection]);
2526 }
2527 }
2528
2529 // Return only marked entries since we've already handled special cases where
2530 // only selection should take precedence. At this point, marked entries may or
2531 // may not include the current selection, which is intentional.
2532 self.marked_entries
2533 .iter()
2534 .map(|entry| SelectedEntry {
2535 entry_id: self.resolve_entry(entry.entry_id),
2536 worktree_id: entry.worktree_id,
2537 })
2538 .collect::<BTreeSet<_>>()
2539 }
2540
2541 /// Finds the currently selected subentry for a given leaf entry id. If a given entry
2542 /// has no ancestors, the project entry ID that's passed in is returned as-is.
2543 fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
2544 self.ancestors
2545 .get(&id)
2546 .and_then(|ancestors| {
2547 if ancestors.current_ancestor_depth == 0 {
2548 return None;
2549 }
2550 ancestors.ancestors.get(ancestors.current_ancestor_depth)
2551 })
2552 .copied()
2553 .unwrap_or(id)
2554 }
2555
2556 pub fn selected_entry<'a>(&self, cx: &'a App) -> Option<(&'a Worktree, &'a project::Entry)> {
2557 let (worktree, entry) = self.selected_entry_handle(cx)?;
2558 Some((worktree.read(cx), entry))
2559 }
2560
2561 /// Compared to selected_entry, this function resolves to the currently
2562 /// selected subentry if dir auto-folding is enabled.
2563 fn selected_sub_entry<'a>(
2564 &self,
2565 cx: &'a App,
2566 ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2567 let (worktree, mut entry) = self.selected_entry_handle(cx)?;
2568
2569 let resolved_id = self.resolve_entry(entry.id);
2570 if resolved_id != entry.id {
2571 let worktree = worktree.read(cx);
2572 entry = worktree.entry_for_id(resolved_id)?;
2573 }
2574 Some((worktree, entry))
2575 }
2576 fn selected_entry_handle<'a>(
2577 &self,
2578 cx: &'a App,
2579 ) -> Option<(Entity<Worktree>, &'a project::Entry)> {
2580 let selection = self.selection?;
2581 let project = self.project.read(cx);
2582 let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
2583 let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
2584 Some((worktree, entry))
2585 }
2586
2587 fn expand_to_selection(&mut self, cx: &mut Context<Self>) -> Option<()> {
2588 let (worktree, entry) = self.selected_entry(cx)?;
2589 let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
2590
2591 for path in entry.path.ancestors() {
2592 let Some(entry) = worktree.entry_for_path(path) else {
2593 continue;
2594 };
2595 if entry.is_dir() {
2596 if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
2597 expanded_dir_ids.insert(idx, entry.id);
2598 }
2599 }
2600 }
2601
2602 Some(())
2603 }
2604
2605 fn update_visible_entries(
2606 &mut self,
2607 new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
2608 cx: &mut Context<Self>,
2609 ) {
2610 let settings = ProjectPanelSettings::get_global(cx);
2611 let auto_collapse_dirs = settings.auto_fold_dirs;
2612 let hide_gitignore = settings.hide_gitignore;
2613 let project = self.project.read(cx);
2614 let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
2615 self.last_worktree_root_id = project
2616 .visible_worktrees(cx)
2617 .next_back()
2618 .and_then(|worktree| worktree.read(cx).root_entry())
2619 .map(|entry| entry.id);
2620
2621 let old_ancestors = std::mem::take(&mut self.ancestors);
2622 self.visible_entries.clear();
2623 let mut max_width_item = None;
2624 for worktree in project.visible_worktrees(cx) {
2625 let worktree_snapshot = worktree.read(cx).snapshot();
2626 let worktree_id = worktree_snapshot.id();
2627
2628 let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
2629 hash_map::Entry::Occupied(e) => e.into_mut(),
2630 hash_map::Entry::Vacant(e) => {
2631 // The first time a worktree's root entry becomes available,
2632 // mark that root entry as expanded.
2633 if let Some(entry) = worktree_snapshot.root_entry() {
2634 e.insert(vec![entry.id]).as_slice()
2635 } else {
2636 &[]
2637 }
2638 }
2639 };
2640
2641 let mut new_entry_parent_id = None;
2642 let mut new_entry_kind = EntryKind::Dir;
2643 if let Some(edit_state) = &self.edit_state {
2644 if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() {
2645 new_entry_parent_id = Some(edit_state.entry_id);
2646 new_entry_kind = if edit_state.is_dir {
2647 EntryKind::Dir
2648 } else {
2649 EntryKind::File
2650 };
2651 }
2652 }
2653
2654 let mut visible_worktree_entries = Vec::new();
2655 let mut entry_iter =
2656 GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
2657 let mut auto_folded_ancestors = vec![];
2658 while let Some(entry) = entry_iter.entry() {
2659 if auto_collapse_dirs && entry.kind.is_dir() {
2660 auto_folded_ancestors.push(entry.id);
2661 if !self.unfolded_dir_ids.contains(&entry.id) {
2662 if let Some(root_path) = worktree_snapshot.root_entry() {
2663 let mut child_entries = worktree_snapshot.child_entries(&entry.path);
2664 if let Some(child) = child_entries.next() {
2665 if entry.path != root_path.path
2666 && child_entries.next().is_none()
2667 && child.kind.is_dir()
2668 {
2669 entry_iter.advance();
2670
2671 continue;
2672 }
2673 }
2674 }
2675 }
2676 let depth = old_ancestors
2677 .get(&entry.id)
2678 .map(|ancestor| ancestor.current_ancestor_depth)
2679 .unwrap_or_default()
2680 .min(auto_folded_ancestors.len());
2681 if let Some(edit_state) = &mut self.edit_state {
2682 if edit_state.entry_id == entry.id {
2683 edit_state.depth = depth;
2684 }
2685 }
2686 let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
2687 if ancestors.len() > 1 {
2688 ancestors.reverse();
2689 self.ancestors.insert(
2690 entry.id,
2691 FoldedAncestors {
2692 current_ancestor_depth: depth,
2693 ancestors,
2694 },
2695 );
2696 }
2697 }
2698 auto_folded_ancestors.clear();
2699 if !hide_gitignore || !entry.is_ignored {
2700 visible_worktree_entries.push(entry.to_owned());
2701 }
2702 let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
2703 entry.id == new_entry_id || {
2704 self.ancestors.get(&entry.id).map_or(false, |entries| {
2705 entries
2706 .ancestors
2707 .iter()
2708 .any(|entry_id| *entry_id == new_entry_id)
2709 })
2710 }
2711 } else {
2712 false
2713 };
2714 if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
2715 visible_worktree_entries.push(GitEntry {
2716 entry: Entry {
2717 id: NEW_ENTRY_ID,
2718 kind: new_entry_kind,
2719 path: entry.path.join("\0").into(),
2720 inode: 0,
2721 mtime: entry.mtime,
2722 size: entry.size,
2723 is_ignored: entry.is_ignored,
2724 is_external: false,
2725 is_private: false,
2726 is_always_included: entry.is_always_included,
2727 canonical_path: entry.canonical_path.clone(),
2728 char_bag: entry.char_bag,
2729 is_fifo: entry.is_fifo,
2730 },
2731 git_summary: entry.git_summary,
2732 });
2733 }
2734 let worktree_abs_path = worktree.read(cx).abs_path();
2735 let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
2736 let Some(path_name) = worktree_abs_path
2737 .file_name()
2738 .with_context(|| {
2739 format!("Worktree abs path has no file name, root entry: {entry:?}")
2740 })
2741 .log_err()
2742 else {
2743 continue;
2744 };
2745 let path = ArcCow::Borrowed(Path::new(path_name));
2746 let depth = 0;
2747 (depth, path)
2748 } else if entry.is_file() {
2749 let Some(path_name) = entry
2750 .path
2751 .file_name()
2752 .with_context(|| format!("Non-root entry has no file name: {entry:?}"))
2753 .log_err()
2754 else {
2755 continue;
2756 };
2757 let path = ArcCow::Borrowed(Path::new(path_name));
2758 let depth = entry.path.ancestors().count() - 1;
2759 (depth, path)
2760 } else {
2761 let path = self
2762 .ancestors
2763 .get(&entry.id)
2764 .and_then(|ancestors| {
2765 let outermost_ancestor = ancestors.ancestors.last()?;
2766 let root_folded_entry = worktree
2767 .read(cx)
2768 .entry_for_id(*outermost_ancestor)?
2769 .path
2770 .as_ref();
2771 entry
2772 .path
2773 .strip_prefix(root_folded_entry)
2774 .ok()
2775 .and_then(|suffix| {
2776 let full_path = Path::new(root_folded_entry.file_name()?);
2777 Some(ArcCow::Owned(Arc::<Path>::from(full_path.join(suffix))))
2778 })
2779 })
2780 .or_else(|| entry.path.file_name().map(Path::new).map(ArcCow::Borrowed))
2781 .unwrap_or_else(|| ArcCow::Owned(entry.path.clone()));
2782 let depth = path.components().count();
2783 (depth, path)
2784 };
2785 let width_estimate = item_width_estimate(
2786 depth,
2787 path.to_string_lossy().chars().count(),
2788 entry.canonical_path.is_some(),
2789 );
2790
2791 match max_width_item.as_mut() {
2792 Some((id, worktree_id, width)) => {
2793 if *width < width_estimate {
2794 *id = entry.id;
2795 *worktree_id = worktree.read(cx).id();
2796 *width = width_estimate;
2797 }
2798 }
2799 None => {
2800 max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
2801 }
2802 }
2803
2804 if expanded_dir_ids.binary_search(&entry.id).is_err()
2805 && entry_iter.advance_to_sibling()
2806 {
2807 continue;
2808 }
2809 entry_iter.advance();
2810 }
2811
2812 project::sort_worktree_entries(&mut visible_worktree_entries);
2813
2814 self.visible_entries
2815 .push((worktree_id, visible_worktree_entries, OnceCell::new()));
2816 }
2817
2818 if let Some((project_entry_id, worktree_id, _)) = max_width_item {
2819 let mut visited_worktrees_length = 0;
2820 let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
2821 if worktree_id == *id {
2822 entries
2823 .iter()
2824 .position(|entry| entry.id == project_entry_id)
2825 } else {
2826 visited_worktrees_length += entries.len();
2827 None
2828 }
2829 });
2830 if let Some(index) = index {
2831 self.max_width_item_index = Some(visited_worktrees_length + index);
2832 }
2833 }
2834 if let Some((worktree_id, entry_id)) = new_selected_entry {
2835 self.selection = Some(SelectedEntry {
2836 worktree_id,
2837 entry_id,
2838 });
2839 }
2840 }
2841
2842 fn expand_entry(
2843 &mut self,
2844 worktree_id: WorktreeId,
2845 entry_id: ProjectEntryId,
2846 cx: &mut Context<Self>,
2847 ) {
2848 self.project.update(cx, |project, cx| {
2849 if let Some((worktree, expanded_dir_ids)) = project
2850 .worktree_for_id(worktree_id, cx)
2851 .zip(self.expanded_dir_ids.get_mut(&worktree_id))
2852 {
2853 project.expand_entry(worktree_id, entry_id, cx);
2854 let worktree = worktree.read(cx);
2855
2856 if let Some(mut entry) = worktree.entry_for_id(entry_id) {
2857 loop {
2858 if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
2859 expanded_dir_ids.insert(ix, entry.id);
2860 }
2861
2862 if let Some(parent_entry) =
2863 entry.path.parent().and_then(|p| worktree.entry_for_path(p))
2864 {
2865 entry = parent_entry;
2866 } else {
2867 break;
2868 }
2869 }
2870 }
2871 }
2872 });
2873 }
2874
2875 fn drop_external_files(
2876 &mut self,
2877 paths: &[PathBuf],
2878 entry_id: ProjectEntryId,
2879 window: &mut Window,
2880 cx: &mut Context<Self>,
2881 ) {
2882 let mut paths: Vec<Arc<Path>> = paths.iter().map(|path| Arc::from(path.clone())).collect();
2883
2884 let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
2885
2886 let Some((target_directory, worktree)) = maybe!({
2887 let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
2888 let entry = worktree.read(cx).entry_for_id(entry_id)?;
2889 let path = worktree.read(cx).absolutize(&entry.path).ok()?;
2890 let target_directory = if path.is_dir() {
2891 path
2892 } else {
2893 path.parent()?.to_path_buf()
2894 };
2895 Some((target_directory, worktree))
2896 }) else {
2897 return;
2898 };
2899
2900 let mut paths_to_replace = Vec::new();
2901 for path in &paths {
2902 if let Some(name) = path.file_name() {
2903 let mut target_path = target_directory.clone();
2904 target_path.push(name);
2905 if target_path.exists() {
2906 paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
2907 }
2908 }
2909 }
2910
2911 cx.spawn_in(window, async move |this, cx| {
2912 async move {
2913 for (filename, original_path) in &paths_to_replace {
2914 let answer = cx.update(|window, cx| {
2915 window
2916 .prompt(
2917 PromptLevel::Info,
2918 format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
2919 None,
2920 &["Replace", "Cancel"],
2921 cx,
2922 )
2923 })?.await?;
2924
2925 if answer == 1 {
2926 if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
2927 paths.remove(item_idx);
2928 }
2929 }
2930 }
2931
2932 if paths.is_empty() {
2933 return Ok(());
2934 }
2935
2936 let task = worktree.update( cx, |worktree, cx| {
2937 worktree.copy_external_entries(target_directory, paths, true, cx)
2938 })?;
2939
2940 let opened_entries = task.await?;
2941 this.update(cx, |this, cx| {
2942 if open_file_after_drop && !opened_entries.is_empty() {
2943 this.open_entry(opened_entries[0], true, false, cx);
2944 }
2945 })
2946 }
2947 .log_err().await
2948 })
2949 .detach();
2950 }
2951
2952 fn drag_onto(
2953 &mut self,
2954 selections: &DraggedSelection,
2955 target_entry_id: ProjectEntryId,
2956 is_file: bool,
2957 window: &mut Window,
2958 cx: &mut Context<Self>,
2959 ) {
2960 let should_copy = window.modifiers().alt;
2961 if should_copy {
2962 let _ = maybe!({
2963 let project = self.project.read(cx);
2964 let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
2965 let worktree_id = target_worktree.read(cx).id();
2966 let target_entry = target_worktree
2967 .read(cx)
2968 .entry_for_id(target_entry_id)?
2969 .clone();
2970
2971 let mut copy_tasks = Vec::new();
2972 let mut disambiguation_range = None;
2973 for selection in selections.items() {
2974 let (new_path, new_disambiguation_range) = self.create_paste_path(
2975 selection,
2976 (target_worktree.clone(), &target_entry),
2977 cx,
2978 )?;
2979
2980 let task = self.project.update(cx, |project, cx| {
2981 project.copy_entry(selection.entry_id, None, new_path, cx)
2982 });
2983 copy_tasks.push(task);
2984 disambiguation_range = new_disambiguation_range.or(disambiguation_range);
2985 }
2986
2987 let item_count = copy_tasks.len();
2988
2989 cx.spawn_in(window, async move |project_panel, cx| {
2990 let mut last_succeed = None;
2991 for task in copy_tasks.into_iter() {
2992 if let Some(Some(entry)) = task.await.log_err() {
2993 last_succeed = Some(entry.id);
2994 }
2995 }
2996 // update selection
2997 if let Some(entry_id) = last_succeed {
2998 project_panel
2999 .update_in(cx, |project_panel, window, cx| {
3000 project_panel.selection = Some(SelectedEntry {
3001 worktree_id,
3002 entry_id,
3003 });
3004
3005 // if only one entry was dragged and it was disambiguated, open the rename editor
3006 if item_count == 1 && disambiguation_range.is_some() {
3007 project_panel.rename_impl(disambiguation_range, window, cx);
3008 }
3009 })
3010 .ok();
3011 }
3012 })
3013 .detach();
3014 Some(())
3015 });
3016 } else {
3017 for selection in selections.items() {
3018 self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
3019 }
3020 }
3021 }
3022
3023 fn index_for_entry(
3024 &self,
3025 entry_id: ProjectEntryId,
3026 worktree_id: WorktreeId,
3027 ) -> Option<(usize, usize, usize)> {
3028 let mut worktree_ix = 0;
3029 let mut total_ix = 0;
3030 for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3031 if worktree_id != *current_worktree_id {
3032 total_ix += visible_worktree_entries.len();
3033 worktree_ix += 1;
3034 continue;
3035 }
3036
3037 return visible_worktree_entries
3038 .iter()
3039 .enumerate()
3040 .find(|(_, entry)| entry.id == entry_id)
3041 .map(|(ix, _)| (worktree_ix, ix, total_ix + ix));
3042 }
3043 None
3044 }
3045
3046 fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
3047 let mut offset = 0;
3048 for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
3049 if visible_worktree_entries.len() > offset + index {
3050 return visible_worktree_entries
3051 .get(index)
3052 .map(|entry| (*worktree_id, entry.to_ref()));
3053 }
3054 offset += visible_worktree_entries.len();
3055 }
3056 None
3057 }
3058
3059 fn iter_visible_entries(
3060 &self,
3061 range: Range<usize>,
3062 window: &mut Window,
3063 cx: &mut Context<ProjectPanel>,
3064 mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
3065 ) {
3066 let mut ix = 0;
3067 for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
3068 if ix >= range.end {
3069 return;
3070 }
3071
3072 if ix + visible_worktree_entries.len() <= range.start {
3073 ix += visible_worktree_entries.len();
3074 continue;
3075 }
3076
3077 let end_ix = range.end.min(ix + visible_worktree_entries.len());
3078 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3079 let entries = entries_paths.get_or_init(|| {
3080 visible_worktree_entries
3081 .iter()
3082 .map(|e| (e.path.clone()))
3083 .collect()
3084 });
3085 for entry in visible_worktree_entries[entry_range].iter() {
3086 callback(&entry, entries, window, cx);
3087 }
3088 ix = end_ix;
3089 }
3090 }
3091
3092 fn for_each_visible_entry(
3093 &self,
3094 range: Range<usize>,
3095 window: &mut Window,
3096 cx: &mut Context<ProjectPanel>,
3097 mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context<ProjectPanel>),
3098 ) {
3099 let mut ix = 0;
3100 for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
3101 if ix >= range.end {
3102 return;
3103 }
3104
3105 if ix + visible_worktree_entries.len() <= range.start {
3106 ix += visible_worktree_entries.len();
3107 continue;
3108 }
3109
3110 let end_ix = range.end.min(ix + visible_worktree_entries.len());
3111 let (git_status_setting, show_file_icons, show_folder_icons) = {
3112 let settings = ProjectPanelSettings::get_global(cx);
3113 (
3114 settings.git_status,
3115 settings.file_icons,
3116 settings.folder_icons,
3117 )
3118 };
3119 if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
3120 let snapshot = worktree.read(cx).snapshot();
3121 let root_name = OsStr::new(snapshot.root_name());
3122 let expanded_entry_ids = self
3123 .expanded_dir_ids
3124 .get(&snapshot.id())
3125 .map(Vec::as_slice)
3126 .unwrap_or(&[]);
3127
3128 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
3129 let entries = entries_paths.get_or_init(|| {
3130 visible_worktree_entries
3131 .iter()
3132 .map(|e| (e.path.clone()))
3133 .collect()
3134 });
3135 for entry in visible_worktree_entries[entry_range].iter() {
3136 let status = git_status_setting
3137 .then_some(entry.git_summary)
3138 .unwrap_or_default();
3139 let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
3140 let icon = match entry.kind {
3141 EntryKind::File => {
3142 if show_file_icons {
3143 FileIcons::get_icon(&entry.path, cx)
3144 } else {
3145 None
3146 }
3147 }
3148 _ => {
3149 if show_folder_icons {
3150 FileIcons::get_folder_icon(is_expanded, cx)
3151 } else {
3152 FileIcons::get_chevron_icon(is_expanded, cx)
3153 }
3154 }
3155 };
3156
3157 let (depth, difference) =
3158 ProjectPanel::calculate_depth_and_difference(&entry, entries);
3159
3160 let filename = match difference {
3161 diff if diff > 1 => entry
3162 .path
3163 .iter()
3164 .skip(entry.path.components().count() - diff)
3165 .collect::<PathBuf>()
3166 .to_str()
3167 .unwrap_or_default()
3168 .to_string(),
3169 _ => entry
3170 .path
3171 .file_name()
3172 .map(|name| name.to_string_lossy().into_owned())
3173 .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
3174 };
3175 let selection = SelectedEntry {
3176 worktree_id: snapshot.id(),
3177 entry_id: entry.id,
3178 };
3179
3180 let is_marked = self.marked_entries.contains(&selection);
3181
3182 let diagnostic_severity = self
3183 .diagnostics
3184 .get(&(*worktree_id, entry.path.to_path_buf()))
3185 .cloned();
3186
3187 let filename_text_color =
3188 entry_git_aware_label_color(status, entry.is_ignored, is_marked);
3189
3190 let mut details = EntryDetails {
3191 filename,
3192 icon,
3193 path: entry.path.clone(),
3194 depth,
3195 kind: entry.kind,
3196 is_ignored: entry.is_ignored,
3197 is_expanded,
3198 is_selected: self.selection == Some(selection),
3199 is_marked,
3200 is_editing: false,
3201 is_processing: false,
3202 is_cut: self
3203 .clipboard
3204 .as_ref()
3205 .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
3206 filename_text_color,
3207 diagnostic_severity,
3208 git_status: status,
3209 is_private: entry.is_private,
3210 worktree_id: *worktree_id,
3211 canonical_path: entry.canonical_path.clone(),
3212 };
3213
3214 if let Some(edit_state) = &self.edit_state {
3215 let is_edited_entry = if edit_state.is_new_entry() {
3216 entry.id == NEW_ENTRY_ID
3217 } else {
3218 entry.id == edit_state.entry_id
3219 || self
3220 .ancestors
3221 .get(&entry.id)
3222 .is_some_and(|auto_folded_dirs| {
3223 auto_folded_dirs
3224 .ancestors
3225 .iter()
3226 .any(|entry_id| *entry_id == edit_state.entry_id)
3227 })
3228 };
3229
3230 if is_edited_entry {
3231 if let Some(processing_filename) = &edit_state.processing_filename {
3232 details.is_processing = true;
3233 if let Some(ancestors) = edit_state
3234 .leaf_entry_id
3235 .and_then(|entry| self.ancestors.get(&entry))
3236 {
3237 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;
3238 let all_components = ancestors.ancestors.len();
3239
3240 let prefix_components = all_components - position;
3241 let suffix_components = position.checked_sub(1);
3242 let mut previous_components =
3243 Path::new(&details.filename).components();
3244 let mut new_path = previous_components
3245 .by_ref()
3246 .take(prefix_components)
3247 .collect::<PathBuf>();
3248 if let Some(last_component) =
3249 Path::new(processing_filename).components().last()
3250 {
3251 new_path.push(last_component);
3252 previous_components.next();
3253 }
3254
3255 if let Some(_) = suffix_components {
3256 new_path.push(previous_components);
3257 }
3258 if let Some(str) = new_path.to_str() {
3259 details.filename.clear();
3260 details.filename.push_str(str);
3261 }
3262 } else {
3263 details.filename.clear();
3264 details.filename.push_str(processing_filename);
3265 }
3266 } else {
3267 if edit_state.is_new_entry() {
3268 details.filename.clear();
3269 }
3270 details.is_editing = true;
3271 }
3272 }
3273 }
3274
3275 callback(entry.id, details, window, cx);
3276 }
3277 }
3278 ix = end_ix;
3279 }
3280 }
3281
3282 fn find_entry_in_worktree(
3283 &self,
3284 worktree_id: WorktreeId,
3285 reverse_search: bool,
3286 only_visible_entries: bool,
3287 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3288 cx: &mut Context<Self>,
3289 ) -> Option<GitEntry> {
3290 if only_visible_entries {
3291 let entries = self
3292 .visible_entries
3293 .iter()
3294 .find_map(|(tree_id, entries, _)| {
3295 if worktree_id == *tree_id {
3296 Some(entries)
3297 } else {
3298 None
3299 }
3300 })?
3301 .clone();
3302
3303 return utils::ReversibleIterable::new(entries.iter(), reverse_search)
3304 .find(|ele| predicate(ele.to_ref(), worktree_id))
3305 .cloned();
3306 }
3307
3308 let repo_snapshots = self
3309 .project
3310 .read(cx)
3311 .git_store()
3312 .read(cx)
3313 .repo_snapshots(cx);
3314 let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
3315 worktree.update(cx, |tree, _| {
3316 utils::ReversibleIterable::new(
3317 GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
3318 reverse_search,
3319 )
3320 .find_single_ended(|ele| predicate(*ele, worktree_id))
3321 .map(|ele| ele.to_owned())
3322 })
3323 }
3324
3325 fn find_entry(
3326 &self,
3327 start: Option<&SelectedEntry>,
3328 reverse_search: bool,
3329 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3330 cx: &mut Context<Self>,
3331 ) -> Option<SelectedEntry> {
3332 let mut worktree_ids: Vec<_> = self
3333 .visible_entries
3334 .iter()
3335 .map(|(worktree_id, _, _)| *worktree_id)
3336 .collect();
3337 let repo_snapshots = self
3338 .project
3339 .read(cx)
3340 .git_store()
3341 .read(cx)
3342 .repo_snapshots(cx);
3343
3344 let mut last_found: Option<SelectedEntry> = None;
3345
3346 if let Some(start) = start {
3347 let worktree = self
3348 .project
3349 .read(cx)
3350 .worktree_for_id(start.worktree_id, cx)?;
3351
3352 let search = worktree.update(cx, |tree, _| {
3353 let entry = tree.entry_for_id(start.entry_id)?;
3354 let root_entry = tree.root_entry()?;
3355 let tree_id = tree.id();
3356
3357 let mut first_iter = GitTraversal::new(
3358 &repo_snapshots,
3359 tree.traverse_from_path(true, true, true, entry.path.as_ref()),
3360 );
3361
3362 if reverse_search {
3363 first_iter.next();
3364 }
3365
3366 let first = first_iter
3367 .enumerate()
3368 .take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
3369 .map(|(_, entry)| entry)
3370 .find(|ele| predicate(*ele, tree_id))
3371 .map(|ele| ele.to_owned());
3372
3373 let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
3374
3375 let second = if reverse_search {
3376 second_iter
3377 .take_until(|ele| ele.id == start.entry_id)
3378 .filter(|ele| predicate(*ele, tree_id))
3379 .last()
3380 .map(|ele| ele.to_owned())
3381 } else {
3382 second_iter
3383 .take_while(|ele| ele.id != start.entry_id)
3384 .filter(|ele| predicate(*ele, tree_id))
3385 .last()
3386 .map(|ele| ele.to_owned())
3387 };
3388
3389 if reverse_search {
3390 Some((second, first))
3391 } else {
3392 Some((first, second))
3393 }
3394 });
3395
3396 if let Some((first, second)) = search {
3397 let first = first.map(|entry| SelectedEntry {
3398 worktree_id: start.worktree_id,
3399 entry_id: entry.id,
3400 });
3401
3402 let second = second.map(|entry| SelectedEntry {
3403 worktree_id: start.worktree_id,
3404 entry_id: entry.id,
3405 });
3406
3407 if first.is_some() {
3408 return first;
3409 }
3410 last_found = second;
3411
3412 let idx = worktree_ids
3413 .iter()
3414 .enumerate()
3415 .find(|(_, ele)| **ele == start.worktree_id)
3416 .map(|(idx, _)| idx);
3417
3418 if let Some(idx) = idx {
3419 worktree_ids.rotate_left(idx + 1usize);
3420 worktree_ids.pop();
3421 }
3422 }
3423 }
3424
3425 for tree_id in worktree_ids.into_iter() {
3426 if let Some(found) =
3427 self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx)
3428 {
3429 return Some(SelectedEntry {
3430 worktree_id: tree_id,
3431 entry_id: found.id,
3432 });
3433 }
3434 }
3435
3436 last_found
3437 }
3438
3439 fn find_visible_entry(
3440 &self,
3441 start: Option<&SelectedEntry>,
3442 reverse_search: bool,
3443 predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
3444 cx: &mut Context<Self>,
3445 ) -> Option<SelectedEntry> {
3446 let mut worktree_ids: Vec<_> = self
3447 .visible_entries
3448 .iter()
3449 .map(|(worktree_id, _, _)| *worktree_id)
3450 .collect();
3451
3452 let mut last_found: Option<SelectedEntry> = None;
3453
3454 if let Some(start) = start {
3455 let entries = self
3456 .visible_entries
3457 .iter()
3458 .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id)
3459 .map(|(_, entries, _)| entries)?;
3460
3461 let mut start_idx = entries
3462 .iter()
3463 .enumerate()
3464 .find(|(_, ele)| ele.id == start.entry_id)
3465 .map(|(idx, _)| idx)?;
3466
3467 if reverse_search {
3468 start_idx = start_idx.saturating_add(1usize);
3469 }
3470
3471 let (left, right) = entries.split_at_checked(start_idx)?;
3472
3473 let (first_iter, second_iter) = if reverse_search {
3474 (
3475 utils::ReversibleIterable::new(left.iter(), reverse_search),
3476 utils::ReversibleIterable::new(right.iter(), reverse_search),
3477 )
3478 } else {
3479 (
3480 utils::ReversibleIterable::new(right.iter(), reverse_search),
3481 utils::ReversibleIterable::new(left.iter(), reverse_search),
3482 )
3483 };
3484
3485 let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3486 let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
3487
3488 if first_search.is_some() {
3489 return first_search.map(|entry| SelectedEntry {
3490 worktree_id: start.worktree_id,
3491 entry_id: entry.id,
3492 });
3493 }
3494
3495 last_found = second_search.map(|entry| SelectedEntry {
3496 worktree_id: start.worktree_id,
3497 entry_id: entry.id,
3498 });
3499
3500 let idx = worktree_ids
3501 .iter()
3502 .enumerate()
3503 .find(|(_, ele)| **ele == start.worktree_id)
3504 .map(|(idx, _)| idx);
3505
3506 if let Some(idx) = idx {
3507 worktree_ids.rotate_left(idx + 1usize);
3508 worktree_ids.pop();
3509 }
3510 }
3511
3512 for tree_id in worktree_ids.into_iter() {
3513 if let Some(found) =
3514 self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx)
3515 {
3516 return Some(SelectedEntry {
3517 worktree_id: tree_id,
3518 entry_id: found.id,
3519 });
3520 }
3521 }
3522
3523 last_found
3524 }
3525
3526 fn calculate_depth_and_difference(
3527 entry: &Entry,
3528 visible_worktree_entries: &HashSet<Arc<Path>>,
3529 ) -> (usize, usize) {
3530 let (depth, difference) = entry
3531 .path
3532 .ancestors()
3533 .skip(1) // Skip the entry itself
3534 .find_map(|ancestor| {
3535 if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
3536 let entry_path_components_count = entry.path.components().count();
3537 let parent_path_components_count = parent_entry.components().count();
3538 let difference = entry_path_components_count - parent_path_components_count;
3539 let depth = parent_entry
3540 .ancestors()
3541 .skip(1)
3542 .filter(|ancestor| visible_worktree_entries.contains(*ancestor))
3543 .count();
3544 Some((depth + 1, difference))
3545 } else {
3546 None
3547 }
3548 })
3549 .unwrap_or((0, 0));
3550
3551 (depth, difference)
3552 }
3553
3554 fn render_entry(
3555 &self,
3556 entry_id: ProjectEntryId,
3557 details: EntryDetails,
3558 window: &mut Window,
3559 cx: &mut Context<Self>,
3560 ) -> Stateful<Div> {
3561 const GROUP_NAME: &str = "project_entry";
3562
3563 let kind = details.kind;
3564 let settings = ProjectPanelSettings::get_global(cx);
3565 let show_editor = details.is_editing && !details.is_processing;
3566
3567 let selection = SelectedEntry {
3568 worktree_id: details.worktree_id,
3569 entry_id,
3570 };
3571
3572 let is_marked = self.marked_entries.contains(&selection);
3573 let is_active = self
3574 .selection
3575 .map_or(false, |selection| selection.entry_id == entry_id);
3576
3577 let file_name = details.filename.clone();
3578
3579 let mut icon = details.icon.clone();
3580 if settings.file_icons && show_editor && details.kind.is_file() {
3581 let filename = self.filename_editor.read(cx).text(cx);
3582 if filename.len() > 2 {
3583 icon = FileIcons::get_icon(Path::new(&filename), cx);
3584 }
3585 }
3586
3587 let filename_text_color = details.filename_text_color;
3588 let diagnostic_severity = details.diagnostic_severity;
3589 let item_colors = get_item_color(cx);
3590
3591 let canonical_path = details
3592 .canonical_path
3593 .as_ref()
3594 .map(|f| f.to_string_lossy().to_string());
3595 let path = details.path.clone();
3596
3597 let depth = details.depth;
3598 let worktree_id = details.worktree_id;
3599 let selections = Arc::new(self.marked_entries.clone());
3600 let is_local = self.project.read(cx).is_local();
3601
3602 let dragged_selection = DraggedSelection {
3603 active_selection: selection,
3604 marked_selections: selections,
3605 };
3606
3607 let bg_color = if is_marked {
3608 item_colors.marked
3609 } else {
3610 item_colors.default
3611 };
3612
3613 let bg_hover_color = if is_marked {
3614 item_colors.marked
3615 } else {
3616 item_colors.hover
3617 };
3618
3619 let border_color =
3620 if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3621 item_colors.focused
3622 } else {
3623 bg_color
3624 };
3625
3626 let border_hover_color =
3627 if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
3628 item_colors.focused
3629 } else {
3630 bg_hover_color
3631 };
3632
3633 let folded_directory_drag_target = self.folded_directory_drag_target;
3634
3635 div()
3636 .id(entry_id.to_proto() as usize)
3637 .group(GROUP_NAME)
3638 .cursor_pointer()
3639 .rounded_none()
3640 .bg(bg_color)
3641 .border_1()
3642 .border_r_2()
3643 .border_color(border_color)
3644 .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
3645 .when(is_local, |div| {
3646 div.on_drag_move::<ExternalPaths>(cx.listener(
3647 move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
3648 if event.bounds.contains(&event.event.position) {
3649 if this.last_external_paths_drag_over_entry == Some(entry_id) {
3650 return;
3651 }
3652 this.last_external_paths_drag_over_entry = Some(entry_id);
3653 this.marked_entries.clear();
3654
3655 let Some((worktree, path, entry)) = maybe!({
3656 let worktree = this
3657 .project
3658 .read(cx)
3659 .worktree_for_id(selection.worktree_id, cx)?;
3660 let worktree = worktree.read(cx);
3661 let abs_path = worktree.absolutize(&path).log_err()?;
3662 let path = if abs_path.is_dir() {
3663 path.as_ref()
3664 } else {
3665 path.parent()?
3666 };
3667 let entry = worktree.entry_for_path(path)?;
3668 Some((worktree, path, entry))
3669 }) else {
3670 return;
3671 };
3672
3673 this.marked_entries.insert(SelectedEntry {
3674 entry_id: entry.id,
3675 worktree_id: worktree.id(),
3676 });
3677
3678 for entry in worktree.child_entries(path) {
3679 this.marked_entries.insert(SelectedEntry {
3680 entry_id: entry.id,
3681 worktree_id: worktree.id(),
3682 });
3683 }
3684
3685 cx.notify();
3686 }
3687 },
3688 ))
3689 .on_drop(cx.listener(
3690 move |this, external_paths: &ExternalPaths, window, cx| {
3691 this.hover_scroll_task.take();
3692 this.last_external_paths_drag_over_entry = None;
3693 this.marked_entries.clear();
3694 this.drop_external_files(external_paths.paths(), entry_id, window, cx);
3695 cx.stop_propagation();
3696 },
3697 ))
3698 })
3699 .on_drag_move::<DraggedSelection>(cx.listener(
3700 move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
3701 if event.bounds.contains(&event.event.position) {
3702 if this.last_selection_drag_over_entry == Some(entry_id) {
3703 return;
3704 }
3705 this.last_selection_drag_over_entry = Some(entry_id);
3706 this.hover_expand_task.take();
3707
3708 if !kind.is_dir()
3709 || this
3710 .expanded_dir_ids
3711 .get(&details.worktree_id)
3712 .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
3713 {
3714 return;
3715 }
3716
3717 let bounds = event.bounds;
3718 this.hover_expand_task =
3719 Some(cx.spawn_in(window, async move |this, cx| {
3720 cx.background_executor()
3721 .timer(Duration::from_millis(500))
3722 .await;
3723 this.update_in(cx, |this, window, cx| {
3724 this.hover_expand_task.take();
3725 if this.last_selection_drag_over_entry == Some(entry_id)
3726 && bounds.contains(&window.mouse_position())
3727 {
3728 this.expand_entry(worktree_id, entry_id, cx);
3729 this.update_visible_entries(
3730 Some((worktree_id, entry_id)),
3731 cx,
3732 );
3733 cx.notify();
3734 }
3735 })
3736 .ok();
3737 }));
3738 }
3739 },
3740 ))
3741 .on_drag(
3742 dragged_selection,
3743 move |selection, click_offset, _window, cx| {
3744 cx.new(|_| DraggedProjectEntryView {
3745 details: details.clone(),
3746 click_offset,
3747 selection: selection.active_selection,
3748 selections: selection.marked_selections.clone(),
3749 })
3750 },
3751 )
3752 .drag_over::<DraggedSelection>(move |style, _, _, _| {
3753 if folded_directory_drag_target.is_some() {
3754 return style;
3755 }
3756 style.bg(item_colors.drag_over)
3757 })
3758 .on_drop(
3759 cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3760 this.hover_scroll_task.take();
3761 this.hover_expand_task.take();
3762 if folded_directory_drag_target.is_some() {
3763 return;
3764 }
3765 this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
3766 }),
3767 )
3768 .on_mouse_down(
3769 MouseButton::Left,
3770 cx.listener(move |this, _, _, cx| {
3771 this.mouse_down = true;
3772 cx.propagate();
3773 }),
3774 )
3775 .on_click(
3776 cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
3777 if event.down.button == MouseButton::Right
3778 || event.down.first_mouse
3779 || show_editor
3780 {
3781 return;
3782 }
3783 if event.down.button == MouseButton::Left {
3784 this.mouse_down = false;
3785 }
3786 cx.stop_propagation();
3787
3788 if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
3789 let current_selection = this.index_for_selection(selection);
3790 let clicked_entry = SelectedEntry {
3791 entry_id,
3792 worktree_id,
3793 };
3794 let target_selection = this.index_for_selection(clicked_entry);
3795 if let Some(((_, _, source_index), (_, _, target_index))) =
3796 current_selection.zip(target_selection)
3797 {
3798 let range_start = source_index.min(target_index);
3799 let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
3800 let mut new_selections = BTreeSet::new();
3801 this.for_each_visible_entry(
3802 range_start..range_end,
3803 window,
3804 cx,
3805 |entry_id, details, _, _| {
3806 new_selections.insert(SelectedEntry {
3807 entry_id,
3808 worktree_id: details.worktree_id,
3809 });
3810 },
3811 );
3812
3813 this.marked_entries = this
3814 .marked_entries
3815 .union(&new_selections)
3816 .cloned()
3817 .collect();
3818
3819 this.selection = Some(clicked_entry);
3820 this.marked_entries.insert(clicked_entry);
3821 }
3822 } else if event.modifiers().secondary() {
3823 if event.down.click_count > 1 {
3824 this.split_entry(entry_id, cx);
3825 } else {
3826 this.selection = Some(selection);
3827 if !this.marked_entries.insert(selection) {
3828 this.marked_entries.remove(&selection);
3829 }
3830 }
3831 } else if kind.is_dir() {
3832 this.marked_entries.clear();
3833 if event.modifiers().alt {
3834 this.toggle_expand_all(entry_id, window, cx);
3835 } else {
3836 this.toggle_expanded(entry_id, window, cx);
3837 }
3838 } else {
3839 let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
3840 let click_count = event.up.click_count;
3841 let focus_opened_item = !preview_tabs_enabled || click_count > 1;
3842 let allow_preview = preview_tabs_enabled && click_count == 1;
3843 this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
3844 }
3845 }),
3846 )
3847 .child(
3848 ListItem::new(entry_id.to_proto() as usize)
3849 .indent_level(depth)
3850 .indent_step_size(px(settings.indent_size))
3851 .spacing(match settings.entry_spacing {
3852 project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
3853 project_panel_settings::EntrySpacing::Standard => {
3854 ListItemSpacing::ExtraDense
3855 }
3856 })
3857 .selectable(false)
3858 .when_some(canonical_path, |this, path| {
3859 this.end_slot::<AnyElement>(
3860 div()
3861 .id("symlink_icon")
3862 .pr_3()
3863 .tooltip(move |window, cx| {
3864 Tooltip::with_meta(
3865 path.to_string(),
3866 None,
3867 "Symbolic Link",
3868 window,
3869 cx,
3870 )
3871 })
3872 .child(
3873 Icon::new(IconName::ArrowUpRight)
3874 .size(IconSize::Indicator)
3875 .color(filename_text_color),
3876 )
3877 .into_any_element(),
3878 )
3879 })
3880 .child(if let Some(icon) = &icon {
3881 if let Some((_, decoration_color)) =
3882 entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity)
3883 {
3884 let is_warning = diagnostic_severity
3885 .map(|severity| matches!(severity, DiagnosticSeverity::WARNING))
3886 .unwrap_or(false);
3887 div().child(
3888 DecoratedIcon::new(
3889 Icon::from_path(icon.clone()).color(Color::Muted),
3890 Some(
3891 IconDecoration::new(
3892 if kind.is_file() {
3893 if is_warning {
3894 IconDecorationKind::Triangle
3895 } else {
3896 IconDecorationKind::X
3897 }
3898 } else {
3899 IconDecorationKind::Dot
3900 },
3901 bg_color,
3902 cx,
3903 )
3904 .group_name(Some(GROUP_NAME.into()))
3905 .knockout_hover_color(bg_hover_color)
3906 .color(decoration_color.color(cx))
3907 .position(Point {
3908 x: px(-2.),
3909 y: px(-2.),
3910 }),
3911 ),
3912 )
3913 .into_any_element(),
3914 )
3915 } else {
3916 h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted))
3917 }
3918 } else {
3919 if let Some((icon_name, color)) =
3920 entry_diagnostic_aware_icon_name_and_color(diagnostic_severity)
3921 {
3922 h_flex()
3923 .size(IconSize::default().rems())
3924 .child(Icon::new(icon_name).color(color).size(IconSize::Small))
3925 } else {
3926 h_flex()
3927 .size(IconSize::default().rems())
3928 .invisible()
3929 .flex_none()
3930 }
3931 })
3932 .child(
3933 if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
3934 h_flex().h_6().w_full().child(editor.clone())
3935 } else {
3936 h_flex().h_6().map(|mut this| {
3937 if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
3938 let components = Path::new(&file_name)
3939 .components()
3940 .map(|comp| {
3941 let comp_str =
3942 comp.as_os_str().to_string_lossy().into_owned();
3943 comp_str
3944 })
3945 .collect::<Vec<_>>();
3946
3947 let components_len = components.len();
3948 let active_index = components_len
3949 - 1
3950 - folded_ancestors.current_ancestor_depth;
3951 const DELIMITER: SharedString =
3952 SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
3953 for (index, component) in components.into_iter().enumerate() {
3954 if index != 0 {
3955 let delimiter_target_index = index - 1;
3956 let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
3957 this = this.child(
3958 div()
3959 .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
3960 this.hover_scroll_task.take();
3961 this.folded_directory_drag_target = None;
3962 if let Some(target_entry_id) = target_entry_id {
3963 this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
3964 }
3965 }))
3966 .on_drag_move(cx.listener(
3967 move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
3968 if event.bounds.contains(&event.event.position) {
3969 this.folded_directory_drag_target = Some(
3970 FoldedDirectoryDragTarget {
3971 entry_id,
3972 index: delimiter_target_index,
3973 is_delimiter_target: true,
3974 }
3975 );
3976 } else {
3977 let is_current_target = this.folded_directory_drag_target
3978 .map_or(false, |target|
3979 target.entry_id == entry_id &&
3980 target.index == delimiter_target_index &&
3981 target.is_delimiter_target
3982 );
3983 if is_current_target {
3984 this.folded_directory_drag_target = None;
3985 }
3986 }
3987
3988 },
3989 ))
3990 .child(
3991 Label::new(DELIMITER.clone())
3992 .single_line()
3993 .color(filename_text_color)
3994 )
3995 );
3996 }
3997 let id = SharedString::from(format!(
3998 "project_panel_path_component_{}_{index}",
3999 entry_id.to_usize()
4000 ));
4001 let label = div()
4002 .id(id)
4003 .on_click(cx.listener(move |this, _, _, cx| {
4004 if index != active_index {
4005 if let Some(folds) =
4006 this.ancestors.get_mut(&entry_id)
4007 {
4008 folds.current_ancestor_depth =
4009 components_len - 1 - index;
4010 cx.notify();
4011 }
4012 }
4013 }))
4014 .when(index != components_len - 1, |div|{
4015 let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
4016 div
4017 .on_drag_move(cx.listener(
4018 move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
4019 if event.bounds.contains(&event.event.position) {
4020 this.folded_directory_drag_target = Some(
4021 FoldedDirectoryDragTarget {
4022 entry_id,
4023 index,
4024 is_delimiter_target: false,
4025 }
4026 );
4027 } else {
4028 let is_current_target = this.folded_directory_drag_target
4029 .as_ref()
4030 .map_or(false, |target|
4031 target.entry_id == entry_id &&
4032 target.index == index &&
4033 !target.is_delimiter_target
4034 );
4035 if is_current_target {
4036 this.folded_directory_drag_target = None;
4037 }
4038 }
4039 },
4040 ))
4041 .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
4042 this.hover_scroll_task.take();
4043 this.folded_directory_drag_target = None;
4044 if let Some(target_entry_id) = target_entry_id {
4045 this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
4046 }
4047 }))
4048 .when(folded_directory_drag_target.map_or(false, |target|
4049 target.entry_id == entry_id &&
4050 target.index == index
4051 ), |this| {
4052 this.bg(item_colors.drag_over)
4053 })
4054 })
4055 .child(
4056 Label::new(component)
4057 .single_line()
4058 .color(filename_text_color)
4059 .when(
4060 index == active_index
4061 && (is_active || is_marked),
4062 |this| this.underline(),
4063 ),
4064 );
4065
4066 this = this.child(label);
4067 }
4068
4069 this
4070 } else {
4071 this.child(
4072 Label::new(file_name)
4073 .single_line()
4074 .color(filename_text_color),
4075 )
4076 }
4077 })
4078 }
4079 .ml_1(),
4080 )
4081 .on_secondary_mouse_down(cx.listener(
4082 move |this, event: &MouseDownEvent, window, cx| {
4083 // Stop propagation to prevent the catch-all context menu for the project
4084 // panel from being deployed.
4085 cx.stop_propagation();
4086 // Some context menu actions apply to all marked entries. If the user
4087 // right-clicks on an entry that is not marked, they may not realize the
4088 // action applies to multiple entries. To avoid inadvertent changes, all
4089 // entries are unmarked.
4090 if !this.marked_entries.contains(&selection) {
4091 this.marked_entries.clear();
4092 }
4093 this.deploy_context_menu(event.position, entry_id, window, cx);
4094 },
4095 ))
4096 .overflow_x(),
4097 )
4098 }
4099
4100 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4101 if !Self::should_show_scrollbar(cx)
4102 || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4103 {
4104 return None;
4105 }
4106 Some(
4107 div()
4108 .occlude()
4109 .id("project-panel-vertical-scroll")
4110 .on_mouse_move(cx.listener(|_, _, _, cx| {
4111 cx.notify();
4112 cx.stop_propagation()
4113 }))
4114 .on_hover(|_, _, cx| {
4115 cx.stop_propagation();
4116 })
4117 .on_any_mouse_down(|_, _, cx| {
4118 cx.stop_propagation();
4119 })
4120 .on_mouse_up(
4121 MouseButton::Left,
4122 cx.listener(|this, _, window, cx| {
4123 if !this.vertical_scrollbar_state.is_dragging()
4124 && !this.focus_handle.contains_focused(window, cx)
4125 {
4126 this.hide_scrollbar(window, cx);
4127 cx.notify();
4128 }
4129
4130 cx.stop_propagation();
4131 }),
4132 )
4133 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4134 cx.notify();
4135 }))
4136 .h_full()
4137 .absolute()
4138 .right_1()
4139 .top_1()
4140 .bottom_1()
4141 .w(px(12.))
4142 .cursor_default()
4143 .children(Scrollbar::vertical(
4144 // percentage as f32..end_offset as f32,
4145 self.vertical_scrollbar_state.clone(),
4146 )),
4147 )
4148 }
4149
4150 fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4151 if !Self::should_show_scrollbar(cx)
4152 || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4153 {
4154 return None;
4155 }
4156
4157 let scroll_handle = self.scroll_handle.0.borrow();
4158 let longest_item_width = scroll_handle
4159 .last_item_size
4160 .filter(|size| size.contents.width > size.item.width)?
4161 .contents
4162 .width
4163 .0 as f64;
4164 if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4165 return None;
4166 }
4167
4168 Some(
4169 div()
4170 .occlude()
4171 .id("project-panel-horizontal-scroll")
4172 .on_mouse_move(cx.listener(|_, _, _, cx| {
4173 cx.notify();
4174 cx.stop_propagation()
4175 }))
4176 .on_hover(|_, _, cx| {
4177 cx.stop_propagation();
4178 })
4179 .on_any_mouse_down(|_, _, cx| {
4180 cx.stop_propagation();
4181 })
4182 .on_mouse_up(
4183 MouseButton::Left,
4184 cx.listener(|this, _, window, cx| {
4185 if !this.horizontal_scrollbar_state.is_dragging()
4186 && !this.focus_handle.contains_focused(window, cx)
4187 {
4188 this.hide_scrollbar(window, cx);
4189 cx.notify();
4190 }
4191
4192 cx.stop_propagation();
4193 }),
4194 )
4195 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4196 cx.notify();
4197 }))
4198 .w_full()
4199 .absolute()
4200 .right_1()
4201 .left_1()
4202 .bottom_1()
4203 .h(px(12.))
4204 .cursor_default()
4205 .when(self.width.is_some(), |this| {
4206 this.children(Scrollbar::horizontal(
4207 self.horizontal_scrollbar_state.clone(),
4208 ))
4209 }),
4210 )
4211 }
4212
4213 fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
4214 let mut dispatch_context = KeyContext::new_with_defaults();
4215 dispatch_context.add("ProjectPanel");
4216 dispatch_context.add("menu");
4217
4218 let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) {
4219 "editing"
4220 } else {
4221 "not_editing"
4222 };
4223
4224 dispatch_context.add(identifier);
4225 dispatch_context
4226 }
4227
4228 fn should_show_scrollbar(cx: &App) -> bool {
4229 let show = ProjectPanelSettings::get_global(cx)
4230 .scrollbar
4231 .show
4232 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4233 match show {
4234 ShowScrollbar::Auto => true,
4235 ShowScrollbar::System => true,
4236 ShowScrollbar::Always => true,
4237 ShowScrollbar::Never => false,
4238 }
4239 }
4240
4241 fn should_autohide_scrollbar(cx: &App) -> bool {
4242 let show = ProjectPanelSettings::get_global(cx)
4243 .scrollbar
4244 .show
4245 .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4246 match show {
4247 ShowScrollbar::Auto => true,
4248 ShowScrollbar::System => cx
4249 .try_global::<ScrollbarAutoHide>()
4250 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4251 ShowScrollbar::Always => false,
4252 ShowScrollbar::Never => true,
4253 }
4254 }
4255
4256 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4257 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4258 if !Self::should_autohide_scrollbar(cx) {
4259 return;
4260 }
4261 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4262 cx.background_executor()
4263 .timer(SCROLLBAR_SHOW_INTERVAL)
4264 .await;
4265 panel
4266 .update(cx, |panel, cx| {
4267 panel.show_scrollbar = false;
4268 cx.notify();
4269 })
4270 .log_err();
4271 }))
4272 }
4273
4274 fn reveal_entry(
4275 &mut self,
4276 project: Entity<Project>,
4277 entry_id: ProjectEntryId,
4278 skip_ignored: bool,
4279 cx: &mut Context<Self>,
4280 ) {
4281 if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
4282 let worktree = worktree.read(cx);
4283 if skip_ignored
4284 && worktree
4285 .entry_for_id(entry_id)
4286 .map_or(true, |entry| entry.is_ignored && !entry.is_always_included)
4287 {
4288 return;
4289 }
4290
4291 let worktree_id = worktree.id();
4292 self.expand_entry(worktree_id, entry_id, cx);
4293 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
4294 self.marked_entries.clear();
4295 self.marked_entries.insert(SelectedEntry {
4296 worktree_id,
4297 entry_id,
4298 });
4299 self.autoscroll(cx);
4300 cx.notify();
4301 }
4302 }
4303
4304 fn find_active_indent_guide(
4305 &self,
4306 indent_guides: &[IndentGuideLayout],
4307 cx: &App,
4308 ) -> Option<usize> {
4309 let (worktree, entry) = self.selected_entry(cx)?;
4310
4311 // Find the parent entry of the indent guide, this will either be the
4312 // expanded folder we have selected, or the parent of the currently
4313 // selected file/collapsed directory
4314 let mut entry = entry;
4315 loop {
4316 let is_expanded_dir = entry.is_dir()
4317 && self
4318 .expanded_dir_ids
4319 .get(&worktree.id())
4320 .map(|ids| ids.binary_search(&entry.id).is_ok())
4321 .unwrap_or(false);
4322 if is_expanded_dir {
4323 break;
4324 }
4325 entry = worktree.entry_for_path(&entry.path.parent()?)?;
4326 }
4327
4328 let (active_indent_range, depth) = {
4329 let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?;
4330 let child_paths = &self.visible_entries[worktree_ix].1;
4331 let mut child_count = 0;
4332 let depth = entry.path.ancestors().count();
4333 while let Some(entry) = child_paths.get(child_offset + child_count + 1) {
4334 if entry.path.ancestors().count() <= depth {
4335 break;
4336 }
4337 child_count += 1;
4338 }
4339
4340 let start = ix + 1;
4341 let end = start + child_count;
4342
4343 let (_, entries, paths) = &self.visible_entries[worktree_ix];
4344 let visible_worktree_entries =
4345 paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect());
4346
4347 // Calculate the actual depth of the entry, taking into account that directories can be auto-folded.
4348 let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries);
4349 (start..end, depth)
4350 };
4351
4352 let candidates = indent_guides
4353 .iter()
4354 .enumerate()
4355 .filter(|(_, indent_guide)| indent_guide.offset.x == depth);
4356
4357 for (i, indent) in candidates {
4358 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
4359 if active_indent_range.start <= indent.offset.y + indent.length
4360 && indent.offset.y <= active_indent_range.end
4361 {
4362 return Some(i);
4363 }
4364 }
4365 None
4366 }
4367}
4368
4369fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
4370 const ICON_SIZE_FACTOR: usize = 2;
4371 let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
4372 if is_symlink {
4373 item_width += ICON_SIZE_FACTOR;
4374 }
4375 item_width
4376}
4377
4378impl Render for ProjectPanel {
4379 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4380 let has_worktree = !self.visible_entries.is_empty();
4381 let project = self.project.read(cx);
4382 let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
4383 let show_indent_guides =
4384 ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
4385 let is_local = project.is_local();
4386
4387 if has_worktree {
4388 let item_count = self
4389 .visible_entries
4390 .iter()
4391 .map(|(_, worktree_entries, _)| worktree_entries.len())
4392 .sum();
4393
4394 fn handle_drag_move_scroll<T: 'static>(
4395 this: &mut ProjectPanel,
4396 e: &DragMoveEvent<T>,
4397 window: &mut Window,
4398 cx: &mut Context<ProjectPanel>,
4399 ) {
4400 if !e.bounds.contains(&e.event.position) {
4401 return;
4402 }
4403 this.hover_scroll_task.take();
4404 let panel_height = e.bounds.size.height;
4405 if panel_height <= px(0.) {
4406 return;
4407 }
4408
4409 let event_offset = e.event.position.y - e.bounds.origin.y;
4410 // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom)
4411 let hovered_region_offset = event_offset / panel_height;
4412
4413 // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list.
4414 // These pixels offsets were picked arbitrarily.
4415 let vertical_scroll_offset = if hovered_region_offset <= 0.05 {
4416 8.
4417 } else if hovered_region_offset <= 0.15 {
4418 5.
4419 } else if hovered_region_offset >= 0.95 {
4420 -8.
4421 } else if hovered_region_offset >= 0.85 {
4422 -5.
4423 } else {
4424 return;
4425 };
4426 let adjustment = point(px(0.), px(vertical_scroll_offset));
4427 this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| loop {
4428 let should_stop_scrolling = this
4429 .update(cx, |this, cx| {
4430 this.hover_scroll_task.as_ref()?;
4431 let handle = this.scroll_handle.0.borrow_mut();
4432 let offset = handle.base_handle.offset();
4433
4434 handle.base_handle.set_offset(offset + adjustment);
4435 cx.notify();
4436 Some(())
4437 })
4438 .ok()
4439 .flatten()
4440 .is_some();
4441 if should_stop_scrolling {
4442 return;
4443 }
4444 cx.background_executor()
4445 .timer(Duration::from_millis(16))
4446 .await;
4447 }));
4448 }
4449 h_flex()
4450 .id("project-panel")
4451 .group("project-panel")
4452 .on_drag_move(cx.listener(handle_drag_move_scroll::<ExternalPaths>))
4453 .on_drag_move(cx.listener(handle_drag_move_scroll::<DraggedSelection>))
4454 .size_full()
4455 .relative()
4456 .on_hover(cx.listener(|this, hovered, window, cx| {
4457 if *hovered {
4458 this.show_scrollbar = true;
4459 this.hide_scrollbar_task.take();
4460 cx.notify();
4461 } else if !this.focus_handle.contains_focused(window, cx) {
4462 this.hide_scrollbar(window, cx);
4463 }
4464 }))
4465 .on_click(cx.listener(|this, _event, _, cx| {
4466 cx.stop_propagation();
4467 this.selection = None;
4468 this.marked_entries.clear();
4469 }))
4470 .key_context(self.dispatch_context(window, cx))
4471 .on_action(cx.listener(Self::select_next))
4472 .on_action(cx.listener(Self::select_previous))
4473 .on_action(cx.listener(Self::select_first))
4474 .on_action(cx.listener(Self::select_last))
4475 .on_action(cx.listener(Self::select_parent))
4476 .on_action(cx.listener(Self::select_next_git_entry))
4477 .on_action(cx.listener(Self::select_prev_git_entry))
4478 .on_action(cx.listener(Self::select_next_diagnostic))
4479 .on_action(cx.listener(Self::select_prev_diagnostic))
4480 .on_action(cx.listener(Self::select_next_directory))
4481 .on_action(cx.listener(Self::select_prev_directory))
4482 .on_action(cx.listener(Self::expand_selected_entry))
4483 .on_action(cx.listener(Self::collapse_selected_entry))
4484 .on_action(cx.listener(Self::collapse_all_entries))
4485 .on_action(cx.listener(Self::open))
4486 .on_action(cx.listener(Self::open_permanent))
4487 .on_action(cx.listener(Self::confirm))
4488 .on_action(cx.listener(Self::cancel))
4489 .on_action(cx.listener(Self::copy_path))
4490 .on_action(cx.listener(Self::copy_relative_path))
4491 .on_action(cx.listener(Self::new_search_in_directory))
4492 .on_action(cx.listener(Self::unfold_directory))
4493 .on_action(cx.listener(Self::fold_directory))
4494 .on_action(cx.listener(Self::remove_from_project))
4495 .when(!project.is_read_only(cx), |el| {
4496 el.on_action(cx.listener(Self::new_file))
4497 .on_action(cx.listener(Self::new_directory))
4498 .on_action(cx.listener(Self::rename))
4499 .on_action(cx.listener(Self::delete))
4500 .on_action(cx.listener(Self::trash))
4501 .on_action(cx.listener(Self::cut))
4502 .on_action(cx.listener(Self::copy))
4503 .on_action(cx.listener(Self::paste))
4504 .on_action(cx.listener(Self::duplicate))
4505 .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| {
4506 if event.up.click_count > 1 {
4507 if let Some(entry_id) = this.last_worktree_root_id {
4508 let project = this.project.read(cx);
4509
4510 let worktree_id = if let Some(worktree) =
4511 project.worktree_for_entry(entry_id, cx)
4512 {
4513 worktree.read(cx).id()
4514 } else {
4515 return;
4516 };
4517
4518 this.selection = Some(SelectedEntry {
4519 worktree_id,
4520 entry_id,
4521 });
4522
4523 this.new_file(&NewFile, window, cx);
4524 }
4525 }
4526 }))
4527 })
4528 .when(project.is_local(), |el| {
4529 el.on_action(cx.listener(Self::reveal_in_finder))
4530 .on_action(cx.listener(Self::open_system))
4531 .on_action(cx.listener(Self::open_in_terminal))
4532 })
4533 .when(project.is_via_ssh(), |el| {
4534 el.on_action(cx.listener(Self::open_in_terminal))
4535 })
4536 .on_mouse_down(
4537 MouseButton::Right,
4538 cx.listener(move |this, event: &MouseDownEvent, window, cx| {
4539 // When deploying the context menu anywhere below the last project entry,
4540 // act as if the user clicked the root of the last worktree.
4541 if let Some(entry_id) = this.last_worktree_root_id {
4542 this.deploy_context_menu(event.position, entry_id, window, cx);
4543 }
4544 }),
4545 )
4546 .track_focus(&self.focus_handle(cx))
4547 .child(
4548 uniform_list(cx.entity().clone(), "entries", item_count, {
4549 |this, range, window, cx| {
4550 let mut items = Vec::with_capacity(range.end - range.start);
4551 this.for_each_visible_entry(
4552 range,
4553 window,
4554 cx,
4555 |id, details, window, cx| {
4556 items.push(this.render_entry(id, details, window, cx));
4557 },
4558 );
4559 items
4560 }
4561 })
4562 .when(show_indent_guides, |list| {
4563 list.with_decoration(
4564 ui::indent_guides(
4565 cx.entity().clone(),
4566 px(indent_size),
4567 IndentGuideColors::panel(cx),
4568 |this, range, window, cx| {
4569 let mut items =
4570 SmallVec::with_capacity(range.end - range.start);
4571 this.iter_visible_entries(
4572 range,
4573 window,
4574 cx,
4575 |entry, entries, _, _| {
4576 let (depth, _) = Self::calculate_depth_and_difference(
4577 entry, entries,
4578 );
4579 items.push(depth);
4580 },
4581 );
4582 items
4583 },
4584 )
4585 .on_click(cx.listener(
4586 |this, active_indent_guide: &IndentGuideLayout, window, cx| {
4587 if window.modifiers().secondary() {
4588 let ix = active_indent_guide.offset.y;
4589 let Some((target_entry, worktree)) = maybe!({
4590 let (worktree_id, entry) = this.entry_at_index(ix)?;
4591 let worktree = this
4592 .project
4593 .read(cx)
4594 .worktree_for_id(worktree_id, cx)?;
4595 let target_entry = worktree
4596 .read(cx)
4597 .entry_for_path(&entry.path.parent()?)?;
4598 Some((target_entry, worktree))
4599 }) else {
4600 return;
4601 };
4602
4603 this.collapse_entry(target_entry.clone(), worktree, cx);
4604 }
4605 },
4606 ))
4607 .with_render_fn(
4608 cx.entity().clone(),
4609 move |this, params, _, cx| {
4610 const LEFT_OFFSET: Pixels = px(14.);
4611 const PADDING_Y: Pixels = px(4.);
4612 const HITBOX_OVERDRAW: Pixels = px(3.);
4613
4614 let active_indent_guide_index =
4615 this.find_active_indent_guide(¶ms.indent_guides, cx);
4616
4617 let indent_size = params.indent_size;
4618 let item_height = params.item_height;
4619
4620 params
4621 .indent_guides
4622 .into_iter()
4623 .enumerate()
4624 .map(|(idx, layout)| {
4625 let offset = if layout.continues_offscreen {
4626 px(0.)
4627 } else {
4628 PADDING_Y
4629 };
4630 let bounds = Bounds::new(
4631 point(
4632 layout.offset.x * indent_size + LEFT_OFFSET,
4633 layout.offset.y * item_height + offset,
4634 ),
4635 size(
4636 px(1.),
4637 layout.length * item_height - offset * 2.,
4638 ),
4639 );
4640 ui::RenderedIndentGuide {
4641 bounds,
4642 layout,
4643 is_active: Some(idx) == active_indent_guide_index,
4644 hitbox: Some(Bounds::new(
4645 point(
4646 bounds.origin.x - HITBOX_OVERDRAW,
4647 bounds.origin.y,
4648 ),
4649 size(
4650 bounds.size.width + HITBOX_OVERDRAW * 2.,
4651 bounds.size.height,
4652 ),
4653 )),
4654 }
4655 })
4656 .collect()
4657 },
4658 ),
4659 )
4660 })
4661 .size_full()
4662 .with_sizing_behavior(ListSizingBehavior::Infer)
4663 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4664 .with_width_from_item(self.max_width_item_index)
4665 .track_scroll(self.scroll_handle.clone()),
4666 )
4667 .children(self.render_vertical_scrollbar(cx))
4668 .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
4669 this.pb_4().child(scrollbar)
4670 })
4671 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4672 deferred(
4673 anchored()
4674 .position(*position)
4675 .anchor(gpui::Corner::TopLeft)
4676 .child(menu.clone()),
4677 )
4678 .with_priority(1)
4679 }))
4680 } else {
4681 v_flex()
4682 .id("empty-project_panel")
4683 .size_full()
4684 .p_4()
4685 .track_focus(&self.focus_handle(cx))
4686 .child(
4687 Button::new("open_project", "Open a project")
4688 .full_width()
4689 .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
4690 .on_click(cx.listener(|this, _, window, cx| {
4691 this.workspace
4692 .update(cx, |_, cx| {
4693 window.dispatch_action(Box::new(workspace::Open), cx)
4694 })
4695 .log_err();
4696 })),
4697 )
4698 .when(is_local, |div| {
4699 div.drag_over::<ExternalPaths>(|style, _, _, cx| {
4700 style.bg(cx.theme().colors().drop_target_background)
4701 })
4702 .on_drop(cx.listener(
4703 move |this, external_paths: &ExternalPaths, window, cx| {
4704 this.last_external_paths_drag_over_entry = None;
4705 this.marked_entries.clear();
4706 this.hover_scroll_task.take();
4707 if let Some(task) = this
4708 .workspace
4709 .update(cx, |workspace, cx| {
4710 workspace.open_workspace_for_paths(
4711 true,
4712 external_paths.paths().to_owned(),
4713 window,
4714 cx,
4715 )
4716 })
4717 .log_err()
4718 {
4719 task.detach_and_log_err(cx);
4720 }
4721 cx.stop_propagation();
4722 },
4723 ))
4724 })
4725 }
4726 }
4727}
4728
4729impl Render for DraggedProjectEntryView {
4730 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4731 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
4732 h_flex()
4733 .font(ui_font)
4734 .pl(self.click_offset.x + px(12.))
4735 .pt(self.click_offset.y + px(12.))
4736 .child(
4737 div()
4738 .flex()
4739 .gap_1()
4740 .items_center()
4741 .py_1()
4742 .px_2()
4743 .rounded_lg()
4744 .bg(cx.theme().colors().background)
4745 .map(|this| {
4746 if self.selections.len() > 1 && self.selections.contains(&self.selection) {
4747 this.child(Label::new(format!("{} entries", self.selections.len())))
4748 } else {
4749 this.child(if let Some(icon) = &self.details.icon {
4750 div().child(Icon::from_path(icon.clone()))
4751 } else {
4752 div()
4753 })
4754 .child(Label::new(self.details.filename.clone()))
4755 }
4756 }),
4757 )
4758 }
4759}
4760
4761impl EventEmitter<Event> for ProjectPanel {}
4762
4763impl EventEmitter<PanelEvent> for ProjectPanel {}
4764
4765impl Panel for ProjectPanel {
4766 fn position(&self, _: &Window, cx: &App) -> DockPosition {
4767 match ProjectPanelSettings::get_global(cx).dock {
4768 ProjectPanelDockPosition::Left => DockPosition::Left,
4769 ProjectPanelDockPosition::Right => DockPosition::Right,
4770 }
4771 }
4772
4773 fn position_is_valid(&self, position: DockPosition) -> bool {
4774 matches!(position, DockPosition::Left | DockPosition::Right)
4775 }
4776
4777 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4778 settings::update_settings_file::<ProjectPanelSettings>(
4779 self.fs.clone(),
4780 cx,
4781 move |settings, _| {
4782 let dock = match position {
4783 DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
4784 DockPosition::Right => ProjectPanelDockPosition::Right,
4785 };
4786 settings.dock = Some(dock);
4787 },
4788 );
4789 }
4790
4791 fn size(&self, _: &Window, cx: &App) -> Pixels {
4792 self.width
4793 .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
4794 }
4795
4796 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4797 self.width = size;
4798 self.serialize(cx);
4799 cx.notify();
4800 }
4801
4802 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4803 ProjectPanelSettings::get_global(cx)
4804 .button
4805 .then_some(IconName::FileTree)
4806 }
4807
4808 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
4809 Some("Project Panel")
4810 }
4811
4812 fn toggle_action(&self) -> Box<dyn Action> {
4813 Box::new(ToggleFocus)
4814 }
4815
4816 fn persistent_name() -> &'static str {
4817 "Project Panel"
4818 }
4819
4820 fn starts_open(&self, _: &Window, cx: &App) -> bool {
4821 let project = &self.project.read(cx);
4822 project.visible_worktrees(cx).any(|tree| {
4823 tree.read(cx)
4824 .root_entry()
4825 .map_or(false, |entry| entry.is_dir())
4826 })
4827 }
4828
4829 fn activation_priority(&self) -> u32 {
4830 0
4831 }
4832}
4833
4834impl Focusable for ProjectPanel {
4835 fn focus_handle(&self, _cx: &App) -> FocusHandle {
4836 self.focus_handle.clone()
4837 }
4838}
4839
4840impl ClipboardEntry {
4841 fn is_cut(&self) -> bool {
4842 matches!(self, Self::Cut { .. })
4843 }
4844
4845 fn items(&self) -> &BTreeSet<SelectedEntry> {
4846 match self {
4847 ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries,
4848 }
4849 }
4850}
4851
4852#[cfg(test)]
4853mod project_panel_tests;