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