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