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