use std::collections::{HashMap, HashSet};use std::ops::Range;use std::path::{Path, PathBuf};use std::sync::Arc; use editing::sort_entries_for_file_tree;use itertools::Itertools;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::Vector2F;use render::RenderState;use repo_metadata::file_tree_store::{ FileTreeDirectoryEntryState, FileTreeEntryState, FileTreeFileMetadata,};use repo_metadata::local_model::IndexedRepoState;use repo_metadata::repositories::DetectedRepositories;use repo_metadata::{FileTreeEntry, RepoMetadataModel};use warp_core::features::FeatureFlag;use warp_core::ui::theme::Fill;use warp_core::ui::theme::color::internal_colors;use warp_core::{HostId, send_telemetry_from_ctx};use warp_util::path::LineAndColumnArg;use warp_util::standardized_path::StandardizedPath;use warpui::clipboard::ClipboardContent;use warpui::elements::{ AcceptedByDropTarget, Align, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Dismiss, Draggable, DraggableState, Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Percentage, Rect, SavePosition, ScrollStateHandle, Scrollable, ScrollableElement, ScrollbarWidth, Shrinkable, Stack, Text, UniformList, UniformListState,};use warpui::fonts::{Properties, Style, Weight};use warpui::keymap::FixedBinding;use warpui::platform::Cursor;use warpui::text_layout::TextAlignment;use warpui::{ AppContext, BlurContext, Element, Entity, EventContext, ModelHandle, SingletonEntity as _, TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle, id,}; use crate::appearance::Appearance;use crate::code::active_file::{ActiveFileEvent, ActiveFileModel};use crate::code::buffer_location::LocalOrRemotePath;use crate::coding_panel_enablement_state::CodingPanelEnablementState;use crate::editor::{EditorOptions, EditorView, TextOptions};use crate::menu::{Menu, MenuItem, MenuItemFields};#[cfg(feature = "local_fs")]use crate::server::telemetry::CodePanelsFileOpenEntrypoint;use crate::server::telemetry::TelemetryEvent;use crate::terminal::input::InputDropTargetData;use crate::terminal::view::{TerminalDropTargetData, TerminalView};use crate::ui_components::icons::Icon;use crate::ui_components::item_highlight::{ImageOrIcon, ItemHighlightState};#[cfg(feature = "local_fs")]use crate::util::file::external_editor::EditorSettings;use crate::util::openable_file_type::{ EditorLayout, FileTarget, is_file_content_binary, is_jupyter_notebook_file, is_markdown_file,};#[cfg(feature = "local_fs")]use crate::util::openable_file_type::{ resolve_file_target_to_open_in_warp, resolve_file_target_with_editor_choice,}; mod editing;mod render; use crate::settings::{CodeSettings, CodeSettingsChangedEvent}; const REMOTE_TEXT: &str = "The Project Explorer requires access to your local workspace, which isn’t supported in remote sessions.";const DISABLED_TEXT: &str = "The Project Explorer requires access to your local workspace. Open a new session or navigate to an active session to view.";const WSL_TEXT: &str = "The Project Explorer doesn't currently work in WSL."; /// Stable identifier for an item in the file tree./// Includes both the root directory and the index within that root's flattened list.#[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct FileTreeIdentifier { /// The root directory this item belongs to pub root: StandardizedPath, /// Index within the flattened list for this root pub index: usize,} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum PendingEditKind { CreateNewFile, RenameExisting,} #[derive(Debug, Clone)]pub enum FileTreeAction { ItemClicked { id: FileTreeIdentifier, }, SelectPreviousItem, SelectNextItem, Expand, Collapse, ExecuteSelectedItem, OpenContextMenu { position: Vector2F, id: FileTreeIdentifier, }, CopyPath { id: FileTreeIdentifier, }, CopyRelativePath { id: FileTreeIdentifier, }, AttachAsContext { id: FileTreeIdentifier, }, OpenInFinder { id: FileTreeIdentifier, }, Rename { id: FileTreeIdentifier, }, Delete { id: FileTreeIdentifier, }, NewFileBelowDirectory { id: FileTreeIdentifier, }, OpenInNewPane { id: FileTreeIdentifier, }, OpenInNewTab { id: FileTreeIdentifier, }, CDToDirectory { id: FileTreeIdentifier, }, DismissEditor, ItemDroppedOnInput { id: FileTreeIdentifier, terminal_input_data: InputDropTargetData, }, ItemDroppedOnTerminal { id: FileTreeIdentifier, terminal_view: WeakViewHandle<TerminalView>, },} pub fn init(app: &mut AppContext) { app.register_fixed_bindings([ FixedBinding::new( "up", FileTreeAction::SelectPreviousItem, id!(FileTreeView::ui_name()), ), FixedBinding::new( "down", FileTreeAction::SelectNextItem, id!(FileTreeView::ui_name()), ), FixedBinding::new( "right", FileTreeAction::Expand, id!(FileTreeView::ui_name()), ), FixedBinding::new( "left", FileTreeAction::Collapse, id!(FileTreeView::ui_name()), ), FixedBinding::new( "enter", FileTreeAction::ExecuteSelectedItem, id!(FileTreeView::ui_name()), ), ]);} // Constants matching the Drive panel stylingconst ITEM_FONT_SIZE: f32 = 14.;const FOLDER_INDENT: f32 = 16.; // Indentation per folder levelconst ITEM_PADDING: f32 = 4.; /// Represents a single item in the flattened file tree list./// This is used to store the necessary information for rendering each item/// in the UniformList.#[derive(Clone)]enum FileTreeItem { /// A file item with its metadata and depth in the tree File { metadata: FileTreeFileMetadata, depth: usize, mouse_state_handle: MouseStateHandle, draggable_state: DraggableState, }, /// A directory header with its metadata, depth, and expanded state DirectoryHeader { directory: FileTreeDirectoryEntryState, depth: usize, mouse_state_handle: MouseStateHandle, draggable_state: DraggableState, },} impl FileTreeItem { fn path(&self) -> &StandardizedPath { match self { FileTreeItem::File { metadata, .. } => &metadata.path, FileTreeItem::DirectoryHeader { directory, .. } => &directory.path, } }} struct ContextMenuState { position: Vector2F,} struct PendingEdit { kind: PendingEditKind, id: FileTreeIdentifier,} /// Per-root directory state for the file tree./// Contains all state that varies per root directory.struct RootDirectory { /// File tree entry for this root. This may be backed by a repository root /// or by a lazily-loaded standalone path. entry: FileTreeEntry, /// Set of expanded folder paths within this root expanded_folders: HashSet<StandardizedPath>, /// Flattened list of items for rendering items: Vec<FileTreeItem>, /// Mouse state handles and draggable state preserved across rebuilds, keyed by path item_states: HashMap<StandardizedPath, (MouseStateHandle, DraggableState)>, /// The remote host this root belongs to, if any. `None` for local roots. remote_host_id: Option<HostId>,} impl RootDirectory { /// Returns whether this root is backed by a remote server. fn is_remote(&self) -> bool { self.remote_host_id.is_some() }} pub struct FileTreeView { /// Per-root state, keyed by root path root_directories: HashMap<StandardizedPath, RootDirectory>, /// The displayed directories displayed_directories: Vec<StandardizedPath>, #[cfg(feature = "local_fs")] enablement: CodingPanelEnablementState, #[cfg(feature = "local_fs")] repository_metadata_model: ModelHandle<RepoMetadataModel>, #[cfg(feature = "local_fs")] is_active: bool, /// Identifier of the currently selected item selected_item: Option<FileTreeIdentifier>, /// State for the UniformList list_state: UniformListState, /// Scroll state handle for the NewScrollable wrapper scroll_state: ScrollStateHandle, /// Weak view handle for the FilePicker view_handle: WeakViewHandle<Self>, context_menu: ViewHandle<Menu<FileTreeAction>>, /// State for the context menu, if one is currently open. context_menu_state: Option<ContextMenuState>, /// Unique ID used to position this view. position_id: String, /// The pending edit, if any pending_edit: Option<PendingEdit>, /// Editor view used for editing (renaming, creating) an item in the tree. editor_view: ViewHandle<EditorView>, /// Handle to track the currently focused file active_file_model: Option<ModelHandle<ActiveFileModel>>, has_terminal_session: bool, /// Paths the user explicitly collapsed (per root). /// /// This is used to prevent automatic expansion behavior (e.g. when switching tabs, /// focusing the left panel, or "reveal active file") from overriding the user's intent. /// /// The root header collapse is represented by including the root path itself in the set. explicitly_collapsed: HashMap<StandardizedPath, HashSet<StandardizedPath>>, /// Lazy-loaded paths that this view has registered with the /// [`LocalRepoMetadataModel`] for file watching. #[cfg(feature = "local_fs")] registered_lazy_loaded_paths: HashSet<StandardizedPath>, /// Directory the view wants to focus once its entry becomes available. /// /// Set when a descendant path is absorbed into an ancestor root but the /// descendant's entry has not yet been materialized (e.g. the ancestor /// is still lazy-loading). Re-evaluated on every rebuild; cleared when /// the target is selected by the user or when the target root stops /// being displayed. pending_focus_target: Option<PendingFocusTarget>, /// Whether to show hidden files (dotfiles) in the file tree. show_hidden_files: bool,} /// Directory the file tree wants to focus once its entry becomes available.#[derive(Clone, Debug, PartialEq, Eq)]struct PendingFocusTarget { root: StandardizedPath, path: StandardizedPath, /// Whether this target has already driven a scroll. /// /// The first successful apply scrolls the tree to the target. On /// subsequent rebuilds we still re-apply the selection marker so it /// doesn't get overridden by a default root-header fallback, but we /// intentionally do NOT scroll again — the user may have scrolled /// elsewhere since the initial focus-follow and a late metadata /// update must not snap them back. scrolled: bool,} impl FileTreeView { fn is_explicitly_collapsed(&self, root: &StandardizedPath, path: &StandardizedPath) -> bool { self.explicitly_collapsed .get(root) .is_some_and(|collapsed| collapsed.contains(path)) } /// Returns whether the item identified by `id` belongs to a remote root. fn is_remote_item(&self, id: &FileTreeIdentifier) -> bool { self.root_directories .get(&id.root) .is_some_and(|r| r.is_remote()) } #[cfg(feature = "local_fs")] fn is_active(&self) -> bool { self.is_active } #[cfg(not(feature = "local_fs"))] fn is_active(&self) -> bool { true } #[cfg(feature = "local_fs")] pub fn set_is_active(&mut self, is_active: bool, ctx: &mut ViewContext<Self>) { self.set_is_active_local_fs(is_active, ctx); } #[cfg(not(feature = "local_fs"))] pub fn set_is_active(&mut self, _is_active: bool, _ctx: &mut ViewContext<Self>) {} #[cfg(feature = "local_fs")] fn set_is_active_local_fs(&mut self, is_active: bool, ctx: &mut ViewContext<Self>) { if self.is_active == is_active { return; } self.is_active = is_active; if is_active { self.subscribe_to_repository_metadata(ctx); self.subscribe_to_active_file_model(ctx); self.subscribe_to_code_settings(ctx); self.show_hidden_files = *CodeSettings::as_ref(ctx).show_hidden_files; // Catch up on any repository/file changes that happened while inactive. // Skip remote-backed roots — their data comes from server pushes, // not from local DetectedRepositories / lazy-loading. let local_dirs: Vec<_> = self .displayed_directories .iter() .filter(|p| !self.root_directories.get(p).is_some_and(|r| r.is_remote())) .cloned() .collect(); self.update_directory_contents(&local_dirs, false, ctx); // Catch up on remote roots already in root_directories whose // content may have changed while inactive. We only refresh // existing roots — new remote roots are managed by the // workspace via `set_remote_root_directories`. let existing_remote_ids: Vec<_> = self .root_directories .iter() .filter_map(|(_, root_dir)| { let host_id = root_dir.remote_host_id.as_ref()?; Some(repo_metadata::RemoteRepositoryIdentifier::new( host_id.clone(), root_dir.entry.root_directory().as_ref().clone(), )) }) .collect(); if !existing_remote_ids.is_empty() { self.insert_or_update_remote_roots(&existing_remote_ids, false, ctx); } } else { ctx.unsubscribe_to_model(&self.repository_metadata_model); self.unsubscribe_from_active_file_model(ctx); self.unsubscribe_from_code_settings(ctx); let repository_metadata_model = self.repository_metadata_model.clone(); let paths: Vec<_> = self.registered_lazy_loaded_paths.drain().collect(); repository_metadata_model.update(ctx, move |model: &mut RepoMetadataModel, ctx| { for path in &paths { model.remove_lazy_loaded_path(path, ctx); } }); } } #[cfg(feature = "local_fs")] fn subscribe_to_repository_metadata(&self, ctx: &mut ViewContext<Self>) { let model = self.repository_metadata_model.clone(); ctx.subscribe_to_model(&model, |me, _, event, ctx| { me.handle_repository_metadata_event(event, ctx); }); } #[cfg(feature = "local_fs")] fn remove_lazy_loaded_entry(&mut self, path: &StandardizedPath, ctx: &mut ViewContext<Self>) { let Some(std_path) = self.registered_lazy_loaded_paths.take(path) else { return; }; let repository_metadata_model = self.repository_metadata_model.clone(); repository_metadata_model.update(ctx, move |model: &mut RepoMetadataModel, ctx| { model.remove_lazy_loaded_path(&std_path, ctx); }); } /// Inserts or updates remote root directories. When `insert_only` is /// true, roots already present in `displayed_directories` are skipped /// (used for catch-up on reactivation). When false, existing roots are /// updated in-place (used for live server push events). /// /// Performs a single `rebuild_flattened_items` / `ctx.notify` at the end /// regardless of how many roots were processed. #[cfg(feature = "local_fs")] fn insert_or_update_remote_roots( &mut self, remote_ids: &[repo_metadata::RemoteRepositoryIdentifier], insert_only: bool, ctx: &mut ViewContext<Self>, ) { use repo_metadata::RepositoryIdentifier; let mut changed = false; for remote_id in remote_ids { let repo_path = remote_id.path.clone(); if insert_only && self.displayed_directories.contains(&repo_path) { continue; } let id = RepositoryIdentifier::Remote(remote_id.clone()); let Some(state) = RepoMetadataModel::as_ref(ctx).get_repository(&id, ctx) else { continue; }; // Remove any existing roots that are ancestors or descendants of the // new path. For example, when the user cd's from /home/user into // /home/user/repo, replace the home directory root with the repo root. self.displayed_directories.retain(|existing| { if *existing == repo_path { return true; } let dominated = existing.starts_with(&repo_path) || repo_path.starts_with(existing); if dominated { self.root_directories.remove(existing); } !dominated }); let host_id = remote_id.host_id.clone(); self.root_directories .entry(repo_path.clone()) .and_modify(|root_dir| { root_dir.entry = state.entry.clone(); root_dir.remote_host_id = Some(host_id.clone()); }) .or_insert_with(|| RootDirectory { entry: state.entry.clone(), expanded_folders: HashSet::new(), items: Vec::new(), item_states: HashMap::new(), remote_host_id: Some(host_id), }); if !self.displayed_directories.contains(&repo_path) { self.displayed_directories.push(repo_path.clone()); } // Auto-expand the root, respecting explicit user collapses. if !self.is_explicitly_collapsed(&repo_path, &repo_path) && let Some(root_dir) = self.root_directories.get_mut(&repo_path) { root_dir.expanded_folders.insert(repo_path); } changed = true; } if changed { self.rebuild_flattened_items(); ctx.notify(); } } #[cfg(feature = "local_fs")] fn handle_repository_metadata_event( &mut self,Lines 1–500 of 3187. Load more
use std::collections::{HashMap, HashSet}; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use editing::sort_entries_for_file_tree; use itertools::Itertools; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; use render::RenderState; use repo_metadata::file_tree_store::{ FileTreeDirectoryEntryState, FileTreeEntryState, FileTreeFileMetadata, }; use repo_metadata::local_model::IndexedRepoState; use repo_metadata::repositories::DetectedRepositories; use repo_metadata::{FileTreeEntry, RepoMetadataModel}; use warp_core::features::FeatureFlag; use warp_core::ui::theme::Fill; use warp_core::ui::theme::color::internal_colors; use warp_core::{HostId, send_telemetry_from_ctx}; use warp_util::path::LineAndColumnArg; use warp_util::standardized_path::StandardizedPath; use warpui::clipboard::ClipboardContent; use warpui::elements::{ AcceptedByDropTarget, Align, ChildAnchor, ChildView, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Dismiss, Draggable, DraggableState, Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Percentage, Rect, SavePosition, ScrollStateHandle, Scrollable, ScrollableElement, ScrollbarWidth, Shrinkable, Stack, Text, UniformList, UniformListState, }; use warpui::fonts::{Properties, Style, Weight}; use warpui::keymap::FixedBinding; use warpui::platform::Cursor; use warpui::text_layout::TextAlignment; use warpui::{ AppContext, BlurContext, Element, Entity, EventContext, ModelHandle, SingletonEntity as _, TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle, id, }; use crate::appearance::Appearance; use crate::code::active_file::{ActiveFileEvent, ActiveFileModel}; use crate::code::buffer_location::LocalOrRemotePath; use crate::coding_panel_enablement_state::CodingPanelEnablementState; use crate::editor::{EditorOptions, EditorView, TextOptions}; use crate::menu::{Menu, MenuItem, MenuItemFields}; #[cfg(feature = "local_fs")] use crate::server::telemetry::CodePanelsFileOpenEntrypoint; use crate::server::telemetry::TelemetryEvent; use crate::terminal::input::InputDropTargetData; use crate::terminal::view::{TerminalDropTargetData, TerminalView}; use crate::ui_components::icons::Icon; use crate::ui_components::item_highlight::{ImageOrIcon, ItemHighlightState}; #[cfg(feature = "local_fs")] use crate::util::file::external_editor::EditorSettings; use crate::util::openable_file_type::{ EditorLayout, FileTarget, is_file_content_binary, is_jupyter_notebook_file, is_markdown_file, }; #[cfg(feature = "local_fs")] use crate::util::openable_file_type::{ resolve_file_target_to_open_in_warp, resolve_file_target_with_editor_choice, }; mod editing; mod render; use crate::settings::{CodeSettings, CodeSettingsChangedEvent}; const REMOTE_TEXT: &str = "The Project Explorer requires access to your local workspace, which isn’t supported in remote sessions."; const DISABLED_TEXT: &str = "The Project Explorer requires access to your local workspace. Open a new session or navigate to an active session to view."; const WSL_TEXT: &str = "The Project Explorer doesn't currently work in WSL."; /// Stable identifier for an item in the file tree. /// Includes both the root directory and the index within that root's flattened list. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FileTreeIdentifier { /// The root directory this item belongs to pub root: StandardizedPath, /// Index within the flattened list for this root pub index: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PendingEditKind { CreateNewFile, RenameExisting, } #[derive(Debug, Clone)] pub enum FileTreeAction { ItemClicked { id: FileTreeIdentifier, }, SelectPreviousItem, SelectNextItem, Expand, Collapse, ExecuteSelectedItem, OpenContextMenu { position: Vector2F, id: FileTreeIdentifier, }, CopyPath { id: FileTreeIdentifier, }, CopyRelativePath { id: FileTreeIdentifier, }, AttachAsContext { id: FileTreeIdentifier, }, OpenInFinder { id: FileTreeIdentifier, }, Rename { id: FileTreeIdentifier, }, Delete { id: FileTreeIdentifier, }, NewFileBelowDirectory { id: FileTreeIdentifier, }, OpenInNewPane { id: FileTreeIdentifier, }, OpenInNewTab { id: FileTreeIdentifier, }, CDToDirectory { id: FileTreeIdentifier, }, DismissEditor, ItemDroppedOnInput { id: FileTreeIdentifier, terminal_input_data: InputDropTargetData, }, ItemDroppedOnTerminal { id: FileTreeIdentifier, terminal_view: WeakViewHandle<TerminalView>, }, } pub fn init(app: &mut AppContext) { app.register_fixed_bindings([ FixedBinding::new( "up", FileTreeAction::SelectPreviousItem, id!(FileTreeView::ui_name()), ), FixedBinding::new( "down", FileTreeAction::SelectNextItem, id!(FileTreeView::ui_name()), ), FixedBinding::new( "right", FileTreeAction::Expand, id!(FileTreeView::ui_name()), ), FixedBinding::new( "left", FileTreeAction::Collapse, id!(FileTreeView::ui_name()), ), FixedBinding::new( "enter", FileTreeAction::ExecuteSelectedItem, id!(FileTreeView::ui_name()), ), ]); } // Constants matching the Drive panel styling const ITEM_FONT_SIZE: f32 = 14.; const FOLDER_INDENT: f32 = 16.; // Indentation per folder level const ITEM_PADDING: f32 = 4.; /// Represents a single item in the flattened file tree list. /// This is used to store the necessary information for rendering each item /// in the UniformList. #[derive(Clone)] enum FileTreeItem { /// A file item with its metadata and depth in the tree File { metadata: FileTreeFileMetadata, depth: usize, mouse_state_handle: MouseStateHandle, draggable_state: DraggableState, }, /// A directory header with its metadata, depth, and expanded state DirectoryHeader { directory: FileTreeDirectoryEntryState, depth: usize, mouse_state_handle: MouseStateHandle, draggable_state: DraggableState, }, } impl FileTreeItem { fn path(&self) -> &StandardizedPath { match self { FileTreeItem::File { metadata, .. } => &metadata.path, FileTreeItem::DirectoryHeader { directory, .. } => &directory.path, } } } struct ContextMenuState { position: Vector2F, } struct PendingEdit { kind: PendingEditKind, id: FileTreeIdentifier, } /// Per-root directory state for the file tree. /// Contains all state that varies per root directory. struct RootDirectory { /// File tree entry for this root. This may be backed by a repository root /// or by a lazily-loaded standalone path. entry: FileTreeEntry, /// Set of expanded folder paths within this root expanded_folders: HashSet<StandardizedPath>, /// Flattened list of items for rendering items: Vec<FileTreeItem>, /// Mouse state handles and draggable state preserved across rebuilds, keyed by path item_states: HashMap<StandardizedPath, (MouseStateHandle, DraggableState)>, /// The remote host this root belongs to, if any. `None` for local roots. remote_host_id: Option<HostId>, } impl RootDirectory { /// Returns whether this root is backed by a remote server. fn is_remote(&self) -> bool { self.remote_host_id.is_some() } } pub struct FileTreeView { /// Per-root state, keyed by root path root_directories: HashMap<StandardizedPath, RootDirectory>, /// The displayed directories displayed_directories: Vec<StandardizedPath>, #[cfg(feature = "local_fs")] enablement: CodingPanelEnablementState, #[cfg(feature = "local_fs")] repository_metadata_model: ModelHandle<RepoMetadataModel>, #[cfg(feature = "local_fs")] is_active: bool, /// Identifier of the currently selected item selected_item: Option<FileTreeIdentifier>, /// State for the UniformList list_state: UniformListState, /// Scroll state handle for the NewScrollable wrapper scroll_state: ScrollStateHandle, /// Weak view handle for the FilePicker view_handle: WeakViewHandle<Self>, context_menu: ViewHandle<Menu<FileTreeAction>>, /// State for the context menu, if one is currently open. context_menu_state: Option<ContextMenuState>, /// Unique ID used to position this view. position_id: String, /// The pending edit, if any pending_edit: Option<PendingEdit>, /// Editor view used for editing (renaming, creating) an item in the tree. editor_view: ViewHandle<EditorView>, /// Handle to track the currently focused file active_file_model: Option<ModelHandle<ActiveFileModel>>, has_terminal_session: bool, /// Paths the user explicitly collapsed (per root). /// /// This is used to prevent automatic expansion behavior (e.g. when switching tabs, /// focusing the left panel, or "reveal active file") from overriding the user's intent. /// /// The root header collapse is represented by including the root path itself in the set. explicitly_collapsed: HashMap<StandardizedPath, HashSet<StandardizedPath>>, /// Lazy-loaded paths that this view has registered with the /// [`LocalRepoMetadataModel`] for file watching. #[cfg(feature = "local_fs")] registered_lazy_loaded_paths: HashSet<StandardizedPath>, /// Directory the view wants to focus once its entry becomes available. /// /// Set when a descendant path is absorbed into an ancestor root but the /// descendant's entry has not yet been materialized (e.g. the ancestor /// is still lazy-loading). Re-evaluated on every rebuild; cleared when /// the target is selected by the user or when the target root stops /// being displayed. pending_focus_target: Option<PendingFocusTarget>, /// Whether to show hidden files (dotfiles) in the file tree. show_hidden_files: bool, } /// Directory the file tree wants to focus once its entry becomes available. #[derive(Clone, Debug, PartialEq, Eq)] struct PendingFocusTarget { root: StandardizedPath, path: StandardizedPath, /// Whether this target has already driven a scroll. /// /// The first successful apply scrolls the tree to the target. On /// subsequent rebuilds we still re-apply the selection marker so it /// doesn't get overridden by a default root-header fallback, but we /// intentionally do NOT scroll again — the user may have scrolled /// elsewhere since the initial focus-follow and a late metadata /// update must not snap them back. scrolled: bool, } impl FileTreeView { fn is_explicitly_collapsed(&self, root: &StandardizedPath, path: &StandardizedPath) -> bool { self.explicitly_collapsed .get(root) .is_some_and(|collapsed| collapsed.contains(path)) } /// Returns whether the item identified by `id` belongs to a remote root. fn is_remote_item(&self, id: &FileTreeIdentifier) -> bool { self.root_directories .get(&id.root) .is_some_and(|r| r.is_remote()) } #[cfg(feature = "local_fs")] fn is_active(&self) -> bool { self.is_active } #[cfg(not(feature = "local_fs"))] fn is_active(&self) -> bool { true } #[cfg(feature = "local_fs")] pub fn set_is_active(&mut self, is_active: bool, ctx: &mut ViewContext<Self>) { self.set_is_active_local_fs(is_active, ctx); } #[cfg(not(feature = "local_fs"))] pub fn set_is_active(&mut self, _is_active: bool, _ctx: &mut ViewContext<Self>) {} #[cfg(feature = "local_fs")] fn set_is_active_local_fs(&mut self, is_active: bool, ctx: &mut ViewContext<Self>) { if self.is_active == is_active { return; } self.is_active = is_active; if is_active { self.subscribe_to_repository_metadata(ctx); self.subscribe_to_active_file_model(ctx); self.subscribe_to_code_settings(ctx); self.show_hidden_files = *CodeSettings::as_ref(ctx).show_hidden_files; // Catch up on any repository/file changes that happened while inactive. // Skip remote-backed roots — their data comes from server pushes, // not from local DetectedRepositories / lazy-loading. let local_dirs: Vec<_> = self .displayed_directories .iter() .filter(|p| !self.root_directories.get(p).is_some_and(|r| r.is_remote())) .cloned() .collect(); self.update_directory_contents(&local_dirs, false, ctx); // Catch up on remote roots already in root_directories whose // content may have changed while inactive. We only refresh // existing roots — new remote roots are managed by the // workspace via `set_remote_root_directories`. let existing_remote_ids: Vec<_> = self .root_directories .iter() .filter_map(|(_, root_dir)| { let host_id = root_dir.remote_host_id.as_ref()?; Some(repo_metadata::RemoteRepositoryIdentifier::new( host_id.clone(), root_dir.entry.root_directory().as_ref().clone(), )) }) .collect(); if !existing_remote_ids.is_empty() { self.insert_or_update_remote_roots(&existing_remote_ids, false, ctx); } } else { ctx.unsubscribe_to_model(&self.repository_metadata_model); self.unsubscribe_from_active_file_model(ctx); self.unsubscribe_from_code_settings(ctx); let repository_metadata_model = self.repository_metadata_model.clone(); let paths: Vec<_> = self.registered_lazy_loaded_paths.drain().collect(); repository_metadata_model.update(ctx, move |model: &mut RepoMetadataModel, ctx| { for path in &paths { model.remove_lazy_loaded_path(path, ctx); } }); } } #[cfg(feature = "local_fs")] fn subscribe_to_repository_metadata(&self, ctx: &mut ViewContext<Self>) { let model = self.repository_metadata_model.clone(); ctx.subscribe_to_model(&model, |me, _, event, ctx| { me.handle_repository_metadata_event(event, ctx); }); } #[cfg(feature = "local_fs")] fn remove_lazy_loaded_entry(&mut self, path: &StandardizedPath, ctx: &mut ViewContext<Self>) { let Some(std_path) = self.registered_lazy_loaded_paths.take(path) else { return; }; let repository_metadata_model = self.repository_metadata_model.clone(); repository_metadata_model.update(ctx, move |model: &mut RepoMetadataModel, ctx| { model.remove_lazy_loaded_path(&std_path, ctx); }); } /// Inserts or updates remote root directories. When `insert_only` is /// true, roots already present in `displayed_directories` are skipped /// (used for catch-up on reactivation). When false, existing roots are /// updated in-place (used for live server push events). /// /// Performs a single `rebuild_flattened_items` / `ctx.notify` at the end /// regardless of how many roots were processed. #[cfg(feature = "local_fs")] fn insert_or_update_remote_roots( &mut self, remote_ids: &[repo_metadata::RemoteRepositoryIdentifier], insert_only: bool, ctx: &mut ViewContext<Self>, ) { use repo_metadata::RepositoryIdentifier; let mut changed = false; for remote_id in remote_ids { let repo_path = remote_id.path.clone(); if insert_only && self.displayed_directories.contains(&repo_path) { continue; } let id = RepositoryIdentifier::Remote(remote_id.clone()); let Some(state) = RepoMetadataModel::as_ref(ctx).get_repository(&id, ctx) else { continue; }; // Remove any existing roots that are ancestors or descendants of the // new path. For example, when the user cd's from /home/user into // /home/user/repo, replace the home directory root with the repo root. self.displayed_directories.retain(|existing| { if *existing == repo_path { return true; } let dominated = existing.starts_with(&repo_path) || repo_path.starts_with(existing); if dominated { self.root_directories.remove(existing); } !dominated }); let host_id = remote_id.host_id.clone(); self.root_directories .entry(repo_path.clone()) .and_modify(|root_dir| { root_dir.entry = state.entry.clone(); root_dir.remote_host_id = Some(host_id.clone()); }) .or_insert_with(|| RootDirectory { entry: state.entry.clone(), expanded_folders: HashSet::new(), items: Vec::new(), item_states: HashMap::new(), remote_host_id: Some(host_id), }); if !self.displayed_directories.contains(&repo_path) { self.displayed_directories.push(repo_path.clone()); } // Auto-expand the root, respecting explicit user collapses. if !self.is_explicitly_collapsed(&repo_path, &repo_path) && let Some(root_dir) = self.root_directories.get_mut(&repo_path) { root_dir.expanded_folders.insert(repo_path); } changed = true; } if changed { self.rebuild_flattened_items(); ctx.notify(); } } #[cfg(feature = "local_fs")] fn handle_repository_metadata_event( &mut self,