3187 lines · 122.9 KB · Rust
RawDownload
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.