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