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