mod outline_panel_settings; use std::{ cmp, ops::Range, path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, time::Duration, }; use anyhow::Context; 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::ScrollAnchor, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange, }; use file_icons::FileIcons; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings}; use project::{File, Fs, Item, Project}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use util::{RangeExt, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::ItemHandle, ui::{ h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon, IconName, IconSize, Label, LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, }, OpenInTerminal, Workspace, }; use worktree::{Entry, ProjectEntryId, WorktreeId}; actions!( outline_panel, [ ExpandSelectedEntry, CollapseSelectedEntry, ExpandAllEntries, CollapseAllEntries, CopyPath, CopyRelativePath, RevealInFileManager, Open, ToggleFocus, UnfoldDirectory, FoldDirectory, SelectParent, ] ); const OUTLINE_PANEL_KEY: &str = "OutlinePanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); type Outline = OutlineItem; pub struct OutlinePanel { fs: Arc, width: Option, project: Model, active: bool, scroll_handle: UniformListScrollHandle, context_menu: Option<(View, Point, Subscription)>, focus_handle: FocusHandle, pending_serialization: Task>, fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>, fs_entries: Vec, fs_children_count: HashMap, FsChildren>>, collapsed_entries: HashSet, unfolded_dirs: HashMap>, selected_entry: Option, active_item: Option, _subscriptions: Vec, updating_fs_entries: bool, fs_entries_update_task: Task<()>, cached_entries_update_task: Task<()>, outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>, excerpts: HashMap>, cached_entries_with_depth: Vec, filter_editor: View, } #[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, entry: EntryOwned, } #[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, 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 { 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), Invalidated(Vec), NotFetched, } #[derive(Clone, Debug, PartialEq, Eq)] enum EntryOwned { Entry(FsEntry), FoldedDirs(WorktreeId, Vec), Excerpt(BufferId, ExcerptId, ExcerptRange), Outline(BufferId, ExcerptId, Outline), } impl EntryOwned { fn to_ref_entry(&self) -> EntryRef<'_> { match self { Self::Entry(entry) => EntryRef::Entry(entry), Self::FoldedDirs(worktree_id, dirs) => EntryRef::FoldedDirs(*worktree_id, dirs), Self::Excerpt(buffer_id, excerpt_id, range) => { EntryRef::Excerpt(*buffer_id, *excerpt_id, range) } Self::Outline(buffer_id, excerpt_id, outline) => { EntryRef::Outline(*buffer_id, *excerpt_id, outline) } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum EntryRef<'a> { Entry(&'a FsEntry), FoldedDirs(WorktreeId, &'a [Entry]), Excerpt(BufferId, ExcerptId, &'a ExcerptRange), Outline(BufferId, ExcerptId, &'a Outline), } impl EntryRef<'_> { fn to_owned_entry(&self) -> EntryOwned { match self { &Self::Entry(entry) => EntryOwned::Entry(entry.clone()), &Self::FoldedDirs(worktree_id, dirs) => { EntryOwned::FoldedDirs(worktree_id, dirs.to_vec()) } &Self::Excerpt(buffer_id, excerpt_id, range) => { EntryOwned::Excerpt(buffer_id, excerpt_id, range.clone()) } &Self::Outline(buffer_id, excerpt_id, outline) => { EntryOwned::Outline(buffer_id, excerpt_id, outline.clone()) } } } } #[derive(Clone, Debug, Eq)] enum FsEntry { ExternalFile(BufferId, Vec), Directory(WorktreeId, Entry), File(WorktreeId, Entry, BufferId, Vec), } impl PartialEq for FsEntry { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b, (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => { id_a == id_b && entry_a.id == entry_b.id } ( Self::File(worktree_a, entry_a, id_a, ..), Self::File(worktree_b, entry_b, id_b, ..), ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b, _ => false, } } } struct ActiveItem { item_id: EntityId, active_editor: WeakView, _editor_subscrpiption: Subscription, } #[derive(Debug)] pub enum Event { Focus, } #[derive(Serialize, Deserialize)] struct SerializedOutlinePanel { width: Option, active: Option, } pub fn init_settings(cx: &mut AppContext) { OutlinePanelSettings::register(cx); } pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_icons::init(assets, cx); cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); }); }) .detach(); } impl OutlinePanel { pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, ) -> anyhow::Result> { let serialized_panel = cx .background_executor() .spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) }) .await .context("loading outline panel") .log_err() .flatten() .map(|panel| serde_json::from_str::(&panel)) .transpose() .log_err() .flatten(); workspace.update(&mut cx, |workspace, cx| { let panel = Self::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|px| px.round()); panel.active = serialized_panel.active.unwrap_or(false); cx.notify(); }); } panel }) } fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); let outline_panel = cx.new_view(|cx| { let filter_editor = cx.new_view(|cx| { let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Filter...", cx); editor }); let filter_update_subscription = cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx); } }); let focus_handle = cx.focus_handle(); let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in); let workspace_subscription = cx.subscribe( &workspace .weak_handle() .upgrade() .expect("have a &mut Workspace"), move |outline_panel, workspace, event, cx| { if let workspace::Event::ActiveItemChanged = event { if let Some(new_active_editor) = workspace .read(cx) .active_item(cx) .and_then(|item| item.act_as::(cx)) { let active_editor_updated = outline_panel .active_item .as_ref() .map_or(true, |active_item| { active_item.item_id != new_active_editor.item_id() }); if active_editor_updated { outline_panel.replace_visible_entries(new_active_editor, cx); } } else { outline_panel.clear_previous(cx); cx.notify(); } } }, ); let icons_subscription = cx.observe_global::(|_, cx| { cx.notify(); }); let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx); let settings_subscription = cx.observe_global::(move |_, cx| { let new_settings = *OutlinePanelSettings::get_global(cx); if outline_panel_settings != new_settings { outline_panel_settings = new_settings; cx.notify(); } }); let mut outline_panel = Self { active: false, project: project.clone(), fs: workspace.app_state().fs.clone(), scroll_handle: UniformListScrollHandle::new(), focus_handle, filter_editor, fs_entries: Vec::new(), fs_entries_depth: HashMap::default(), fs_children_count: HashMap::default(), collapsed_entries: HashSet::default(), unfolded_dirs: HashMap::default(), selected_entry: None, context_menu: None, width: None, active_item: None, pending_serialization: Task::ready(None), updating_fs_entries: false, fs_entries_update_task: Task::ready(()), cached_entries_update_task: Task::ready(()), outline_fetch_tasks: HashMap::default(), excerpts: HashMap::default(), cached_entries_with_depth: Vec::new(), _subscriptions: vec![ settings_subscription, icons_subscription, focus_subscription, workspace_subscription, filter_update_subscription, ], }; if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) { outline_panel.replace_visible_entries(editor, cx); } outline_panel }); outline_panel } fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; let active = Some(self.active); self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( OUTLINE_PANEL_KEY.into(), serde_json::to_string(&SerializedOutlinePanel { width, active })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn dispatch_context(&self, _: &ViewContext) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("OutlinePanel"); dispatch_context.add("menu"); dispatch_context } fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext) { if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry { self.unfolded_dirs .entry(*worktree_id) .or_default() .extend(entries.iter().map(|entry| entry.id)); self.update_cached_entries(None, cx); } } fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext) { let (worktree_id, entry) = match &self.selected_entry { Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, entry))) => { (worktree_id, Some(entry)) } Some(EntryOwned::FoldedDirs(worktree_id, entries)) => (worktree_id, entries.last()), _ => return, }; let Some(entry) = entry else { return; }; let unfolded_dirs = self.unfolded_dirs.get_mut(worktree_id); let worktree = self .project .read(cx) .worktree_for_id(*worktree_id, cx) .map(|w| w.read(cx).snapshot()); let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else { return; }; unfolded_dirs.remove(&entry.id); self.update_cached_entries(None, cx); } fn open(&mut self, _: &Open, cx: &mut ViewContext) { if self.filter_editor.focus_handle(cx).is_focused(cx) { cx.propagate() } else if let Some(selected_entry) = self.selected_entry.clone() { self.open_entry(&selected_entry, cx); } } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { if self.filter_editor.focus_handle(cx).is_focused(cx) { self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); } }); } else { cx.focus_view(&self.filter_editor); } if self.context_menu.is_some() { self.context_menu.take(); cx.notify(); } } fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext) { let Some(active_editor) = self .active_item .as_ref() .and_then(|item| item.active_editor.upgrade()) else { return; }; let active_multi_buffer = active_editor.read(cx).buffer().clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); let offset_from_top = if active_multi_buffer.read(cx).is_singleton() { Point::default() } else { Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32)) }; self.toggle_expanded(entry, cx); match entry { EntryOwned::FoldedDirs(..) | EntryOwned::Entry(FsEntry::Directory(..)) => {} EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => { let scroll_target = multi_buffer_snapshot.excerpts().find_map( |(excerpt_id, buffer_snapshot, excerpt_range)| { if &buffer_snapshot.remote_id() == buffer_id { multi_buffer_snapshot .anchor_in_excerpt(excerpt_id, excerpt_range.context.start) } else { None } }, ); if let Some(anchor) = scroll_target { self.selected_entry = Some(entry.clone()); active_editor.update(cx, |editor, cx| { editor.set_scroll_anchor( ScrollAnchor { offset: offset_from_top, anchor, }, cx, ); }) } } EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => { let scroll_target = self .project .update(cx, |project, cx| { project .path_for_entry(file_entry.id, cx) .and_then(|path| project.get_open_buffer(&path, cx)) }) .map(|buffer| { active_multi_buffer .read(cx) .excerpts_for_buffer(&buffer, cx) }) .and_then(|excerpts| { let (excerpt_id, excerpt_range) = excerpts.first()?; multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start) }); if let Some(anchor) = scroll_target { self.selected_entry = Some(entry.clone()); active_editor.update(cx, |editor, cx| { editor.set_scroll_anchor( ScrollAnchor { offset: offset_from_top, anchor, }, cx, ); }) } } EntryOwned::Outline(_, excerpt_id, outline) => { let scroll_target = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, outline.range.start) .or_else(|| { multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end) }); if let Some(anchor) = scroll_target { self.selected_entry = Some(entry.clone()); active_editor.update(cx, |editor, cx| { editor.set_scroll_anchor( ScrollAnchor { offset: Point::default(), anchor, }, cx, ); }) } } EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => { let scroll_target = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start); if let Some(anchor) = scroll_target { self.selected_entry = Some(entry.clone()); active_editor.update(cx, |editor, cx| { editor.set_scroll_anchor( ScrollAnchor { offset: Point::default(), anchor, }, cx, ); }) } } } } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| { self.cached_entries_with_depth .iter() .map(|cached_entry| &cached_entry.entry) .skip_while(|entry| entry != &&selected_entry) .skip(1) .next() .cloned() }) { self.selected_entry = Some(entry_to_select); self.autoscroll(cx); cx.notify(); } else { self.select_first(&SelectFirst {}, cx) } } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| { self.cached_entries_with_depth .iter() .rev() .map(|cached_entry| &cached_entry.entry) .skip_while(|entry| entry != &&selected_entry) .skip(1) .next() .cloned() }) { self.selected_entry = Some(entry_to_select); self.autoscroll(cx); cx.notify(); } else { self.select_first(&SelectFirst {}, cx) } } fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext) { if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| { let mut previous_entries = self .cached_entries_with_depth .iter() .rev() .map(|cached_entry| &cached_entry.entry) .skip_while(|entry| entry != &&selected_entry) .skip(1); match &selected_entry { EntryOwned::Entry(fs_entry) => match fs_entry { FsEntry::ExternalFile(..) => None, FsEntry::File(worktree_id, entry, ..) | FsEntry::Directory(worktree_id, entry) => { entry.path.parent().and_then(|parent_path| { previous_entries.find(|entry| match entry { EntryOwned::Entry(FsEntry::Directory( dir_worktree_id, dir_entry, )) => { dir_worktree_id == worktree_id && dir_entry.path.as_ref() == parent_path } EntryOwned::FoldedDirs(dirs_worktree_id, dirs) => { dirs_worktree_id == worktree_id && dirs .first() .map_or(false, |dir| dir.path.as_ref() == parent_path) } _ => false, }) }) } }, EntryOwned::FoldedDirs(worktree_id, entries) => entries .first() .and_then(|entry| entry.path.parent()) .and_then(|parent_path| { previous_entries.find(|entry| { if let EntryOwned::Entry(FsEntry::Directory( dir_worktree_id, dir_entry, )) = entry { dir_worktree_id == worktree_id && dir_entry.path.as_ref() == parent_path } else { false } }) }), EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => { previous_entries.find(|entry| match entry { EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => { file_buffer_id == excerpt_buffer_id && file_excerpts.contains(&excerpt_id) } EntryOwned::Entry(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => { file_buffer_id == excerpt_buffer_id && file_excerpts.contains(&excerpt_id) } _ => false, }) } EntryOwned::Outline(outline_buffer_id, outline_excerpt_id, _) => previous_entries .find(|entry| { if let EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) = entry { outline_buffer_id == excerpt_buffer_id && outline_excerpt_id == excerpt_id } else { false } }), } }) { self.selected_entry = Some(entry_to_select.clone()); self.autoscroll(cx); cx.notify(); } else { self.select_first(&SelectFirst {}, cx); } } fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { if let Some(first_entry) = self.cached_entries_with_depth.iter().next() { self.selected_entry = Some(first_entry.entry.clone()); self.autoscroll(cx); cx.notify(); } } fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { if let Some(new_selection) = self .cached_entries_with_depth .iter() .rev() .map(|cached_entry| &cached_entry.entry) .next() { self.selected_entry = Some(new_selection.clone()); self.autoscroll(cx); cx.notify(); } } fn autoscroll(&mut self, cx: &mut ViewContext) { if let Some(selected_entry) = self.selected_entry.clone() { let index = self .cached_entries_with_depth .iter() .position(|cached_entry| cached_entry.entry == selected_entry); if let Some(index) = index { self.scroll_handle.scroll_to_item(index); cx.notify(); } } } fn focus_in(&mut self, cx: &mut ViewContext) { if !self.focus_handle.contains_focused(cx) { cx.emit(Event::Focus); } } fn deploy_context_menu( &mut self, position: Point, entry: EntryRef<'_>, cx: &mut ViewContext, ) { self.selected_entry = Some(entry.to_owned_entry()); let is_root = match entry { EntryRef::Entry(FsEntry::File(worktree_id, entry, ..)) | EntryRef::Entry(FsEntry::Directory(worktree_id, entry)) => self .project .read(cx) .worktree_for_id(*worktree_id, cx) .map(|worktree| { worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) }) .unwrap_or(false), EntryRef::FoldedDirs(worktree_id, entries) => entries .first() .and_then(|entry| { self.project .read(cx) .worktree_for_id(worktree_id, cx) .map(|worktree| { worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) }) }) .unwrap_or(false), EntryRef::Entry(FsEntry::ExternalFile(..)) => false, EntryRef::Excerpt(..) => { cx.notify(); return; } EntryRef::Outline(..) => { cx.notify(); return; } }; let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(entry); let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(entry); let context_menu = ContextMenu::build(cx, |menu, _| { menu.context(self.focus_handle.clone()) .when(cfg!(target_os = "macos"), |menu| { menu.action("Reveal in Finder", Box::new(RevealInFileManager)) }) .when(cfg!(not(target_os = "macos")), |menu| { menu.action("Reveal in File Manager", Box::new(RevealInFileManager)) }) .action("Open in Terminal", Box::new(OpenInTerminal)) .when(is_unfoldable, |menu| { menu.action("Unfold Directory", Box::new(UnfoldDirectory)) }) .when(is_foldable, |menu| { menu.action("Fold Directory", Box::new(FoldDirectory)) }) .separator() .action("Copy Path", Box::new(CopyPath)) .action("Copy Relative Path", Box::new(CopyRelativePath)) }); cx.focus_view(&context_menu); let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| { outline_panel.context_menu.take(); cx.notify(); }); self.context_menu = Some((context_menu, position, subscription)); cx.notify(); } fn is_unfoldable(&self, entry: EntryRef) -> bool { matches!(entry, EntryRef::FoldedDirs(..)) } fn is_foldable(&self, entry: EntryRef) -> bool { let (directory_worktree, directory_entry) = match entry { EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => { (*directory_worktree, Some(directory_entry)) } _ => return false, }; let Some(directory_entry) = directory_entry else { return false; }; if self .unfolded_dirs .get(&directory_worktree) .map_or(true, |unfolded_dirs| { !unfolded_dirs.contains(&directory_entry.id) }) { return false; } let children = self .fs_children_count .get(&directory_worktree) .and_then(|entries| entries.get(&directory_entry.path)) .copied() .unwrap_or_default(); children.may_be_fold_part() && children.dirs > 0 } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { let entry_to_expand = match &self.selected_entry { Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries .last() .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)), Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => { Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id)) } Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => { Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => { Some(CollapsedEntry::ExternalFile(*buffer_id)) } Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => { Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) } None | Some(EntryOwned::Outline(..)) => None, }; let Some(collapsed_entry) = entry_to_expand else { return; }; let expanded = self.collapsed_entries.remove(&collapsed_entry); if expanded { if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry { self.project.update(cx, |project, cx| { project.expand_entry(worktree_id, dir_entry_id, cx); }); } self.update_cached_entries(None, cx); } else { self.select_next(&SelectNext, cx) } } fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { match &self.selected_entry { Some( dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)), ) => { self.collapsed_entries .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id)); self.selected_entry = Some(dir_entry.clone()); self.update_cached_entries(None, cx); } Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => { self.collapsed_entries .insert(CollapsedEntry::File(*worktree_id, *buffer_id)); self.selected_entry = Some(file_entry.clone()); self.update_cached_entries(None, cx); } Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => { self.collapsed_entries .insert(CollapsedEntry::ExternalFile(*buffer_id)); self.selected_entry = Some(file_entry.clone()); self.update_cached_entries(None, cx); } Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => { if let Some(dir_entry) = dir_entries.last() { if self .collapsed_entries .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id)) { self.selected_entry = Some(dirs_entry.clone()); self.update_cached_entries(None, cx); } } } Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => { if self .collapsed_entries .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) { self.selected_entry = Some(excerpt_entry.clone()); self.update_cached_entries(None, cx); } } None | Some(EntryOwned::Outline(..)) => {} } } pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext) { let expanded_entries = self.fs_entries .iter() .fold(HashSet::default(), |mut entries, fs_entry| { match fs_entry { FsEntry::ExternalFile(buffer_id, _) => { entries.insert(CollapsedEntry::ExternalFile(*buffer_id)); entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map( |excerpts| { excerpts.iter().map(|(excerpt_id, _)| { CollapsedEntry::Excerpt(*buffer_id, *excerpt_id) }) }, )); } FsEntry::Directory(worktree_id, entry) => { entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id)); } FsEntry::File(worktree_id, _, buffer_id, _) => { entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id)); entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map( |excerpts| { excerpts.iter().map(|(excerpt_id, _)| { CollapsedEntry::Excerpt(*buffer_id, *excerpt_id) }) }, )); } } entries }); self.collapsed_entries .retain(|entry| !expanded_entries.contains(entry)); self.update_cached_entries(None, cx); } pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { let new_entries = self .cached_entries_with_depth .iter() .flat_map(|cached_entry| match &cached_entry.entry { EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => { Some(CollapsedEntry::Dir(*worktree_id, entry.id)) } EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => { Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => { Some(CollapsedEntry::ExternalFile(*buffer_id)) } EntryOwned::FoldedDirs(worktree_id, entries) => { Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)) } EntryOwned::Excerpt(buffer_id, excerpt_id, _) => { Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) } EntryOwned::Outline(..) => None, }) .collect::>(); self.collapsed_entries.extend(new_entries); self.update_cached_entries(None, cx); } fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext) { match entry { EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => { let entry_id = dir_entry.id; let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); if self.collapsed_entries.remove(&collapsed_entry) { self.project .update(cx, |project, cx| { project.expand_entry(*worktree_id, entry_id, cx) }) .unwrap_or_else(|| Task::ready(Ok(()))) .detach_and_log_err(cx); } else { self.collapsed_entries.insert(collapsed_entry); } } EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => { let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id); if !self.collapsed_entries.remove(&collapsed_entry) { self.collapsed_entries.insert(collapsed_entry); } } EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => { let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id); if !self.collapsed_entries.remove(&collapsed_entry) { self.collapsed_entries.insert(collapsed_entry); } } EntryOwned::FoldedDirs(worktree_id, dir_entries) => { if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) { let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); if self.collapsed_entries.remove(&collapsed_entry) { self.project .update(cx, |project, cx| { project.expand_entry(*worktree_id, entry_id, cx) }) .unwrap_or_else(|| Task::ready(Ok(()))) .detach_and_log_err(cx); } else { self.collapsed_entries.insert(collapsed_entry); } } } EntryOwned::Excerpt(buffer_id, excerpt_id, _) => { let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id); if !self.collapsed_entries.remove(&collapsed_entry) { self.collapsed_entries.insert(collapsed_entry); } } EntryOwned::Outline(..) => return, } self.selected_entry = Some(entry.clone()); self.update_cached_entries(None, cx); } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { if let Some(clipboard_text) = self .selected_entry .as_ref() .and_then(|entry| self.abs_path(&entry, cx)) .map(|p| p.to_string_lossy().to_string()) { cx.write_to_clipboard(ClipboardItem::new(clipboard_text)); } } fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { if let Some(clipboard_text) = self .selected_entry .as_ref() .and_then(|entry| match entry { EntryOwned::Entry(entry) => self.relative_path(&entry, cx), EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()), EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None, }) .map(|p| p.to_string_lossy().to_string()) { cx.write_to_clipboard(ClipboardItem::new(clipboard_text)); } } fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext) { if let Some(abs_path) = self .selected_entry .as_ref() .and_then(|entry| self.abs_path(&entry, cx)) { cx.reveal_path(&abs_path); } } fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { let selected_entry = self.selected_entry.as_ref(); let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx)); let working_directory = if let ( Some(abs_path), Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))), ) = (&abs_path, selected_entry) { abs_path.parent().map(|p| p.to_owned()) } else { abs_path }; if let Some(working_directory) = working_directory { cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone()) } } fn reveal_entry_for_selection( &mut self, editor: &View, cx: &mut ViewContext<'_, Self>, ) { if !OutlinePanelSettings::get_global(cx).auto_reveal_entries { return; } let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else { self.selected_entry = None; cx.notify(); return; }; let related_buffer_entry = match entry_with_selection { EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => { let project = self.project.read(cx); let entry_id = project .buffer_for_id(buffer_id, cx) .and_then(|buffer| buffer.read(cx).entry_id(cx)); project .worktree_for_id(worktree_id, cx) .zip(entry_id) .and_then(|(worktree, entry_id)| { let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); Some((worktree, entry)) }) } EntryOwned::Outline(buffer_id, excerpt_id, _) | EntryOwned::Excerpt(buffer_id, excerpt_id, _) => { self.collapsed_entries .remove(&CollapsedEntry::ExternalFile(buffer_id)); self.collapsed_entries .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); let project = self.project.read(cx); let entry_id = project .buffer_for_id(buffer_id, cx) .and_then(|buffer| buffer.read(cx).entry_id(cx)); entry_id.and_then(|entry_id| { project .worktree_for_entry(entry_id, cx) .and_then(|worktree| { let worktree_id = worktree.read(cx).id(); self.collapsed_entries .remove(&CollapsedEntry::File(worktree_id, buffer_id)); let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); Some((worktree, entry)) }) }) } EntryOwned::Entry(FsEntry::ExternalFile(..)) => None, _ => return, }; if let Some((worktree, buffer_entry)) = related_buffer_entry { let worktree_id = worktree.read(cx).id(); let mut dirs_to_expand = Vec::new(); { let mut traversal = worktree.read(cx).traverse_from_path( true, true, true, buffer_entry.path.as_ref(), ); let mut current_entry = buffer_entry; loop { if current_entry.is_dir() { if self .collapsed_entries .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id)) { dirs_to_expand.push(current_entry.id); } } if traversal.back_to_parent() { if let Some(parent_entry) = traversal.entry() { current_entry = parent_entry.clone(); continue; } } break; } } for dir_to_expand in dirs_to_expand { self.project .update(cx, |project, cx| { project.expand_entry(worktree_id, dir_to_expand, cx) }) .unwrap_or_else(|| Task::ready(Ok(()))) .detach_and_log_err(cx) } } self.selected_entry = Some(entry_with_selection); self.update_cached_entries(None, cx); self.autoscroll(cx); } fn render_excerpt( &self, buffer_id: BufferId, excerpt_id: ExcerptId, range: &ExcerptRange, depth: usize, cx: &mut ViewContext, ) -> Option> { let item_id = ElementId::from(excerpt_id.to_proto() as usize); let is_active = match &self.selected_entry { Some(EntryOwned::Excerpt(selected_buffer_id, selected_excerpt_id, _)) => { selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id } _ => false, }; let has_outlines = self .excerpts .get(&buffer_id) .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines { ExcerptOutlines::Outlines(outlines) => Some(outlines), ExcerptOutlines::Invalidated(outlines) => Some(outlines), ExcerptOutlines::NotFetched => None, }) .map_or(false, |outlines| !outlines.is_empty()); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); let color = entry_git_aware_label_color(None, false, is_active); let icon = if has_outlines { FileIcons::get_chevron_icon(is_expanded, cx) .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) } else { None } .unwrap_or_else(empty_icon); let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?; let excerpt_range = range.context.to_point(&buffer_snapshot); let label_element = Label::new(format!( "Lines {}-{}", excerpt_range.start.row + 1, excerpt_range.end.row + 1, )) .single_line() .color(color) .into_any_element(); Some(self.entry_element( EntryRef::Excerpt(buffer_id, excerpt_id, range), item_id, depth, Some(icon), is_active, label_element, cx, )) } fn render_outline( &self, buffer_id: BufferId, excerpt_id: ExcerptId, rendered_outline: &Outline, depth: usize, string_match: Option<&StringMatch>, cx: &mut ViewContext, ) -> Stateful
{ let (item_id, label_element) = ( ElementId::from(SharedString::from(format!( "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}", rendered_outline.range, &rendered_outline.text, ))), language::render_item( &rendered_outline, string_match .map(|string_match| string_match.ranges().collect::>()) .unwrap_or_default(), cx, ) .into_any_element(), ); let is_active = match &self.selected_entry { Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => { selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id && selected_entry == rendered_outline } _ => false, }; let icon = if self.is_singleton_active(cx) { None } else { Some(empty_icon()) }; self.entry_element( EntryRef::Outline(buffer_id, excerpt_id, rendered_outline), item_id, depth, icon, is_active, label_element, cx, ) } fn render_entry( &self, rendered_entry: &FsEntry, depth: usize, string_match: Option<&StringMatch>, cx: &mut ViewContext, ) -> Stateful
{ let settings = OutlinePanelSettings::get_global(cx); let is_active = match &self.selected_entry { Some(EntryOwned::Entry(selected_entry)) => selected_entry == rendered_entry, _ => false, }; let (item_id, label_element, icon) = match rendered_entry { FsEntry::File(worktree_id, entry, ..) => { let name = self.entry_name(worktree_id, entry, cx); let color = entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active); let icon = if settings.file_icons { FileIcons::get_icon(&entry.path, cx) .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) } else { None }; ( ElementId::from(entry.id.to_proto() as usize), HighlightedLabel::new( name, string_match .map(|string_match| string_match.positions.clone()) .unwrap_or_default(), ) .color(color) .into_any_element(), icon.unwrap_or_else(empty_icon), ) } FsEntry::Directory(worktree_id, entry) => { let name = self.entry_name(worktree_id, entry, cx); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Dir(*worktree_id, entry.id)); let color = entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) } else { FileIcons::get_chevron_icon(is_expanded, cx) } .map(Icon::from_path) .map(|icon| icon.color(color).into_any_element()); ( ElementId::from(entry.id.to_proto() as usize), HighlightedLabel::new( name, string_match .map(|string_match| string_match.positions.clone()) .unwrap_or_default(), ) .color(color) .into_any_element(), icon.unwrap_or_else(empty_icon), ) } FsEntry::ExternalFile(buffer_id, ..) => { let color = entry_label_color(is_active); let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) { Some(buffer_snapshot) => match buffer_snapshot.file() { Some(file) => { let path = file.path(); let icon = if settings.file_icons { FileIcons::get_icon(path.as_ref(), cx) } else { None } .map(Icon::from_path) .map(|icon| icon.color(color).into_any_element()); (icon, file_name(path.as_ref())) } None => (None, "Untitled".to_string()), }, None => (None, "Unknown buffer".to_string()), }; ( ElementId::from(buffer_id.to_proto() as usize), HighlightedLabel::new( name, string_match .map(|string_match| string_match.positions.clone()) .unwrap_or_default(), ) .color(color) .into_any_element(), icon.unwrap_or_else(empty_icon), ) } }; self.entry_element( EntryRef::Entry(rendered_entry), item_id, depth, Some(icon), is_active, label_element, cx, ) } fn render_folded_dirs( &self, worktree_id: WorktreeId, dir_entries: &[Entry], depth: usize, string_match: Option<&StringMatch>, cx: &mut ViewContext, ) -> Stateful
{ let settings = OutlinePanelSettings::get_global(cx); let is_active = match &self.selected_entry { Some(EntryOwned::FoldedDirs(selected_worktree_id, selected_entries)) => { selected_worktree_id == &worktree_id && selected_entries == dir_entries } _ => false, }; let (item_id, label_element, icon) = { let name = self.dir_names_string(dir_entries, worktree_id, cx); let is_expanded = dir_entries.iter().all(|dir| { !self .collapsed_entries .contains(&CollapsedEntry::Dir(worktree_id, dir.id)) }); let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored); let git_status = dir_entries.first().and_then(|entry| entry.git_status); let color = entry_git_aware_label_color(git_status, is_ignored, is_active); let icon = if settings.folder_icons { FileIcons::get_folder_icon(is_expanded, cx) } else { FileIcons::get_chevron_icon(is_expanded, cx) } .map(Icon::from_path) .map(|icon| icon.color(color).into_any_element()); ( ElementId::from( dir_entries .last() .map(|entry| entry.id.to_proto()) .unwrap_or_else(|| worktree_id.to_proto()) as usize, ), HighlightedLabel::new( name, string_match .map(|string_match| string_match.positions.clone()) .unwrap_or_default(), ) .color(color) .into_any_element(), icon.unwrap_or_else(empty_icon), ) }; self.entry_element( EntryRef::FoldedDirs(worktree_id, dir_entries), item_id, depth, Some(icon), is_active, label_element, cx, ) } #[allow(clippy::too_many_arguments)] fn entry_element( &self, rendered_entry: EntryRef<'_>, item_id: ElementId, depth: usize, icon_element: Option, is_active: bool, label_element: gpui::AnyElement, cx: &mut ViewContext, ) -> Stateful
{ let settings = OutlinePanelSettings::get_global(cx); let rendered_entry = rendered_entry.to_owned_entry(); div() .text_ui(cx) .id(item_id.clone()) .child( ListItem::new(item_id) .indent_level(depth) .indent_step_size(px(settings.indent_size)) .selected(is_active) .when_some(icon_element, |list_item, icon_element| { list_item.child(h_flex().child(icon_element)) }) .child(h_flex().h_6().child(label_element).ml_1()) .on_click({ let clicked_entry = rendered_entry.clone(); cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| { if event.down.button == MouseButton::Right || event.down.first_mouse { return; } outline_panel.open_entry(&clicked_entry, cx); }) }) .on_secondary_mouse_down(cx.listener( move |outline_panel, event: &MouseDownEvent, cx| { // Stop propagation to prevent the catch-all context menu for the project // panel from being deployed. cx.stop_propagation(); outline_panel.deploy_context_menu( event.position, rendered_entry.to_ref_entry(), cx, ) }, )), ) .border_1() .border_r_2() .rounded_none() .hover(|style| { if is_active { style } else { let hover_color = cx.theme().colors().ghost_element_hover; style.bg(hover_color).border_color(hover_color) } }) .when(is_active && self.focus_handle.contains_focused(cx), |div| { div.border_color(Color::Selected.color(cx)) }) } fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String { let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) { Some(worktree) => { let worktree = worktree.read(cx); match worktree.snapshot().root_entry() { Some(root_entry) => { if root_entry.id == entry.id { file_name(worktree.abs_path().as_ref()) } else { let path = worktree.absolutize(entry.path.as_ref()).ok(); let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref()); file_name(path) } } None => { let path = worktree.absolutize(entry.path.as_ref()).ok(); let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref()); file_name(path) } } } None => file_name(entry.path.as_ref()), }; name } fn update_fs_entries( &mut self, active_editor: &View, new_entries: HashSet, new_selected_entry: Option, debounce: Option, cx: &mut ViewContext, ) { if !self.active { return; } let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; let active_multi_buffer = active_editor.read(cx).buffer().clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); let mut new_collapsed_entries = self.collapsed_entries.clone(); let mut new_unfolded_dirs = self.unfolded_dirs.clone(); let mut root_entries = HashSet::default(); let mut new_excerpts = HashMap::>::default(); let buffer_excerpts = multi_buffer_snapshot.excerpts().fold( HashMap::default(), |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { let buffer_id = buffer_snapshot.remote_id(); let file = File::from_dyn(buffer_snapshot.file()); let entry_id = file.and_then(|file| file.project_entry_id(cx)); let worktree = file.map(|file| file.worktree.read(cx).snapshot()); let is_new = new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id); buffer_excerpts .entry(buffer_id) .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree)) .1 .push(excerpt_id); let outlines = match self .excerpts .get(&buffer_id) .and_then(|excerpts| excerpts.get(&excerpt_id)) { Some(old_excerpt) => match &old_excerpt.outlines { ExcerptOutlines::Outlines(outlines) => { ExcerptOutlines::Outlines(outlines.clone()) } ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched, ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched, }, None => ExcerptOutlines::NotFetched, }; new_excerpts.entry(buffer_id).or_default().insert( excerpt_id, Excerpt { range: excerpt_range, outlines, }, ); buffer_excerpts }, ); self.updating_fs_entries = true; self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move { if let Some(debounce) = debounce { cx.background_executor().timer(debounce).await; } let Some(( new_collapsed_entries, new_unfolded_dirs, new_fs_entries, new_depth_map, new_children_count, )) = cx .background_executor() .spawn(async move { let mut processed_external_buffers = HashSet::default(); let mut new_worktree_entries = HashMap::)>::default(); let mut worktree_excerpts = HashMap::< WorktreeId, HashMap)>, >::default(); let mut external_excerpts = HashMap::default(); for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts { if is_new { match &worktree { Some(worktree) => { new_collapsed_entries .insert(CollapsedEntry::File(worktree.id(), buffer_id)); } None => { new_collapsed_entries .insert(CollapsedEntry::ExternalFile(buffer_id)); } } } if let Some(worktree) = worktree { let worktree_id = worktree.id(); let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default(); match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() { Some(entry) => { let mut traversal = worktree.traverse_from_path( true, true, true, entry.path.as_ref(), ); let mut entries_to_add = HashSet::default(); worktree_excerpts .entry(worktree_id) .or_default() .insert(entry.id, (buffer_id, excerpts)); let mut current_entry = entry; loop { if current_entry.is_dir() { let is_root = worktree.root_entry().map(|entry| entry.id) == Some(current_entry.id); if is_root { root_entries.insert(current_entry.id); if auto_fold_dirs { unfolded_dirs.insert(current_entry.id); } } if is_new { new_collapsed_entries.remove(&CollapsedEntry::Dir( worktree_id, current_entry.id, )); } } let new_entry_added = entries_to_add.insert(current_entry); if new_entry_added && traversal.back_to_parent() { if let Some(parent_entry) = traversal.entry() { current_entry = parent_entry.clone(); continue; } } break; } new_worktree_entries .entry(worktree_id) .or_insert_with(|| (worktree.clone(), HashSet::default())) .1 .extend(entries_to_add); } None => { if processed_external_buffers.insert(buffer_id) { external_excerpts .entry(buffer_id) .or_insert_with(|| Vec::new()) .extend(excerpts); } } } } else if processed_external_buffers.insert(buffer_id) { external_excerpts .entry(buffer_id) .or_insert_with(|| Vec::new()) .extend(excerpts); } } let mut new_children_count = HashMap::, FsChildren>>::default(); let worktree_entries = new_worktree_entries .into_iter() .map(|(worktree_id, (worktree_snapshot, entries))| { let mut entries = entries.into_iter().collect::>(); // For a proper git status propagation, we have to keep the entries sorted lexicographically. entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); worktree_snapshot.propagate_git_statuses(&mut entries); project::sort_worktree_entries(&mut entries); (worktree_id, entries) }) .flat_map(|(worktree_id, entries)| { { entries .into_iter() .filter_map(|entry| { if auto_fold_dirs { if let Some(parent) = entry.path.parent() { let children = new_children_count .entry(worktree_id) .or_default() .entry(Arc::from(parent)) .or_default(); if entry.is_dir() { children.dirs += 1; } else { children.files += 1; } } } if entry.is_dir() { Some(FsEntry::Directory(worktree_id, entry)) } else { let (buffer_id, excerpts) = worktree_excerpts .get_mut(&worktree_id) .and_then(|worktree_excerpts| { worktree_excerpts.remove(&entry.id) })?; Some(FsEntry::File( worktree_id, entry, buffer_id, excerpts, )) } }) .collect::>() } }) .collect::>(); let mut visited_dirs = Vec::new(); let mut new_depth_map = HashMap::default(); let new_visible_entries = external_excerpts .into_iter() .sorted_by_key(|(id, _)| *id) .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts)) .chain(worktree_entries) .filter(|visible_item| { match visible_item { FsEntry::Directory(worktree_id, dir_entry) => { let parent_id = back_to_common_visited_parent( &mut visited_dirs, worktree_id, dir_entry, ); let depth = if root_entries.contains(&dir_entry.id) { 0 } else { if auto_fold_dirs { let children = new_children_count .get(&worktree_id) .and_then(|children_count| { children_count.get(&dir_entry.path) }) .copied() .unwrap_or_default(); if !children.may_be_fold_part() || (children.dirs == 0 && visited_dirs .last() .map(|(parent_dir_id, _)| { new_unfolded_dirs .get(&worktree_id) .map_or(true, |unfolded_dirs| { unfolded_dirs .contains(&parent_dir_id) }) }) .unwrap_or(true)) { new_unfolded_dirs .entry(*worktree_id) .or_default() .insert(dir_entry.id); } } parent_id .and_then(|(worktree_id, id)| { new_depth_map.get(&(worktree_id, id)).copied() }) .unwrap_or(0) + 1 }; visited_dirs.push((dir_entry.id, dir_entry.path.clone())); new_depth_map.insert((*worktree_id, dir_entry.id), depth); } FsEntry::File(worktree_id, file_entry, ..) => { let parent_id = back_to_common_visited_parent( &mut visited_dirs, worktree_id, file_entry, ); let depth = if root_entries.contains(&file_entry.id) { 0 } else { parent_id .and_then(|(worktree_id, id)| { new_depth_map.get(&(worktree_id, id)).copied() }) .unwrap_or(0) + 1 }; new_depth_map.insert((*worktree_id, file_entry.id), depth); } FsEntry::ExternalFile(..) => { visited_dirs.clear(); } } true }) .collect::>(); anyhow::Ok(( new_collapsed_entries, new_unfolded_dirs, new_visible_entries, new_depth_map, new_children_count, )) }) .await .log_err() else { return; }; outline_panel .update(&mut cx, |outline_panel, cx| { outline_panel.updating_fs_entries = false; outline_panel.excerpts = new_excerpts; outline_panel.collapsed_entries = new_collapsed_entries; outline_panel.unfolded_dirs = new_unfolded_dirs; outline_panel.fs_entries = new_fs_entries; outline_panel.fs_entries_depth = new_depth_map; outline_panel.fs_children_count = new_children_count; outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx); if new_selected_entry.is_some() { outline_panel.selected_entry = new_selected_entry; } outline_panel.fetch_outdated_outlines(cx); outline_panel.autoscroll(cx); cx.notify(); }) .ok(); }); } fn replace_visible_entries( &mut self, new_active_editor: View, cx: &mut ViewContext, ) { let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx); self.clear_previous(cx); self.active_item = Some(ActiveItem { item_id: new_active_editor.item_id(), _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx), active_editor: new_active_editor.downgrade(), }); let new_entries = HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids()); self.update_fs_entries( &new_active_editor, new_entries, new_selected_entry, None, cx, ); } fn clear_previous(&mut self, cx: &mut WindowContext<'_>) { self.filter_editor.update(cx, |editor, cx| editor.clear(cx)); self.collapsed_entries.clear(); self.unfolded_dirs.clear(); self.selected_entry = None; self.fs_entries_update_task = Task::ready(()); self.cached_entries_update_task = Task::ready(()); self.active_item = None; self.fs_entries.clear(); self.fs_entries_depth.clear(); self.fs_children_count.clear(); self.outline_fetch_tasks.clear(); self.excerpts.clear(); self.cached_entries_with_depth = Vec::new(); } fn location_for_editor_selection( &mut self, editor: &View, cx: &mut ViewContext, ) -> Option { let selection = editor .read(cx) .selections .newest::(cx) .head(); let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)); let multi_buffer = editor.read(cx).buffer(); let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); let (excerpt_id, buffer, _) = editor .read(cx) .buffer() .read(cx) .excerpt_containing(selection, cx)?; let buffer_id = buffer.read(cx).remote_id(); let selection_display_point = selection.to_display_point(&editor_snapshot); let excerpt_outlines = self .excerpts .get(&buffer_id) .and_then(|excerpts| excerpts.get(&excerpt_id)) .into_iter() .flat_map(|excerpt| excerpt.iter_outlines()) .flat_map(|outline| { let start = multi_buffer_snapshot .anchor_in_excerpt(excerpt_id, outline.range.start)? .to_display_point(&editor_snapshot); let end = multi_buffer_snapshot .anchor_in_excerpt(excerpt_id, outline.range.end)? .to_display_point(&editor_snapshot); Some((start..end, outline)) }) .collect::>(); let mut matching_outline_indices = Vec::new(); let mut children = HashMap::default(); let mut parents_stack = Vec::<(&Range, &&Outline, usize)>::new(); for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() { if outline_range .to_inclusive() .contains(&selection_display_point) { matching_outline_indices.push(i); } else if (outline_range.start.row()..outline_range.end.row()) .to_inclusive() .contains(&selection_display_point.row()) { matching_outline_indices.push(i); } while let Some((parent_range, parent_outline, _)) = parents_stack.last() { if parent_outline.depth >= outline.depth || !parent_range.contains(&outline_range.start) { parents_stack.pop(); } else { break; } } if let Some((_, _, parent_index)) = parents_stack.last_mut() { children .entry(*parent_index) .or_insert_with(Vec::new) .push(i); } parents_stack.push((outline_range, outline, i)); } let outline_item = matching_outline_indices .into_iter() .flat_map(|i| Some((i, excerpt_outlines.get(i)?))) .filter(|(i, _)| { children .get(i) .map(|children| { children.iter().all(|child_index| { excerpt_outlines .get(*child_index) .map(|(child_range, _)| child_range.start > selection_display_point) .unwrap_or(false) }) }) .unwrap_or(true) }) .min_by_key(|(_, (outline_range, outline))| { let distance_from_start = if outline_range.start > selection_display_point { outline_range.start - selection_display_point } else { selection_display_point - outline_range.start }; let distance_from_end = if outline_range.end > selection_display_point { outline_range.end - selection_display_point } else { selection_display_point - outline_range.end }; ( cmp::Reverse(outline.depth), distance_from_start + distance_from_end, ) }) .map(|(_, (_, outline))| *outline) .cloned(); let closest_container = match outline_item { Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline), None => self .cached_entries_with_depth .iter() .rev() .find_map(|cached_entry| match &cached_entry.entry { EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => { if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id { Some(cached_entry.entry.clone()) } else { None } } EntryOwned::Entry( FsEntry::ExternalFile(file_buffer_id, file_excerpts) | FsEntry::File(_, _, file_buffer_id, file_excerpts), ) => { if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) { Some(cached_entry.entry.clone()) } else { None } } _ => None, })?, }; Some(closest_container) } fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext) { let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx); if excerpt_fetch_ranges.is_empty() { return; } let syntax_theme = cx.theme().syntax().clone(); for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges { for (excerpt_id, excerpt_range) in excerpt_ranges { let syntax_theme = syntax_theme.clone(); let buffer_snapshot = buffer_snapshot.clone(); self.outline_fetch_tasks.insert( (buffer_id, excerpt_id), cx.spawn(|outline_panel, mut cx| async move { let fetched_outlines = cx .background_executor() .spawn(async move { buffer_snapshot .outline_items_containing( excerpt_range.context, false, Some(&syntax_theme), ) .unwrap_or_default() }) .await; outline_panel .update(&mut cx, |outline_panel, cx| { if let Some(excerpt) = outline_panel .excerpts .entry(buffer_id) .or_default() .get_mut(&excerpt_id) { excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); } outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx); }) .ok(); }), ); } } } fn is_singleton_active(&self, cx: &AppContext) -> bool { self.active_item .as_ref() .and_then(|active_item| { Some( active_item .active_editor .upgrade()? .read(cx) .buffer() .read(cx) .is_singleton(), ) }) .unwrap_or(false) } fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { self.outline_fetch_tasks.clear(); let mut ids = ids.into_iter().collect::>(); for excerpts in self.excerpts.values_mut() { ids.retain(|id| { if let Some(excerpt) = excerpts.get_mut(id) { excerpt.invalidate_outlines(); false } else { true } }); if ids.is_empty() { break; } } } fn excerpt_fetch_ranges( &self, cx: &AppContext, ) -> HashMap< BufferId, ( BufferSnapshot, HashMap>, ), > { self.fs_entries .iter() .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| { match fs_entry { FsEntry::File(_, _, buffer_id, file_excerpts) | FsEntry::ExternalFile(buffer_id, file_excerpts) => { let excerpts = self.excerpts.get(&buffer_id); for &file_excerpt in file_excerpts { if let Some(excerpt) = excerpts .and_then(|excerpts| excerpts.get(&file_excerpt)) .filter(|excerpt| excerpt.should_fetch_outlines()) { match excerpts_to_fetch.entry(*buffer_id) { hash_map::Entry::Occupied(mut o) => { o.get_mut().1.insert(file_excerpt, excerpt.range.clone()); } hash_map::Entry::Vacant(v) => { if let Some(buffer_snapshot) = self.buffer_snapshot_for_id(*buffer_id, cx) { v.insert((buffer_snapshot, HashMap::default())) .1 .insert(file_excerpt, excerpt.range.clone()); } } } } } } FsEntry::Directory(..) => {} } excerpts_to_fetch }) } fn buffer_snapshot_for_id( &self, buffer_id: BufferId, cx: &AppContext, ) -> Option { let editor = self.active_item.as_ref()?.active_editor.upgrade()?; Some( editor .read(cx) .buffer() .read(cx) .buffer(buffer_id)? .read(cx) .snapshot(), ) } fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option { match entry { EntryOwned::Entry( FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _), ) => self .buffer_snapshot_for_id(*buffer_id, cx) .and_then(|buffer_snapshot| { let file = File::from_dyn(buffer_snapshot.file())?; file.worktree.read(cx).absolutize(&file.path).ok() }), EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self .project .read(cx) .worktree_for_id(*worktree_id, cx)? .read(cx) .absolutize(&entry.path) .ok(), EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| { self.project .read(cx) .worktree_for_id(*worktree_id, cx) .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok()) }), EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None, } } fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option> { match entry { FsEntry::ExternalFile(buffer_id, _) => { let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?; Some(buffer_snapshot.file()?.path().clone()) } FsEntry::Directory(_, entry) => Some(entry.path.clone()), FsEntry::File(_, entry, ..) => Some(entry.path.clone()), } } fn update_cached_entries( &mut self, debounce: Option, cx: &mut ViewContext, ) { let is_singleton = self.is_singleton_active(cx); let query = self.query(cx); self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move { if let Some(debounce) = debounce { cx.background_executor().timer(debounce).await; } let Some(new_cached_entries) = outline_panel .update(&mut cx, |outline_panel, cx| { outline_panel.generate_cached_entries(is_singleton, query, cx) }) .ok() else { return; }; let new_cached_entries = new_cached_entries.await; outline_panel .update(&mut cx, |outline_panel, cx| { outline_panel.cached_entries_with_depth = new_cached_entries; cx.notify(); }) .ok(); }); } fn generate_cached_entries( &self, is_singleton: bool, query: Option, cx: &mut ViewContext<'_, Self>, ) -> Task> { let project = self.project.clone(); cx.spawn(|outline_panel, mut cx| async move { let mut entries = Vec::new(); let mut match_candidates = Vec::new(); let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| { let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec)>; let track_matches = query.is_some(); let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new(); for entry in &outline_panel.fs_entries { let is_expanded = outline_panel.is_expanded(entry); let (depth, should_add) = match entry { FsEntry::Directory(worktree_id, dir_entry) => { let is_root = project .read(cx) .worktree_for_id(*worktree_id, cx) .map_or(false, |worktree| { worktree.read(cx).root_entry() == Some(dir_entry) }); let folded = auto_fold_dirs && !is_root && outline_panel .unfolded_dirs .get(worktree_id) .map_or(true, |unfolded_dirs| { !unfolded_dirs.contains(&dir_entry.id) }); let fs_depth = outline_panel .fs_entries_depth .get(&(*worktree_id, dir_entry.id)) .copied() .unwrap_or(0); while let Some(&(previous_path, ..)) = parent_dirs.last() { if dir_entry.path.starts_with(previous_path) { break; } parent_dirs.pop(); } let auto_fold = match parent_dirs.last() { Some((parent_path, parent_folded, _, _)) => { *parent_folded && Some(*parent_path) == dir_entry.path.parent() && outline_panel .fs_children_count .get(worktree_id) .and_then(|entries| entries.get(&dir_entry.path)) .copied() .unwrap_or_default() .may_be_fold_part() } None => false, }; let folded = folded || auto_fold; let (depth, parent_expanded) = match parent_dirs.last() { Some(&(_, previous_folded, previous_expanded, previous_depth)) => { let new_depth = if folded && previous_folded { previous_depth } else { previous_depth + 1 }; parent_dirs.push(( &dir_entry.path, folded, previous_expanded && is_expanded, new_depth, )); (new_depth, previous_expanded) } None => { parent_dirs.push(( &dir_entry.path, folded, is_expanded, fs_depth, )); (fs_depth, true) } }; if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) = folded_dirs_entry.take() { if folded && worktree_id == &folded_worktree_id && dir_entry.path.parent() == folded_dirs.last().map(|entry| entry.path.as_ref()) { folded_dirs.push(dir_entry.clone()); folded_dirs_entry = Some((folded_depth, folded_worktree_id, folded_dirs)) } else { if parent_expanded || query.is_some() { let new_folded_dirs = EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs); outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, new_folded_dirs, folded_depth, cx, ); } folded_dirs_entry = Some((depth, *worktree_id, vec![dir_entry.clone()])) } } else if folded { folded_dirs_entry = Some((depth, *worktree_id, vec![dir_entry.clone()])); } let should_add = parent_expanded && folded_dirs_entry.is_none(); (depth, should_add) } FsEntry::ExternalFile(..) => { if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() .find(|(parent_path, ..)| { folded_dirs .iter() .all(|entry| entry.path.as_ref() != *parent_path) }) .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); } } parent_dirs.clear(); (0, true) } FsEntry::File(worktree_id, file_entry, ..) => { if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() .find(|(parent_path, ..)| { folded_dirs .iter() .all(|entry| entry.path.as_ref() != *parent_path) }) .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); } } let fs_depth = outline_panel .fs_entries_depth .get(&(*worktree_id, file_entry.id)) .copied() .unwrap_or(0); while let Some(&(previous_path, ..)) = parent_dirs.last() { if file_entry.path.starts_with(previous_path) { break; } parent_dirs.pop(); } let (depth, should_add) = match parent_dirs.last() { Some(&(_, _, previous_expanded, previous_depth)) => { let new_depth = previous_depth + 1; (new_depth, previous_expanded) } None => (fs_depth, true), }; (depth, should_add) } }; if !is_singleton && (should_add || (query.is_some() && folded_dirs_entry.is_none())) { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::Entry(entry.clone()), depth, cx, ); } let excerpts_to_consider = if is_singleton || query.is_some() || (should_add && is_expanded) { match entry { FsEntry::File(_, _, buffer_id, entry_excerpts) => { Some((*buffer_id, entry_excerpts)) } FsEntry::ExternalFile(buffer_id, entry_excerpts) => { Some((*buffer_id, entry_excerpts)) } _ => None, } } else { None }; if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider { if let Some(excerpts) = outline_panel.excerpts.get(&buffer_id) { for &entry_excerpt in entry_excerpts { let Some(excerpt) = excerpts.get(&entry_excerpt) else { continue; }; let excerpt_depth = depth + 1; outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::Excerpt( buffer_id, entry_excerpt, excerpt.range.clone(), ), excerpt_depth, cx, ); let mut outline_base_depth = excerpt_depth + 1; if is_singleton { outline_base_depth = 0; entries.clear(); match_candidates.clear(); } else if query.is_none() && outline_panel.collapsed_entries.contains( &CollapsedEntry::Excerpt(buffer_id, entry_excerpt), ) { continue; } for outline in excerpt.iter_outlines() { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::Outline( buffer_id, entry_excerpt, outline.clone(), ), outline_base_depth + outline.depth, cx, ); } if is_singleton && entries.is_empty() { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::Entry(entry.clone()), 0, cx, ); } } } } } if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() { let parent_expanded = parent_dirs .iter() .rev() .find(|(parent_path, ..)| { folded_dirs .iter() .all(|entry| entry.path.as_ref() != *parent_path) }) .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, EntryOwned::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); } } }) else { return Vec::new(); }; let Some(query) = query else { return entries; }; let mut matched_ids = match_strings( &match_candidates, &query, true, usize::MAX, &AtomicBool::default(), cx.background_executor().clone(), ) .await .into_iter() .map(|string_match| (string_match.candidate_id, string_match)) .collect::>(); let mut id = 0; entries.retain_mut(|cached_entry| { let retain = match matched_ids.remove(&id) { Some(string_match) => { cached_entry.string_match = Some(string_match); true } None => false, }; id += 1; retain }); entries }) } fn push_entry( &self, entries: &mut Vec, match_candidates: &mut Vec, track_matches: bool, entry: EntryOwned, depth: usize, cx: &AppContext, ) { if track_matches { let id = entries.len(); match &entry { EntryOwned::Entry(fs_entry) => { if let Some(file_name) = self.relative_path(fs_entry, cx).as_deref().map(file_name) { match_candidates.push(StringMatchCandidate { id, string: file_name.to_string(), char_bag: file_name.chars().collect(), }); } } EntryOwned::FoldedDirs(worktree_id, entries) => { let dir_names = self.dir_names_string(entries, *worktree_id, cx); { match_candidates.push(StringMatchCandidate { id, string: dir_names.to_string(), char_bag: dir_names.chars().collect(), }); } } EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate { id, string: outline.text.clone(), char_bag: outline.text.chars().collect(), }), EntryOwned::Excerpt(..) => {} } } entries.push(CachedEntry { depth, entry, string_match: None, }); } fn dir_names_string( &self, entries: &[Entry], worktree_id: WorktreeId, cx: &AppContext, ) -> String { let dir_names_segment = entries .iter() .map(|entry| self.entry_name(&worktree_id, entry, cx)) .collect::(); dir_names_segment.to_string_lossy().to_string() } fn query(&self, cx: &AppContext) -> Option { let query = self.filter_editor.read(cx).text(cx); if query.trim().is_empty() { None } else { Some(query) } } fn is_expanded(&self, entry: &FsEntry) -> bool { let entry_to_check = match entry { FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id), FsEntry::File(worktree_id, _, buffer_id, _) => { CollapsedEntry::File(*worktree_id, *buffer_id) } FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id), }; !self.collapsed_entries.contains(&entry_to_check) } } fn back_to_common_visited_parent( visited_dirs: &mut Vec<(ProjectEntryId, Arc)>, worktree_id: &WorktreeId, new_entry: &Entry, ) -> Option<(WorktreeId, ProjectEntryId)> { while let Some((visited_dir_id, visited_path)) = visited_dirs.last() { match new_entry.path.parent() { Some(parent_path) => { if parent_path == visited_path.as_ref() { return Some((*worktree_id, *visited_dir_id)); } } None => { break; } } visited_dirs.pop(); } None } fn file_name(path: &Path) -> String { let mut current_path = path; loop { if let Some(file_name) = current_path.file_name() { return file_name.to_string_lossy().into_owned(); } match current_path.parent() { Some(parent) => current_path = parent, None => return path.to_string_lossy().into_owned(), } } } impl Panel for OutlinePanel { fn persistent_name() -> &'static str { "Outline Panel" } fn position(&self, cx: &WindowContext) -> DockPosition { match OutlinePanelSettings::get_global(cx).dock { OutlinePanelDockPosition::Left => DockPosition::Left, OutlinePanelDockPosition::Right => DockPosition::Right, } } fn position_is_valid(&self, position: DockPosition) -> bool { matches!(position, DockPosition::Left | DockPosition::Right) } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { settings::update_settings_file::( self.fs.clone(), cx, move |settings, _| { let dock = match position { DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left, DockPosition::Right => OutlinePanelDockPosition::Right, }; settings.dock = Some(dock); }, ); } fn size(&self, cx: &WindowContext) -> Pixels { self.width .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { self.width = size; self.serialize(cx); cx.notify(); } fn icon(&self, cx: &WindowContext) -> Option { OutlinePanelSettings::get_global(cx) .button .then(|| IconName::ListTree) } fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> { Some("Outline Panel") } fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } fn starts_open(&self, _: &WindowContext) -> bool { self.active } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { let old_active = self.active; self.active = active; if active && old_active != active { if let Some(active_editor) = self .active_item .as_ref() .and_then(|item| item.active_editor.upgrade()) { if self.active_item.as_ref().map(|item| item.item_id) == Some(active_editor.item_id()) { let new_selected_entry = self.location_for_editor_selection(&active_editor, cx); self.update_fs_entries( &active_editor, HashSet::default(), new_selected_entry, None, cx, ) } else { self.replace_visible_entries(active_editor, cx); } } } self.serialize(cx); } } impl FocusableView for OutlinePanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.filter_editor.focus_handle(cx).clone() } } impl EventEmitter for OutlinePanel {} impl EventEmitter for OutlinePanel {} impl Render for OutlinePanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let project = self.project.read(cx); let query = self.query(cx); let outline_panel = v_flex() .id("outline-panel") .size_full() .relative() .key_context(self.dispatch_context(cx)) .on_action(cx.listener(Self::open)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_parent)) .on_action(cx.listener(Self::expand_selected_entry)) .on_action(cx.listener(Self::collapse_selected_entry)) .on_action(cx.listener(Self::expand_all_entries)) .on_action(cx.listener(Self::collapse_all_entries)) .on_action(cx.listener(Self::copy_path)) .on_action(cx.listener(Self::copy_relative_path)) .on_action(cx.listener(Self::unfold_directory)) .on_action(cx.listener(Self::fold_directory)) .when(project.is_local(), |el| { el.on_action(cx.listener(Self::reveal_in_finder)) .on_action(cx.listener(Self::open_in_terminal)) }) .on_mouse_down( MouseButton::Right, cx.listener(move |outline_panel, event: &MouseDownEvent, cx| { if let Some(entry) = outline_panel.selected_entry.clone() { outline_panel.deploy_context_menu(event.position, entry.to_ref_entry(), cx) } else if let Some(entry) = outline_panel.fs_entries.first().cloned() { outline_panel.deploy_context_menu( event.position, EntryRef::Entry(&entry), cx, ) } }), ) .track_focus(&self.focus_handle); if self.cached_entries_with_depth.is_empty() { let header = if self.updating_fs_entries { "Loading outlines" } else if query.is_some() { "No matches for query" } else { "No outlines available" }; outline_panel.child( v_flex() .justify_center() .size_full() .child(h_flex().justify_center().child(Label::new(header))) .when_some(query.clone(), |panel, query| { panel.child(h_flex().justify_center().child(Label::new(query))) }) .child( h_flex() .pt(Spacing::Small.rems(cx)) .justify_center() .child({ let keystroke = match self.position(cx) { DockPosition::Left => { cx.keystroke_text_for(&workspace::ToggleLeftDock) } DockPosition::Bottom => { cx.keystroke_text_for(&workspace::ToggleBottomDock) } DockPosition::Right => { cx.keystroke_text_for(&workspace::ToggleRightDock) } }; Label::new(format!("Toggle this panel with {keystroke}")) }), ), ) } else { outline_panel.child({ let items_len = self.cached_entries_with_depth.len(); uniform_list(cx.view().clone(), "entries", items_len, { move |outline_panel, range, cx| { let entries = outline_panel.cached_entries_with_depth.get(range); entries .map(|entries| entries.to_vec()) .unwrap_or_default() .into_iter() .filter_map(|cached_entry| match cached_entry.entry { EntryOwned::Entry(entry) => Some(outline_panel.render_entry( &entry, cached_entry.depth, cached_entry.string_match.as_ref(), cx, )), EntryOwned::FoldedDirs(worktree_id, entries) => { Some(outline_panel.render_folded_dirs( worktree_id, &entries, cached_entry.depth, cached_entry.string_match.as_ref(), cx, )) } EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => { outline_panel.render_excerpt( buffer_id, excerpt_id, &excerpt, cached_entry.depth, cx, ) } EntryOwned::Outline(buffer_id, excerpt_id, outline) => { Some(outline_panel.render_outline( buffer_id, excerpt_id, &outline, cached_entry.depth, cached_entry.string_match.as_ref(), cx, )) } }) .collect() } }) .size_full() .track_scroll(self.scroll_handle.clone()) }) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( anchored() .position(*position) .anchor(gpui::AnchorCorner::TopLeft) .child(menu.clone()), ) .with_priority(1) })) .child( v_flex() .child(div().mx_2().border_primary(cx).border_t_1()) .child(v_flex().p_2().child(self.filter_editor.clone())), ) } } fn subscribe_for_editor_events( editor: &View, cx: &mut ViewContext, ) -> Subscription { let debounce = Some(UPDATE_DEBOUNCE); cx.subscribe( editor, move |outline_panel, editor, e: &EditorEvent, cx| match e { EditorEvent::SelectionsChanged { local: true } => { outline_panel.reveal_entry_for_selection(&editor, cx); cx.notify(); } EditorEvent::ExcerptsAdded { excerpts, .. } => { outline_panel.update_fs_entries( &editor, excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(), None, debounce, cx, ); } EditorEvent::ExcerptsRemoved { ids } => { let mut ids = ids.into_iter().collect::>(); for excerpts in outline_panel.excerpts.values_mut() { excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id)); if ids.is_empty() { break; } } outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx); } EditorEvent::ExcerptsExpanded { ids } => { outline_panel.invalidate_outlines(ids); outline_panel.fetch_outdated_outlines(cx) } EditorEvent::ExcerptsEdited { ids } => { outline_panel.invalidate_outlines(ids); outline_panel.fetch_outdated_outlines(cx); } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { for (_, excerpt) in excerpts { excerpt.invalidate_outlines(); } } outline_panel.fetch_outdated_outlines(cx); } _ => {} }, ) } fn empty_icon() -> AnyElement { h_flex() .size(IconSize::default().rems()) .invisible() .flex_none() .into_any_element() }