6885 lines · 266.4 KB · Rust
RawDownload
mod outline_panel_settings; use std::{    cmp,    collections::BTreeMap,    hash::Hash,    ops::Range,    path::{Path, PathBuf, MAIN_SEPARATOR_STR},    sync::{        atomic::{self, AtomicBool},        Arc, OnceLock,    },    time::Duration,    u32,}; use anyhow::Context as _;use collections::{hash_map, BTreeSet, HashMap, HashSet};use db::kvp::KEY_VALUE_STORE;use editor::{    display_map::ToDisplayPoint,    items::{entry_git_aware_label_color, entry_label_color},    scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},    AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, ExcerptId,    ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,};use file_icons::FileIcons;use fuzzy::{match_strings, StringMatch, StringMatchCandidate};use gpui::{    actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement, App,    AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div,    ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveElement,    IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton,    MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,    StatefulInteractiveElement as _, Styled, Subscription, Task, UniformListScrollHandle,    WeakEntity, Window,};use itertools::Itertools;use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};use project::{File, Fs, Project, ProjectItem};use search::{BufferSearchBar, ProjectSearchView};use serde::{Deserialize, Serialize};use settings::{Settings, SettingsStore};use smol::channel;use theme::{SyntaxTheme, ThemeSettings};use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout};use util::{debug_panic, RangeExt, ResultExt, TryFutureExt};use workspace::{    dock::{DockPosition, Panel, PanelEvent},    item::ItemHandle,    searchable::{SearchEvent, SearchableItem},    ui::{        h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,        HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,        LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable,        Tooltip,    },    OpenInTerminal, WeakItemHandle, Workspace,};use worktree::{Entry, GitEntry, ProjectEntryId, WorktreeId}; actions!(    outline_panel,    [        CollapseAllEntries,        CollapseSelectedEntry,        ExpandAllEntries,        ExpandSelectedEntry,        FoldDirectory,        Open,        RevealInFileManager,        SelectParent,        ToggleActiveEditorPin,        ToggleFocus,        UnfoldDirectory,    ]); const OUTLINE_PANEL_KEY: &str = "OutlinePanel";const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); type Outline = OutlineItem<language::Anchor>;type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>; pub struct OutlinePanel {    fs: Arc<dyn Fs>,    width: Option<Pixels>,    project: Entity<Project>,    workspace: WeakEntity<Workspace>,    active: bool,    pinned: bool,    scroll_handle: UniformListScrollHandle,    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,    focus_handle: FocusHandle,    pending_serialization: Task<Option<()>>,    fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,    fs_entries: Vec<FsEntry>,    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,    collapsed_entries: HashSet<CollapsedEntry>,    unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,    selected_entry: SelectedEntry,    active_item: Option<ActiveItem>,    _subscriptions: Vec<Subscription>,    updating_fs_entries: bool,    updating_cached_entries: bool,    new_entries_for_fs_update: HashSet<ExcerptId>,    fs_entries_update_task: Task<()>,    cached_entries_update_task: Task<()>,    reveal_selection_task: Task<anyhow::Result<()>>,    outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,    excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,    cached_entries: Vec<CachedEntry>,    filter_editor: Entity<Editor>,    mode: ItemsDisplayMode,    show_scrollbar: bool,    vertical_scrollbar_state: ScrollbarState,    horizontal_scrollbar_state: ScrollbarState,    hide_scrollbar_task: Option<Task<()>>,    max_width_item_index: Option<usize>,    preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,} #[derive(Debug)]enum ItemsDisplayMode {    Search(SearchState),    Outline,} #[derive(Debug)]struct SearchState {    kind: SearchKind,    query: String,    matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,    highlight_search_match_tx: channel::Sender<HighlightArguments>,    _search_match_highlighter: Task<()>,    _search_match_notify: Task<()>,} struct HighlightArguments {    multi_buffer_snapshot: MultiBufferSnapshot,    match_range: Range<editor::Anchor>,    search_data: Arc<OnceLock<SearchData>>,} impl SearchState {    fn new(        kind: SearchKind,        query: String,        previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,        new_matches: Vec<Range<editor::Anchor>>,        theme: Arc<SyntaxTheme>,        window: &mut Window,        cx: &mut Context<OutlinePanel>,    ) -> Self {        let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();        let (notify_tx, notify_rx) = channel::unbounded::<()>();        Self {            kind,            query,            matches: new_matches                .into_iter()                .map(|range| {                    let search_data = previous_matches                        .get(&range)                        .map(Arc::clone)                        .unwrap_or_default();                    (range, search_data)                })                .collect(),            highlight_search_match_tx,            _search_match_highlighter: cx.background_spawn(async move {                while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {                    let needs_init = highlight_arguments.search_data.get().is_none();                    let search_data = highlight_arguments.search_data.get_or_init(|| {                        SearchData::new(                            &highlight_arguments.match_range,                            &highlight_arguments.multi_buffer_snapshot,                        )                    });                    if needs_init {                        notify_tx.try_send(()).ok();                    }                     let highlight_data = &search_data.highlights_data;                    if highlight_data.get().is_some() {                        continue;                    }                    let mut left_whitespaces_count = 0;                    let mut non_whitespace_symbol_occurred = false;                    let context_offset_range = search_data                        .context_range                        .to_offset(&highlight_arguments.multi_buffer_snapshot);                    let mut offset = context_offset_range.start;                    let mut context_text = String::new();                    let mut highlight_ranges = Vec::new();                    for mut chunk in highlight_arguments                        .multi_buffer_snapshot                        .chunks(context_offset_range.start..context_offset_range.end, true)                    {                        if !non_whitespace_symbol_occurred {                            for c in chunk.text.chars() {                                if c.is_whitespace() {                                    left_whitespaces_count += c.len_utf8();                                } else {                                    non_whitespace_symbol_occurred = true;                                    break;                                }                            }                        }                         if chunk.text.len() > context_offset_range.end - offset {                            chunk.text = &chunk.text[0..(context_offset_range.end - offset)];                            offset = context_offset_range.end;                        } else {                            offset += chunk.text.len();                        }                        let style = chunk                            .syntax_highlight_id                            .and_then(|highlight| highlight.style(&theme));                        if let Some(style) = style {                            let start = context_text.len();                            let end = start + chunk.text.len();                            highlight_ranges.push((start..end, style));                        }                        context_text.push_str(chunk.text);                        if offset >= context_offset_range.end {                            break;                        }                    }                     highlight_ranges.iter_mut().for_each(|(range, _)| {                        range.start = range.start.saturating_sub(left_whitespaces_count);                        range.end = range.end.saturating_sub(left_whitespaces_count);                    });                    if highlight_data.set(highlight_ranges).ok().is_some() {                        notify_tx.try_send(()).ok();                    }                     let trimmed_text = context_text[left_whitespaces_count..].to_owned();                    debug_assert_eq!(                        trimmed_text, search_data.context_text,                        "Highlighted text that does not match the buffer text"                    );                }            }),            _search_match_notify: cx.spawn_in(window, |outline_panel, mut cx| async move {                loop {                    match notify_rx.recv().await {                        Ok(()) => {}                        Err(_) => break,                    };                    while let Ok(()) = notify_rx.try_recv() {                        //                    }                    let update_result = outline_panel.update(&mut cx, |_, cx| {                        cx.notify();                    });                    if update_result.is_err() {                        break;                    }                }            }),        }    }} #[derive(Debug)]enum SelectedEntry {    Invalidated(Option<PanelEntry>),    Valid(PanelEntry, usize),    None,} impl SelectedEntry {    fn invalidate(&mut self) {        match std::mem::replace(self, SelectedEntry::None) {            Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),            Self::None => *self = Self::Invalidated(None),            other => *self = other,        }    }     fn is_invalidated(&self) -> bool {        matches!(self, Self::Invalidated(_))    }} #[derive(Debug, Clone, Copy, Default)]struct FsChildren {    files: usize,    dirs: usize,} impl FsChildren {    fn may_be_fold_part(&self) -> bool {        self.dirs == 0 || (self.dirs == 1 && self.files == 0)    }} #[derive(Clone, Debug)]struct CachedEntry {    depth: usize,    string_match: Option<StringMatch>,    entry: PanelEntry,} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]enum CollapsedEntry {    Dir(WorktreeId, ProjectEntryId),    File(WorktreeId, BufferId),    ExternalFile(BufferId),    Excerpt(BufferId, ExcerptId),} #[derive(Debug)]struct Excerpt {    range: ExcerptRange<language::Anchor>,    outlines: ExcerptOutlines,} impl Excerpt {    fn invalidate_outlines(&mut self) {        if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {            self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));        }    }     fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {        match &self.outlines {            ExcerptOutlines::Outlines(outlines) => outlines.iter(),            ExcerptOutlines::Invalidated(outlines) => outlines.iter(),            ExcerptOutlines::NotFetched => [].iter(),        }    }     fn should_fetch_outlines(&self) -> bool {        match &self.outlines {            ExcerptOutlines::Outlines(_) => false,            ExcerptOutlines::Invalidated(_) => true,            ExcerptOutlines::NotFetched => true,        }    }} #[derive(Debug)]enum ExcerptOutlines {    Outlines(Vec<Outline>),    Invalidated(Vec<Outline>),    NotFetched,} #[derive(Clone, Debug, PartialEq, Eq)]struct FoldedDirsEntry {    worktree_id: WorktreeId,    entries: Vec<GitEntry>,} // TODO: collapse the inner enums into panel entry#[derive(Clone, Debug)]enum PanelEntry {    Fs(FsEntry),    FoldedDirs(FoldedDirsEntry),    Outline(OutlineEntry),    Search(SearchEntry),} #[derive(Clone, Debug)]struct SearchEntry {    match_range: Range<editor::Anchor>,    kind: SearchKind,    render_data: Arc<OnceLock<SearchData>>,} #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]enum SearchKind {    Project,    Buffer,} #[derive(Clone, Debug)]struct SearchData {    context_range: Range<editor::Anchor>,    context_text: String,    truncated_left: bool,    truncated_right: bool,    search_match_indices: Vec<Range<usize>>,    highlights_data: HighlightStyleData,} impl PartialEq for PanelEntry {    fn eq(&self, other: &Self) -> bool {        match (self, other) {            (Self::Fs(a), Self::Fs(b)) => a == b,            (                Self::FoldedDirs(FoldedDirsEntry {                    worktree_id: worktree_id_a,                    entries: entries_a,                }),                Self::FoldedDirs(FoldedDirsEntry {                    worktree_id: worktree_id_b,                    entries: entries_b,                }),            ) => worktree_id_a == worktree_id_b && entries_a == entries_b,            (Self::Outline(a), Self::Outline(b)) => a == b,            (                Self::Search(SearchEntry {                    match_range: match_range_a,                    kind: kind_a,                    ..                }),                Self::Search(SearchEntry {                    match_range: match_range_b,                    kind: kind_b,                    ..                }),            ) => match_range_a == match_range_b && kind_a == kind_b,            _ => false,        }    }} impl Eq for PanelEntry {} const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;const TRUNCATED_CONTEXT_MARK: &str = "…"; impl SearchData {    fn new(        match_range: &Range<editor::Anchor>,        multi_buffer_snapshot: &MultiBufferSnapshot,    ) -> Self {        let match_point_range = match_range.to_point(multi_buffer_snapshot);        let context_left_border = multi_buffer_snapshot.clip_point(            language::Point::new(                match_point_range.start.row,                match_point_range                    .start                    .column                    .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),            ),            Bias::Left,        );        let context_right_border = multi_buffer_snapshot.clip_point(            language::Point::new(                match_point_range.end.row,                match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,            ),            Bias::Right,        );         let context_anchor_range =            (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);        let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);        let match_offset_range = match_range.to_offset(multi_buffer_snapshot);         let mut search_match_indices = vec![            multi_buffer_snapshot.clip_offset(                match_offset_range.start - context_offset_range.start,                Bias::Left,            )                ..multi_buffer_snapshot.clip_offset(                    match_offset_range.end - context_offset_range.start,                    Bias::Right,                ),        ];         let entire_context_text = multi_buffer_snapshot            .text_for_range(context_offset_range.clone())            .collect::<String>();        let left_whitespaces_offset = entire_context_text            .chars()            .take_while(|c| c.is_whitespace())            .map(|c| c.len_utf8())            .sum::<usize>();         let mut extended_context_left_border = context_left_border;        extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);        let extended_context_left_border =            multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);        let mut extended_context_right_border = context_right_border;        extended_context_right_border.column += 1;        let extended_context_right_border =            multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);         let truncated_left = left_whitespaces_offset == 0            && extended_context_left_border < context_left_border            && multi_buffer_snapshot                .chars_at(extended_context_left_border)                .last()                .map_or(false, |c| !c.is_whitespace());        let truncated_right = entire_context_text            .chars()            .last()            .map_or(true, |c| !c.is_whitespace())            && extended_context_right_border > context_right_border            && multi_buffer_snapshot                .chars_at(extended_context_right_border)

Lines 1–500 of 6885.