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