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