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