9757 lines · 361.9 KB · Rust
RawDownload
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(&current_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.