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