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. Load more
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)