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