mod project_panel_settings;mod utils; use anyhow::{anyhow, Context as _, Result};use client::{ErrorCode, ErrorExt};use collections::{hash_map, BTreeSet, HashMap};use command_palette_hooks::CommandPaletteFilter;use db::kvp::KEY_VALUE_STORE;use editor::{ items::{ entry_diagnostic_aware_icon_decoration_and_color, entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, }, scroll::{Autoscroll, ScrollbarAutoHide}, Editor, EditorEvent, EditorSettings, ShowScrollbar,};use file_icons::FileIcons;use git::status::GitSummary;use gpui::{ actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window,};use indexmap::IndexMap;use language::DiagnosticSeverity;use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};use project::{ relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId,};use project_panel_settings::{ ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::{Settings, SettingsStore};use smallvec::SmallVec;use std::any::TypeId;use std::{ cell::OnceCell, cmp, collections::HashSet, ffi::OsStr, ops::Range, path::{Path, PathBuf}, sync::Arc, time::Duration,};use theme::ThemeSettings;use ui::{ prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,};use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt};use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyTaskExt}, DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,};use worktree::{CreatedEntry, GitEntry, GitEntryRef}; const PROJECT_PANEL_KEY: &str = "ProjectPanel";const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { project: Entity<Project>, fs: Arc<dyn Fs>, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option<Task<()>>, visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). ancestors: HashMap<ProjectEntryId, FoldedAncestors>, folded_directory_drag_target: Option<FoldedDirectoryDragTarget>, last_worktree_root_id: Option<ProjectEntryId>, last_selection_drag_over_entry: Option<ProjectEntryId>, last_external_paths_drag_over_entry: Option<ProjectEntryId>, expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>, unfolded_dir_ids: HashSet<ProjectEntryId>, // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree selection: Option<SelectedEntry>, marked_entries: BTreeSet<SelectedEntry>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, edit_state: Option<EditState>, filename_editor: Entity<Editor>, clipboard: Option<ClipboardEntry>, _dragged_entry_destination: Option<Arc<Path>>, workspace: WeakEntity<Workspace>, width: Option<Pixels>, pending_serialization: Task<Option<()>>, show_scrollbar: bool, vertical_scrollbar_state: ScrollbarState, horizontal_scrollbar_state: ScrollbarState, hide_scrollbar_task: Option<Task<()>>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option<usize>, // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. mouse_down: bool, hover_expand_task: Option<Task<()>>,} #[derive(Copy, Clone, Debug)]struct FoldedDirectoryDragTarget { entry_id: ProjectEntryId, index: usize, /// Whether we are dragging over the delimiter rather than the component itself. is_delimiter_target: bool,} #[derive(Clone, Debug)]struct EditState { worktree_id: WorktreeId, entry_id: ProjectEntryId, leaf_entry_id: Option<ProjectEntryId>, is_dir: bool, depth: usize, processing_filename: Option<String>, previously_focused: Option<SelectedEntry>,} impl EditState { fn is_new_entry(&self) -> bool { self.leaf_entry_id.is_none() }} #[derive(Clone, Debug)]enum ClipboardEntry { Copied(BTreeSet<SelectedEntry>), Cut(BTreeSet<SelectedEntry>),} #[derive(Debug, PartialEq, Eq, Clone)]struct EntryDetails { filename: String, icon: Option<SharedString>, path: Arc<Path>, depth: usize, kind: EntryKind, is_ignored: bool, is_expanded: bool, is_selected: bool, is_marked: bool, is_editing: bool, is_processing: bool, is_cut: bool, filename_text_color: Color, diagnostic_severity: Option<DiagnosticSeverity>, git_status: GitSummary, is_private: bool, worktree_id: WorktreeId, canonical_path: Option<Box<Path>>,} #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]#[serde(deny_unknown_fields)]struct Delete { #[serde(default)] pub skip_prompt: bool,} #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]#[serde(deny_unknown_fields)]struct Trash { #[serde(default)] pub skip_prompt: bool,} impl_actions!(project_panel, [Delete, Trash]); actions!( project_panel, [ ExpandSelectedEntry, CollapseSelectedEntry, CollapseAllEntries, NewDirectory, NewFile, Copy, Duplicate, RevealInFileManager, RemoveFromProject, OpenWithSystem, Cut, Paste, Rename, Open, OpenPermanent, ToggleFocus, NewSearchInDirectory, UnfoldDirectory, FoldDirectory, SelectParent, SelectNextGitEntry, SelectPrevGitEntry, SelectNextDiagnostic, SelectPrevDiagnostic, SelectNextDirectory, SelectPrevDirectory, ]); #[derive(Debug, Default)]struct FoldedAncestors { current_ancestor_depth: usize, ancestors: Vec<ProjectEntryId>,} impl FoldedAncestors { fn max_ancestor_depth(&self) -> usize { self.ancestors.len() }} pub fn init_settings(cx: &mut App) { ProjectPanelSettings::register(cx);} pub fn init(cx: &mut App) { init_settings(cx); cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<ProjectPanel>(window, cx); }); }) .detach();} #[derive(Debug)]pub enum Event { OpenedEntry { entry_id: ProjectEntryId, focus_opened_item: bool, allow_preview: bool, }, SplitEntry { entry_id: ProjectEntryId, }, Focus,} #[derive(Serialize, Deserialize)]struct SerializedProjectPanel { width: Option<Pixels>,} struct DraggedProjectEntryView { selection: SelectedEntry, details: EntryDetails, click_offset: Point<Pixels>, selections: Arc<BTreeSet<SelectedEntry>>,} struct ItemColors { default: Hsla, hover: Hsla, drag_over: Hsla, marked: Hsla, focused: Hsla,} fn get_item_color(cx: &App) -> ItemColors { let colors = cx.theme().colors(); ItemColors { default: colors.panel_background, hover: colors.element_hover, marked: colors.element_selected, focused: colors.panel_focused_border, drag_over: colors.drop_target_background, }} impl ProjectPanel { fn new( workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { let project = workspace.project().clone(); let project_panel = cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.focus_out(window, cx); this.hide_scrollbar(window, cx); }) .detach(); cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { if ProjectPanelSettings::get_global(cx).auto_reveal_entries { this.reveal_entry(project.clone(), *entry_id, true, cx); } } project::Event::ActiveEntryChanged(None) => { this.marked_entries.clear(); } project::Event::RevealInProjectPanel(entry_id) => { this.reveal_entry(project.clone(), *entry_id, false, cx); cx.emit(PanelEvent::Activate); } project::Event::ActivateProjectPanel => { cx.emit(PanelEvent::Activate); } project::Event::DiskBasedDiagnosticsFinished { .. } | project::Event::DiagnosticsUpdated { .. } => { if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { this.update_diagnostics(cx); cx.notify(); } } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); cx.notify(); } project::Event::WorktreeUpdatedGitRepositories(_) | project::Event::WorktreeUpdatedEntries(_, _) | project::Event::WorktreeAdded(_) | project::Event::WorktreeOrderChanged => { this.update_visible_entries(None, cx); cx.notify(); } project::Event::ExpandedAllForEntry(worktree_id, entry_id) => { if let Some((worktree, expanded_dir_ids)) = project .read(cx) .worktree_for_id(*worktree_id, cx) .zip(this.expanded_dir_ids.get_mut(&worktree_id)) { let worktree = worktree.read(cx); let Some(entry) = worktree.entry_for_id(*entry_id) else { return; }; let include_ignored_dirs = !entry.is_ignored; let mut dirs_to_expand = vec![*entry_id]; while let Some(current_id) = dirs_to_expand.pop() { let Some(current_entry) = worktree.entry_for_id(current_id) else { continue; }; for child in worktree.child_entries(¤t_entry.path) { if !child.is_dir() || (include_ignored_dirs && child.is_ignored) { continue; } dirs_to_expand.push(child.id); if let Err(ix) = expanded_dir_ids.binary_search(&child.id) { expanded_dir_ids.insert(ix, child.id); } this.unfolded_dir_ids.insert(child.id); } } this.update_visible_entries(None, cx); cx.notify(); } } _ => {} }) .detach(); let trash_action = [TypeId::of::<Trash>()]; let is_remote = project.read(cx).is_via_collab(); if is_remote { CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&trash_action); }); } let filename_editor = cx.new(|cx| Editor::single_line(window, cx)); cx.subscribe( &filename_editor, |project_panel, _, editor_event, cx| match editor_event { EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => { project_panel.autoscroll(cx); } EditorEvent::Blurred => { if project_panel .edit_state .as_ref() .map_or(false, |state| state.processing_filename.is_none()) { project_panel.edit_state = None; project_panel.update_visible_entries(None, cx); cx.notify(); } } _ => {} }, ) .detach(); cx.observe_global::<FileIcons>(|_, cx| { cx.notify(); }) .detach(); let mut project_panel_settings = *ProjectPanelSettings::get_global(cx); cx.observe_global::<SettingsStore>(move |this, cx| { let new_settings = *ProjectPanelSettings::get_global(cx); if project_panel_settings != new_settings { project_panel_settings = new_settings; this.update_diagnostics(cx); cx.notify(); } }) .detach(); let scroll_handle = UniformListScrollHandle::new(); let mut this = Self { project: project.clone(), hover_scroll_task: None, fs: workspace.app_state().fs.clone(), focus_handle, visible_entries: Default::default(), ancestors: Default::default(), folded_directory_drag_target: None, last_worktree_root_id: Default::default(), last_external_paths_drag_over_entry: None, last_selection_drag_over_entry: None, expanded_dir_ids: Default::default(), unfolded_dir_ids: Default::default(), selection: None, marked_entries: Default::default(), edit_state: None, context_menu: None, filename_editor, clipboard: None, _dragged_entry_destination: None, workspace: workspace.weak_handle(), width: None, pending_serialization: Task::ready(None), show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) .parent_entity(&cx.entity()), horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) .parent_entity(&cx.entity()), max_width_item_index: None, diagnostics: Default::default(), scroll_handle, mouse_down: false, hover_expand_task: None, }; this.update_visible_entries(None, cx); this }); cx.subscribe_in(&project_panel, window, { let project_panel = project_panel.downgrade(); move |workspace, _, event, window, cx| match event { &Event::OpenedEntry { entry_id, focus_opened_item, allow_preview, } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; let is_via_ssh = project.read(cx).is_via_ssh(); workspace .open_path_preview( ProjectPath { worktree_id, path: file_path.clone(), }, None, focus_opened_item, allow_preview, true, window, cx, ) .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| { match e.error_code() { ErrorCode::Disconnected => if is_via_ssh { Some("Disconnected from SSH host".to_string()) } else { Some("Disconnected from remote project".to_string()) }, ErrorCode::UnsharedItem => Some(format!( "{} is not shared by the host. This could be because it has been marked as `private`", file_path.display()Lines 1–500 of 9757. Load more
mod project_panel_settings; mod utils; use anyhow::{anyhow, Context as _, Result}; use client::{ErrorCode, ErrorExt}; use collections::{hash_map, BTreeSet, HashMap}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use editor::{ items::{ entry_diagnostic_aware_icon_decoration_and_color, entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, }, scroll::{Autoscroll, ScrollbarAutoHide}, Editor, EditorEvent, EditorSettings, ShowScrollbar, }; use file_icons::FileIcons; use git::status::GitSummary; use gpui::{ actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, }; use indexmap::IndexMap; use language::DiagnosticSeverity; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::{ relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, }; use project_panel_settings::{ ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use smallvec::SmallVec; use std::any::TypeId; use std::{ cell::OnceCell, cmp, collections::HashSet, ffi::OsStr, ops::Range, path::{Path, PathBuf}, sync::Arc, time::Duration, }; use theme::ThemeSettings; use ui::{ prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, }; use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyTaskExt}, DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace, }; use worktree::{CreatedEntry, GitEntry, GitEntryRef}; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { project: Entity<Project>, fs: Arc<dyn Fs>, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option<Task<()>>, visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). ancestors: HashMap<ProjectEntryId, FoldedAncestors>, folded_directory_drag_target: Option<FoldedDirectoryDragTarget>, last_worktree_root_id: Option<ProjectEntryId>, last_selection_drag_over_entry: Option<ProjectEntryId>, last_external_paths_drag_over_entry: Option<ProjectEntryId>, expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>, unfolded_dir_ids: HashSet<ProjectEntryId>, // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree selection: Option<SelectedEntry>, marked_entries: BTreeSet<SelectedEntry>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, edit_state: Option<EditState>, filename_editor: Entity<Editor>, clipboard: Option<ClipboardEntry>, _dragged_entry_destination: Option<Arc<Path>>, workspace: WeakEntity<Workspace>, width: Option<Pixels>, pending_serialization: Task<Option<()>>, show_scrollbar: bool, vertical_scrollbar_state: ScrollbarState, horizontal_scrollbar_state: ScrollbarState, hide_scrollbar_task: Option<Task<()>>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option<usize>, // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. mouse_down: bool, hover_expand_task: Option<Task<()>>, } #[derive(Copy, Clone, Debug)] struct FoldedDirectoryDragTarget { entry_id: ProjectEntryId, index: usize, /// Whether we are dragging over the delimiter rather than the component itself. is_delimiter_target: bool, } #[derive(Clone, Debug)] struct EditState { worktree_id: WorktreeId, entry_id: ProjectEntryId, leaf_entry_id: Option<ProjectEntryId>, is_dir: bool, depth: usize, processing_filename: Option<String>, previously_focused: Option<SelectedEntry>, } impl EditState { fn is_new_entry(&self) -> bool { self.leaf_entry_id.is_none() } } #[derive(Clone, Debug)] enum ClipboardEntry { Copied(BTreeSet<SelectedEntry>), Cut(BTreeSet<SelectedEntry>), } #[derive(Debug, PartialEq, Eq, Clone)] struct EntryDetails { filename: String, icon: Option<SharedString>, path: Arc<Path>, depth: usize, kind: EntryKind, is_ignored: bool, is_expanded: bool, is_selected: bool, is_marked: bool, is_editing: bool, is_processing: bool, is_cut: bool, filename_text_color: Color, diagnostic_severity: Option<DiagnosticSeverity>, git_status: GitSummary, is_private: bool, worktree_id: WorktreeId, canonical_path: Option<Box<Path>>, } #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] struct Delete { #[serde(default)] pub skip_prompt: bool, } #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] struct Trash { #[serde(default)] pub skip_prompt: bool, } impl_actions!(project_panel, [Delete, Trash]); actions!( project_panel, [ ExpandSelectedEntry, CollapseSelectedEntry, CollapseAllEntries, NewDirectory, NewFile, Copy, Duplicate, RevealInFileManager, RemoveFromProject, OpenWithSystem, Cut, Paste, Rename, Open, OpenPermanent, ToggleFocus, NewSearchInDirectory, UnfoldDirectory, FoldDirectory, SelectParent, SelectNextGitEntry, SelectPrevGitEntry, SelectNextDiagnostic, SelectPrevDiagnostic, SelectNextDirectory, SelectPrevDirectory, ] ); #[derive(Debug, Default)] struct FoldedAncestors { current_ancestor_depth: usize, ancestors: Vec<ProjectEntryId>, } impl FoldedAncestors { fn max_ancestor_depth(&self) -> usize { self.ancestors.len() } } pub fn init_settings(cx: &mut App) { ProjectPanelSettings::register(cx); } pub fn init(cx: &mut App) { init_settings(cx); cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<ProjectPanel>(window, cx); }); }) .detach(); } #[derive(Debug)] pub enum Event { OpenedEntry { entry_id: ProjectEntryId, focus_opened_item: bool, allow_preview: bool, }, SplitEntry { entry_id: ProjectEntryId, }, Focus, } #[derive(Serialize, Deserialize)] struct SerializedProjectPanel { width: Option<Pixels>, } struct DraggedProjectEntryView { selection: SelectedEntry, details: EntryDetails, click_offset: Point<Pixels>, selections: Arc<BTreeSet<SelectedEntry>>, } struct ItemColors { default: Hsla, hover: Hsla, drag_over: Hsla, marked: Hsla, focused: Hsla, } fn get_item_color(cx: &App) -> ItemColors { let colors = cx.theme().colors(); ItemColors { default: colors.panel_background, hover: colors.element_hover, marked: colors.element_selected, focused: colors.panel_focused_border, drag_over: colors.drop_target_background, } } impl ProjectPanel { fn new( workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { let project = workspace.project().clone(); let project_panel = cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.focus_out(window, cx); this.hide_scrollbar(window, cx); }) .detach(); cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { if ProjectPanelSettings::get_global(cx).auto_reveal_entries { this.reveal_entry(project.clone(), *entry_id, true, cx); } } project::Event::ActiveEntryChanged(None) => { this.marked_entries.clear(); } project::Event::RevealInProjectPanel(entry_id) => { this.reveal_entry(project.clone(), *entry_id, false, cx); cx.emit(PanelEvent::Activate); } project::Event::ActivateProjectPanel => { cx.emit(PanelEvent::Activate); } project::Event::DiskBasedDiagnosticsFinished { .. } | project::Event::DiagnosticsUpdated { .. } => { if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { this.update_diagnostics(cx); cx.notify(); } } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); cx.notify(); } project::Event::WorktreeUpdatedGitRepositories(_) | project::Event::WorktreeUpdatedEntries(_, _) | project::Event::WorktreeAdded(_) | project::Event::WorktreeOrderChanged => { this.update_visible_entries(None, cx); cx.notify(); } project::Event::ExpandedAllForEntry(worktree_id, entry_id) => { if let Some((worktree, expanded_dir_ids)) = project .read(cx) .worktree_for_id(*worktree_id, cx) .zip(this.expanded_dir_ids.get_mut(&worktree_id)) { let worktree = worktree.read(cx); let Some(entry) = worktree.entry_for_id(*entry_id) else { return; }; let include_ignored_dirs = !entry.is_ignored; let mut dirs_to_expand = vec![*entry_id]; while let Some(current_id) = dirs_to_expand.pop() { let Some(current_entry) = worktree.entry_for_id(current_id) else { continue; }; for child in worktree.child_entries(¤t_entry.path) { if !child.is_dir() || (include_ignored_dirs && child.is_ignored) { continue; } dirs_to_expand.push(child.id); if let Err(ix) = expanded_dir_ids.binary_search(&child.id) { expanded_dir_ids.insert(ix, child.id); } this.unfolded_dir_ids.insert(child.id); } } this.update_visible_entries(None, cx); cx.notify(); } } _ => {} }) .detach(); let trash_action = [TypeId::of::<Trash>()]; let is_remote = project.read(cx).is_via_collab(); if is_remote { CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&trash_action); }); } let filename_editor = cx.new(|cx| Editor::single_line(window, cx)); cx.subscribe( &filename_editor, |project_panel, _, editor_event, cx| match editor_event { EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => { project_panel.autoscroll(cx); } EditorEvent::Blurred => { if project_panel .edit_state .as_ref() .map_or(false, |state| state.processing_filename.is_none()) { project_panel.edit_state = None; project_panel.update_visible_entries(None, cx); cx.notify(); } } _ => {} }, ) .detach(); cx.observe_global::<FileIcons>(|_, cx| { cx.notify(); }) .detach(); let mut project_panel_settings = *ProjectPanelSettings::get_global(cx); cx.observe_global::<SettingsStore>(move |this, cx| { let new_settings = *ProjectPanelSettings::get_global(cx); if project_panel_settings != new_settings { project_panel_settings = new_settings; this.update_diagnostics(cx); cx.notify(); } }) .detach(); let scroll_handle = UniformListScrollHandle::new(); let mut this = Self { project: project.clone(), hover_scroll_task: None, fs: workspace.app_state().fs.clone(), focus_handle, visible_entries: Default::default(), ancestors: Default::default(), folded_directory_drag_target: None, last_worktree_root_id: Default::default(), last_external_paths_drag_over_entry: None, last_selection_drag_over_entry: None, expanded_dir_ids: Default::default(), unfolded_dir_ids: Default::default(), selection: None, marked_entries: Default::default(), edit_state: None, context_menu: None, filename_editor, clipboard: None, _dragged_entry_destination: None, workspace: workspace.weak_handle(), width: None, pending_serialization: Task::ready(None), show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) .parent_entity(&cx.entity()), horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) .parent_entity(&cx.entity()), max_width_item_index: None, diagnostics: Default::default(), scroll_handle, mouse_down: false, hover_expand_task: None, }; this.update_visible_entries(None, cx); this }); cx.subscribe_in(&project_panel, window, { let project_panel = project_panel.downgrade(); move |workspace, _, event, window, cx| match event { &Event::OpenedEntry { entry_id, focus_opened_item, allow_preview, } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; let is_via_ssh = project.read(cx).is_via_ssh(); workspace .open_path_preview( ProjectPath { worktree_id, path: file_path.clone(), }, None, focus_opened_item, allow_preview, true, window, cx, ) .detach_and_prompt_err("Failed to open file", window, cx, move |e, _, _| { match e.error_code() { ErrorCode::Disconnected => if is_via_ssh { Some("Disconnected from SSH host".to_string()) } else { Some("Disconnected from remote project".to_string()) }, ErrorCode::UnsharedItem => Some(format!( "{} is not shared by the host. This could be because it has been marked as `private`", file_path.display()