diff --git a/Cargo.lock b/Cargo.lock index c0e93600d2..966633f3dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7387,9 +7387,11 @@ dependencies = [ "menu", "project", "schemars", + "search", "serde", "serde_json", "settings", + "theme", "util", "workspace", "worktree", diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg new file mode 100644 index 0000000000..75798fd29f --- /dev/null +++ b/assets/icons/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/unpin.svg b/assets/icons/unpin.svg new file mode 100644 index 0000000000..382ed70b33 --- /dev/null +++ b/assets/icons/unpin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index cc68d0fa63..bbb8450598 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -524,7 +524,7 @@ "ctrl-alt-c": "outline_panel::CopyPath", "alt-ctrl-shift-c": "outline_panel::CopyRelativePath", "alt-ctrl-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::Open", + "space": ["outline_panel::Open", { "change_selection": false }], "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev" } diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 4d588a4c8f..752a05cbee 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -536,7 +536,7 @@ "cmd-alt-c": "outline_panel::CopyPath", "alt-cmd-shift-c": "outline_panel::CopyRelativePath", "alt-cmd-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::Open", + "space": ["outline_panel::Open", { "change_selection": false }], "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev" } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 64a94581e3..5f438f1bd6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1144,16 +1144,37 @@ pub(crate) enum BufferSearchHighlights {} impl SearchableItem for Editor { type Match = Range; + fn get_matches(&self, _: &mut WindowContext) -> Vec> { + self.background_highlights + .get(&TypeId::of::()) + .map_or(Vec::new(), |(_color, ranges)| { + ranges.iter().map(|range| range.clone()).collect() + }) + } + fn clear_matches(&mut self, cx: &mut ViewContext) { - self.clear_background_highlights::(cx); + if self + .clear_background_highlights::(cx) + .is_some() + { + cx.emit(SearchEvent::MatchesInvalidated); + } } fn update_matches(&mut self, matches: &[Range], cx: &mut ViewContext) { + let existing_range = self + .background_highlights + .get(&TypeId::of::()) + .map(|(_, range)| range.as_ref()); + let updated = existing_range != Some(matches); self.highlight_background::( matches, |theme| theme.search_match_background, cx, ); + if updated { + cx.emit(SearchEvent::MatchesInvalidated); + } } fn has_filtered_search_ranges(&mut self) -> bool { diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml index 2b83897646..5314a26e64 100644 --- a/crates/outline_panel/Cargo.toml +++ b/crates/outline_panel/Cargo.toml @@ -26,9 +26,11 @@ log.workspace = true menu.workspace = true project.workspace = true schemars.workspace = true +search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +theme.workspace = true util.workspace = true worktree.workspace = true workspace.workspace = true diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 0f08a56c31..54da15dea1 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1,11 +1,13 @@ mod outline_panel_settings; use std::{ + cell::OnceCell, cmp, ops::Range, path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, time::Duration, + u32, }; use anyhow::Context; @@ -14,18 +16,19 @@ 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, + scroll::{Autoscroll, ScrollAnchor}, + AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, ExcerptId, ExcerptRange, + MultiBufferSnapshot, RangeToAnchorExt, }; 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, + actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement, + AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, + EventEmitter, FocusHandle, FocusableView, HighlightStyle, 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}; @@ -33,36 +36,46 @@ use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings}; use project::{File, Fs, Item, Project}; +use search::{BufferSearchBar, ProjectSearchView}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; +use theme::SyntaxTheme; use util::{RangeExt, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::ItemHandle, + searchable::{SearchEvent, SearchableItem}, ui::{ - h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon, - IconName, IconSize, Label, LabelCommon, ListItem, Selectable, Spacing, StyledExt, - StyledTypography, + h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, + HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label, + LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, Tooltip, }, OpenInTerminal, Workspace, }; use worktree::{Entry, ProjectEntryId, WorktreeId}; +#[derive(Clone, Default, Deserialize, PartialEq)] +pub struct Open { + change_selection: bool, +} + +impl_actions!(outline_panel, [Open]); + actions!( outline_panel, [ - ExpandSelectedEntry, - CollapseSelectedEntry, - ExpandAllEntries, CollapseAllEntries, + CollapseSelectedEntry, CopyPath, CopyRelativePath, + ExpandAllEntries, + ExpandSelectedEntry, + FoldDirectory, + ToggleActiveEditorPin, RevealInFileManager, - Open, + SelectParent, ToggleFocus, UnfoldDirectory, - FoldDirectory, - SelectParent, ] ); @@ -75,7 +88,9 @@ pub struct OutlinePanel { fs: Arc, width: Option, project: Model, + workspace: View, active: bool, + pinned: bool, scroll_handle: UniformListScrollHandle, context_menu: Option<(View, Point, Subscription)>, focus_handle: FocusHandle, @@ -85,16 +100,47 @@ pub struct OutlinePanel { fs_children_count: HashMap, FsChildren>>, collapsed_entries: HashSet, unfolded_dirs: HashMap>, - selected_entry: Option, + selected_entry: SelectedEntry, active_item: Option, _subscriptions: Vec, updating_fs_entries: bool, fs_entries_update_task: Task<()>, cached_entries_update_task: Task<()>, + reveal_selection_task: Task>, outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>, excerpts: HashMap>, - cached_entries_with_depth: Vec, + cached_entries: Vec, filter_editor: View, + mode: ItemsDisplayMode, + search: Option<(SearchKind, String)>, + search_matches: Vec>, +} + +#[derive(Debug)] +enum SelectedEntry { + Invalidated(Option), + Valid(PanelEntry), + 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, PartialEq, Eq)] +enum ItemsDisplayMode { + Search, + Outline, } #[derive(Debug, Clone, Copy, Default)] @@ -113,7 +159,7 @@ impl FsChildren { struct CachedEntry { depth: usize, string_match: Option, - entry: EntryOwned, + entry: PanelEntry, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -161,54 +207,152 @@ enum ExcerptOutlines { NotFetched, } -#[derive(Clone, Debug, PartialEq, Eq)] -enum EntryOwned { - Entry(FsEntry), +#[derive(Clone, Debug)] +enum PanelEntry { + Fs(FsEntry), FoldedDirs(WorktreeId, Vec), + Outline(OutlineEntry), + Search(SearchEntry), +} + +#[derive(Clone, Debug)] +struct SearchEntry { + match_range: Range, + same_line_matches: Vec>, + kind: SearchKind, + render_data: Option>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum SearchKind { + Project, + Buffer, +} + +#[derive(Clone, Debug)] +struct SearchData { + context_range: Range, + context_text: String, + highlight_ranges: Vec<(Range, HighlightStyle)>, + search_match_indices: Vec>, +} + +impl PartialEq for PanelEntry { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Fs(a), Self::Fs(b)) => a == b, + (Self::FoldedDirs(a1, a2), Self::FoldedDirs(b1, b2)) => a1 == b1 && a2 == b2, + (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 {} + +impl SearchData { + fn new( + kind: SearchKind, + match_range: &Range, + multi_buffer_snapshot: &MultiBufferSnapshot, + theme: &SyntaxTheme, + ) -> Self { + let match_point_range = match_range.to_point(&multi_buffer_snapshot); + let entire_row_range_start = language::Point::new(match_point_range.start.row, 0); + let entire_row_range_end = multi_buffer_snapshot.clip_point( + language::Point::new(match_point_range.end.row, u32::MAX), + Bias::Right, + ); + let entire_row_range = + (entire_row_range_start..entire_row_range_end).to_anchors(&multi_buffer_snapshot); + let entire_row_offset_range = entire_row_range.to_offset(&multi_buffer_snapshot); + let match_offset_range = match_range.to_offset(&multi_buffer_snapshot); + let mut search_match_indices = vec![ + match_offset_range.start - entire_row_offset_range.start + ..match_offset_range.end - entire_row_offset_range.start, + ]; + + let mut left_whitespaces_count = 0; + let mut non_whitespace_symbol_occurred = false; + let mut offset = entire_row_offset_range.start; + let mut entire_row_text = String::new(); + let mut highlight_ranges = Vec::new(); + for mut chunk in multi_buffer_snapshot.chunks( + entire_row_offset_range.start..entire_row_offset_range.end, + true, + ) { + if !non_whitespace_symbol_occurred { + for c in chunk.text.chars() { + if c.is_whitespace() { + left_whitespaces_count += 1; + } else { + non_whitespace_symbol_occurred = true; + break; + } + } + } + + if chunk.text.len() > entire_row_offset_range.end - offset { + chunk.text = &chunk.text[0..(entire_row_offset_range.end - offset)]; + offset = entire_row_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 = entire_row_text.len(); + let end = start + chunk.text.len(); + highlight_ranges.push((start..end, style)); + } + entire_row_text.push_str(chunk.text); + if offset >= entire_row_offset_range.end { + break; + } + } + + if let SearchKind::Buffer = kind { + left_whitespaces_count = 0; + } + 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); + }); + search_match_indices.iter_mut().for_each(|range| { + range.start = range.start.saturating_sub(left_whitespaces_count); + range.end = range.end.saturating_sub(left_whitespaces_count); + }); + let trimmed_row_offset_range = + entire_row_offset_range.start + left_whitespaces_count..entire_row_offset_range.end; + let trimmed_text = entire_row_text[left_whitespaces_count..].to_owned(); + Self { + highlight_ranges, + search_match_indices, + context_range: trimmed_row_offset_range.to_anchors(&multi_buffer_snapshot), + context_text: trimmed_text, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum OutlineEntry { 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), @@ -233,8 +377,8 @@ impl PartialEq for FsEntry { } struct ActiveItem { - item_id: EntityId, active_editor: WeakView, + _buffer_search_subscription: Subscription, _editor_subscrpiption: Subscription, } @@ -297,6 +441,7 @@ impl OutlinePanel { fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); + let workspace_handle = cx.view().clone(); let outline_panel = cx.new_view(|cx| { let filter_editor = cx.new_view(|cx| { let mut editor = Editor::single_line(cx); @@ -319,19 +464,11 @@ impl OutlinePanel { .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)) + if let Some(new_active_editor) = + workspace_active_editor(workspace.read(cx), 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); + if outline_panel.should_replace_active_editor(&new_active_editor) { + outline_panel.replace_active_editor(new_active_editor, cx); } } else { outline_panel.clear_previous(cx); @@ -355,18 +492,23 @@ impl OutlinePanel { }); let mut outline_panel = Self { + mode: ItemsDisplayMode::Outline, active: false, - project: project.clone(), + pinned: false, + workspace: workspace_handle, + project, fs: workspace.app_state().fs.clone(), scroll_handle: UniformListScrollHandle::new(), focus_handle, filter_editor, fs_entries: Vec::new(), + search_matches: Vec::new(), + search: None, fs_entries_depth: HashMap::default(), fs_children_count: HashMap::default(), collapsed_entries: HashSet::default(), unfolded_dirs: HashMap::default(), - selected_entry: None, + selected_entry: SelectedEntry::None, context_menu: None, width: None, active_item: None, @@ -374,9 +516,10 @@ impl OutlinePanel { updating_fs_entries: false, fs_entries_update_task: Task::ready(()), cached_entries_update_task: Task::ready(()), + reveal_selection_task: Task::ready(Ok(())), outline_fetch_tasks: HashMap::default(), excerpts: HashMap::default(), - cached_entries_with_depth: Vec::new(), + cached_entries: Vec::new(), _subscriptions: vec![ settings_subscription, icons_subscription, @@ -385,11 +528,8 @@ impl OutlinePanel { 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); + if let Some(editor) = workspace_active_editor(workspace, cx) { + outline_panel.replace_active_editor(editor, cx); } outline_panel }); @@ -422,9 +562,9 @@ impl OutlinePanel { } fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext) { - if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry { + if let Some(PanelEntry::FoldedDirs(worktree_id, entries)) = self.selected_entry().cloned() { self.unfolded_dirs - .entry(*worktree_id) + .entry(worktree_id) .or_default() .extend(entries.iter().map(|entry| entry.id)); self.update_cached_entries(None, cx); @@ -432,21 +572,23 @@ impl OutlinePanel { } 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))) => { + let (worktree_id, entry) = match self.selected_entry().cloned() { + Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, entry))) => { (worktree_id, Some(entry)) } - Some(EntryOwned::FoldedDirs(worktree_id, entries)) => (worktree_id, entries.last()), + Some(PanelEntry::FoldedDirs(worktree_id, entries)) => { + (worktree_id, entries.last().cloned()) + } _ => return, }; let Some(entry) = entry else { return; }; - let unfolded_dirs = self.unfolded_dirs.get_mut(worktree_id); + let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id); let worktree = self .project .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(worktree_id, cx) .map(|w| w.read(cx).snapshot()); let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else { return; @@ -456,23 +598,19 @@ impl OutlinePanel { self.update_cached_entries(None, cx); } - fn open(&mut self, _: &Open, cx: &mut ViewContext) { + fn open(&mut self, open: &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); + } else if let Some(selected_entry) = self.selected_entry().cloned() { + self.open_entry(&selected_entry, open.change_selection, 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); - } - }); + self.focus_handle.focus(cx); } else { - cx.focus_view(&self.filter_editor); + self.filter_editor.focus_handle(cx).focus(cx); } if self.context_menu.is_some() { @@ -481,12 +619,13 @@ impl OutlinePanel { } } - 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 { + fn open_entry( + &mut self, + entry: &PanelEntry, + change_selection: bool, + cx: &mut ViewContext, + ) { + let Some(active_editor) = self.active_editor() else { return; }; let active_multi_buffer = active_editor.read(cx).buffer().clone(); @@ -498,9 +637,9 @@ impl OutlinePanel { }; self.toggle_expanded(entry, cx); - match entry { - EntryOwned::FoldedDirs(..) | EntryOwned::Entry(FsEntry::Directory(..)) => {} - EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => { + let scroll_target = match entry { + PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None, + PanelEntry::Fs(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 { @@ -511,20 +650,9 @@ impl OutlinePanel { } }, ); - 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, - ); - }) - } + Some(offset_from_top).zip(scroll_target) } - EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => { + PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => { let scroll_target = self .project .update(cx, |project, cx| { @@ -542,118 +670,112 @@ impl OutlinePanel { 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, - ); - }) - } + Some(offset_from_top).zip(scroll_target) } - EntryOwned::Outline(_, excerpt_id, outline) => { + PanelEntry::Outline(OutlineEntry::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, - ); - }) - } + Some(Point::default()).zip(scroll_target) } - EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => { + PanelEntry::Outline(OutlineEntry::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, - ); - }) - } + Some(Point::default()).zip(scroll_target) } + PanelEntry::Search(SearchEntry { match_range, .. }) => { + Some((Point::default(), match_range.start)) + } + }; + + if let Some((offset, anchor)) = scroll_target { + self.select_entry(entry.clone(), true, cx); + if change_selection { + active_editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(Some(anchor..anchor)) + }); + }); + active_editor.focus_handle(cx).focus(cx); + } else { + active_editor.update(cx, |editor, cx| { + editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx); + }); + self.focus_handle.focus(cx); + } + + if let PanelEntry::Search(_) = entry { + if let Some(active_project_search) = + self.active_project_search(Some(&active_editor), cx) + { + self.workspace.update(cx, |workspace, cx| { + workspace.activate_item(&active_project_search, true, change_selection, cx) + }); + } + } else { + self.workspace.update(cx, |workspace, cx| { + workspace.activate_item(&active_editor, true, change_selection, 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 + if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { + self.cached_entries .iter() .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &&selected_entry) + .skip_while(|entry| entry != &selected_entry) .skip(1) .next() .cloned() }) { - self.selected_entry = Some(entry_to_select); - self.autoscroll(cx); - cx.notify(); + self.select_entry(entry_to_select, true, cx); } 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 + if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { + self.cached_entries .iter() .rev() .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &&selected_entry) + .skip_while(|entry| entry != &selected_entry) .skip(1) .next() .cloned() }) { - self.selected_entry = Some(entry_to_select); - self.autoscroll(cx); - cx.notify(); + self.select_entry(entry_to_select, true, cx); } 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| { + if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { let mut previous_entries = self - .cached_entries_with_depth + .cached_entries .iter() .rev() .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &&selected_entry) + .skip_while(|entry| entry != &selected_entry) .skip(1); match &selected_entry { - EntryOwned::Entry(fs_entry) => match fs_entry { + PanelEntry::Fs(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, - )) => { + PanelEntry::Fs(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) => { + PanelEntry::FoldedDirs(dirs_worktree_id, dirs) => { dirs_worktree_id == worktree_id && dirs .first() @@ -664,15 +786,13 @@ impl OutlinePanel { }) } }, - EntryOwned::FoldedDirs(worktree_id, entries) => entries + PanelEntry::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 + if let PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) = + entry { dir_worktree_id == worktree_id && dir_entry.path.as_ref() == parent_path @@ -681,66 +801,70 @@ impl OutlinePanel { } }) }), - EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => { + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt_buffer_id, excerpt_id, _)) => { previous_entries.find(|entry| match entry { - EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => { + PanelEntry::Fs(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)) => { + PanelEntry::Fs(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 - } - }), + PanelEntry::Outline(OutlineEntry::Outline( + outline_buffer_id, + outline_excerpt_id, + _, + )) => previous_entries.find(|entry| { + if let PanelEntry::Outline(OutlineEntry::Excerpt( + excerpt_buffer_id, + excerpt_id, + _, + )) = entry + { + outline_buffer_id == excerpt_buffer_id && outline_excerpt_id == excerpt_id + } else { + false + } + }), + PanelEntry::Search(_) => { + previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_))) + } } }) { - self.selected_entry = Some(entry_to_select.clone()); - self.autoscroll(cx); - cx.notify(); + self.select_entry(entry_to_select.clone(), true, cx); } 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(); + if let Some(first_entry) = self.cached_entries.iter().next() { + self.select_entry(first_entry.entry.clone(), true, cx); } } fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { if let Some(new_selection) = self - .cached_entries_with_depth + .cached_entries .iter() .rev() .map(|cached_entry| &cached_entry.entry) .next() { - self.selected_entry = Some(new_selection.clone()); - self.autoscroll(cx); - cx.notify(); + self.select_entry(new_selection.clone(), true, cx); } } fn autoscroll(&mut self, cx: &mut ViewContext) { - if let Some(selected_entry) = self.selected_entry.clone() { + if let Some(selected_entry) = self.selected_entry() { let index = self - .cached_entries_with_depth + .cached_entries .iter() - .position(|cached_entry| cached_entry.entry == selected_entry); + .position(|cached_entry| &cached_entry.entry == selected_entry); if let Some(index) = index { self.scroll_handle.scroll_to_item(index); cx.notify(); @@ -757,13 +881,13 @@ impl OutlinePanel { fn deploy_context_menu( &mut self, position: Point, - entry: EntryRef<'_>, + entry: PanelEntry, 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 + self.select_entry(entry.clone(), true, cx); + let is_root = match &entry { + PanelEntry::Fs(FsEntry::File(worktree_id, entry, ..)) + | PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self .project .read(cx) .worktree_for_id(*worktree_id, cx) @@ -771,30 +895,30 @@ impl OutlinePanel { worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) }) .unwrap_or(false), - EntryRef::FoldedDirs(worktree_id, entries) => entries + PanelEntry::FoldedDirs(worktree_id, entries) => entries .first() .and_then(|entry| { self.project .read(cx) - .worktree_for_id(worktree_id, 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(..) => { + PanelEntry::Fs(FsEntry::ExternalFile(..)) => false, + PanelEntry::Outline(..) => { cx.notify(); return; } - EntryRef::Outline(..) => { + PanelEntry::Search(_) => { 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 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()) @@ -824,13 +948,13 @@ impl OutlinePanel { cx.notify(); } - fn is_unfoldable(&self, entry: EntryRef) -> bool { - matches!(entry, EntryRef::FoldedDirs(..)) + fn is_unfoldable(&self, entry: &PanelEntry) -> bool { + matches!(entry, PanelEntry::FoldedDirs(..)) } - fn is_foldable(&self, entry: EntryRef) -> bool { + fn is_foldable(&self, entry: &PanelEntry) -> bool { let (directory_worktree, directory_entry) = match entry { - EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => { + PanelEntry::Fs(FsEntry::Directory(directory_worktree, directory_entry)) => { (*directory_worktree, Some(directory_entry)) } _ => return false, @@ -860,23 +984,23 @@ impl OutlinePanel { } 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 + let entry_to_expand = match self.selected_entry() { + Some(PanelEntry::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(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => { Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id)) } - Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => { + Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => { Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } - Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => { + Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => { Some(CollapsedEntry::ExternalFile(*buffer_id)) } - Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => { + Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => { Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) } - None | Some(EntryOwned::Outline(..)) => None, + None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None, }; let Some(collapsed_entry) = entry_to_expand else { return; @@ -895,48 +1019,49 @@ impl OutlinePanel { } 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)), - ) => { + let Some(selected_entry) = self.selected_entry().cloned() else { + return; + }; + match &selected_entry { + PanelEntry::Fs(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.select_entry(selected_entry, true, cx); self.update_cached_entries(None, cx); } - Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => { + PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { self.collapsed_entries .insert(CollapsedEntry::File(*worktree_id, *buffer_id)); - self.selected_entry = Some(file_entry.clone()); + self.select_entry(selected_entry, true, cx); self.update_cached_entries(None, cx); } - Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => { + PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { self.collapsed_entries .insert(CollapsedEntry::ExternalFile(*buffer_id)); - self.selected_entry = Some(file_entry.clone()); + self.select_entry(selected_entry, true, cx); self.update_cached_entries(None, cx); } - Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => { + PanelEntry::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.select_entry(selected_entry, true, cx); self.update_cached_entries(None, cx); } } } - Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => { + PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => { if self .collapsed_entries .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) { - self.selected_entry = Some(excerpt_entry.clone()); + self.select_entry(selected_entry, true, cx); self.update_cached_entries(None, cx); } } - None | Some(EntryOwned::Outline(..)) => {} + PanelEntry::Search(_) | PanelEntry::Outline(..) => {} } } @@ -979,34 +1104,34 @@ impl OutlinePanel { pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { let new_entries = self - .cached_entries_with_depth + .cached_entries .iter() .flat_map(|cached_entry| match &cached_entry.entry { - EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => { + PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => { Some(CollapsedEntry::Dir(*worktree_id, entry.id)) } - EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { Some(CollapsedEntry::File(*worktree_id, *buffer_id)) } - EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => { + PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { Some(CollapsedEntry::ExternalFile(*buffer_id)) } - EntryOwned::FoldedDirs(worktree_id, entries) => { + PanelEntry::FoldedDirs(worktree_id, entries) => { Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)) } - EntryOwned::Excerpt(buffer_id, excerpt_id, _) => { + PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => { Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)) } - EntryOwned::Outline(..) => None, + PanelEntry::Search(_) | PanelEntry::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) { + fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext) { match entry { - EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => { + PanelEntry::Fs(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) { @@ -1020,19 +1145,19 @@ impl OutlinePanel { self.collapsed_entries.insert(collapsed_entry); } } - EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => { + PanelEntry::Fs(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, _)) => { + PanelEntry::Fs(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) => { + PanelEntry::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) { @@ -1047,23 +1172,22 @@ impl OutlinePanel { } } } - EntryOwned::Excerpt(buffer_id, excerpt_id, _) => { + PanelEntry::Outline(OutlineEntry::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, + PanelEntry::Search(_) | PanelEntry::Outline(..) => return, } - self.selected_entry = Some(entry.clone()); + self.select_entry(entry.clone(), true, cx); 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() + .selected_entry() .and_then(|entry| self.abs_path(&entry, cx)) .map(|p| p.to_string_lossy().to_string()) { @@ -1073,12 +1197,11 @@ impl OutlinePanel { fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { if let Some(clipboard_text) = self - .selected_entry - .as_ref() + .selected_entry() .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, + PanelEntry::Fs(entry) => self.relative_path(&entry, cx), + PanelEntry::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()), + PanelEntry::Search(_) | PanelEntry::Outline(..) => None, }) .map(|p| p.to_string_lossy().to_string()) { @@ -1088,8 +1211,7 @@ impl OutlinePanel { fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext) { if let Some(abs_path) = self - .selected_entry - .as_ref() + .selected_entry() .and_then(|entry| self.abs_path(&entry, cx)) { cx.reveal_path(&abs_path); @@ -1097,11 +1219,11 @@ impl OutlinePanel { } fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { - let selected_entry = self.selected_entry.as_ref(); + let selected_entry = self.selected_entry(); 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(..))), + Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))), ) = (&abs_path, selected_entry) { abs_path.parent().map(|p| p.to_owned()) @@ -1119,97 +1241,148 @@ impl OutlinePanel { editor: &View, cx: &mut ViewContext<'_, Self>, ) { + if !self.active { + return; + } 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; + self.selected_entry = SelectedEntry::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; + let project = self.project.clone(); + self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move { + cx.background_executor().timer(UPDATE_DEBOUNCE).await; + let related_buffer_entry = match &entry_with_selection { + PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { + project.update(&mut cx, |project, 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)) + }) + })? } - } - 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) - } - } + PanelEntry::Outline(outline_entry) => { + let &(OutlineEntry::Outline(buffer_id, excerpt_id, _) + | OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) = outline_entry; + outline_panel.update(&mut cx, |outline_panel, cx| { + outline_panel + .collapsed_entries + .remove(&CollapsedEntry::ExternalFile(buffer_id)); + outline_panel + .collapsed_entries + .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); + let project = outline_panel.project.read(cx); + let entry_id = project + .buffer_for_id(buffer_id, cx) + .and_then(|buffer| buffer.read(cx).entry_id(cx)); - self.selected_entry = Some(entry_with_selection); - self.update_cached_entries(None, cx); - self.autoscroll(cx); + entry_id.and_then(|entry_id| { + project + .worktree_for_entry(entry_id, cx) + .and_then(|worktree| { + let worktree_id = worktree.read(cx).id(); + outline_panel + .collapsed_entries + .remove(&CollapsedEntry::File(worktree_id, buffer_id)); + let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); + Some((worktree, entry)) + }) + }) + })? + } + PanelEntry::Fs(FsEntry::ExternalFile(..)) => None, + PanelEntry::Search(SearchEntry { match_range, .. }) => match_range + .start + .buffer_id + .or(match_range.end.buffer_id) + .map(|buffer_id| { + outline_panel.update(&mut cx, |outline_panel, cx| { + outline_panel + .collapsed_entries + .remove(&CollapsedEntry::ExternalFile(buffer_id)); + let project = 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(); + outline_panel + .collapsed_entries + .remove(&CollapsedEntry::File(worktree_id, buffer_id)); + let entry = + worktree.read(cx).entry_for_id(entry_id)?.clone(); + Some((worktree, entry)) + }) + }) + }) + }) + .transpose()? + .flatten(), + _ => return anyhow::Ok(()), + }; + if let Some((worktree, buffer_entry)) = related_buffer_entry { + outline_panel.update(&mut cx, |outline_panel, cx| { + 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 outline_panel + .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 { + 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) + } + })? + } + + outline_panel.update(&mut cx, |outline_panel, cx| { + outline_panel.select_entry(entry_with_selection, false, cx); + outline_panel.update_cached_entries(None, cx); + })?; + + anyhow::Ok(()) + }); } fn render_excerpt( @@ -1221,10 +1394,12 @@ impl OutlinePanel { 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 - } + let is_active = match self.selected_entry() { + Some(PanelEntry::Outline(OutlineEntry::Excerpt( + selected_buffer_id, + selected_excerpt_id, + _, + ))) => selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id, _ => false, }; let has_outlines = self @@ -1251,7 +1426,7 @@ impl OutlinePanel { 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 {}-{}", + "Lines {}- {}", excerpt_range.start.row + 1, excerpt_range.end.row + 1, )) @@ -1260,7 +1435,7 @@ impl OutlinePanel { .into_any_element(); Some(self.entry_element( - EntryRef::Excerpt(buffer_id, excerpt_id, range), + PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())), item_id, depth, Some(icon), @@ -1293,8 +1468,12 @@ impl OutlinePanel { ) .into_any_element(), ); - let is_active = match &self.selected_entry { - Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => { + let is_active = match self.selected_entry() { + Some(PanelEntry::Outline(OutlineEntry::Outline( + selected_buffer_id, + selected_excerpt_id, + selected_entry, + ))) => { selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id && selected_entry == rendered_outline @@ -1307,7 +1486,11 @@ impl OutlinePanel { Some(empty_icon()) }; self.entry_element( - EntryRef::Outline(buffer_id, excerpt_id, rendered_outline), + PanelEntry::Outline(OutlineEntry::Outline( + buffer_id, + excerpt_id, + rendered_outline.clone(), + )), item_id, depth, icon, @@ -1325,8 +1508,8 @@ impl OutlinePanel { 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, + let is_active = match self.selected_entry() { + Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry, _ => false, }; let (item_id, label_element, icon) = match rendered_entry { @@ -1416,7 +1599,7 @@ impl OutlinePanel { }; self.entry_element( - EntryRef::Entry(rendered_entry), + PanelEntry::Fs(rendered_entry.clone()), item_id, depth, Some(icon), @@ -1435,8 +1618,8 @@ impl OutlinePanel { 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)) => { + let is_active = match self.selected_entry() { + Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => { selected_worktree_id == &worktree_id && selected_entries == dir_entries } _ => false, @@ -1479,7 +1662,7 @@ impl OutlinePanel { }; self.entry_element( - EntryRef::FoldedDirs(worktree_id, dir_entries), + PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()), item_id, depth, Some(icon), @@ -1489,10 +1672,66 @@ impl OutlinePanel { ) } + fn render_search_match( + &self, + match_range: &Range, + search_data: &SearchData, + kind: SearchKind, + depth: usize, + string_match: Option<&StringMatch>, + cx: &mut ViewContext, + ) -> Stateful
{ + let search_matches = string_match + .iter() + .flat_map(|string_match| string_match.ranges()) + .collect::>(); + let match_ranges = if search_matches.is_empty() { + &search_data.search_match_indices + } else { + &search_matches + }; + let label_element = language::render_item( + &OutlineItem { + depth, + annotation_range: None, + range: search_data.context_range.clone(), + text: search_data.context_text.clone(), + highlight_ranges: search_data.highlight_ranges.clone(), + name_ranges: search_data.search_match_indices.clone(), + body_range: Some(search_data.context_range.clone()), + }, + match_ranges.into_iter().cloned(), + cx, + ) + .into_any_element(); + + let is_active = match self.selected_entry() { + Some(PanelEntry::Search(SearchEntry { + match_range: selected_match_range, + .. + })) => match_range == selected_match_range, + _ => false, + }; + self.entry_element( + PanelEntry::Search(SearchEntry { + kind, + match_range: match_range.clone(), + same_line_matches: Vec::new(), + render_data: Some(OnceCell::new()), + }), + ElementId::from(SharedString::from(format!("search-{match_range:?}"))), + depth, + None, + is_active, + label_element, + cx, + ) + } + #[allow(clippy::too_many_arguments)] fn entry_element( &self, - rendered_entry: EntryRef<'_>, + rendered_entry: PanelEntry, item_id: ElementId, depth: usize, icon_element: Option, @@ -1501,7 +1740,6 @@ impl OutlinePanel { 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()) @@ -1520,7 +1758,8 @@ impl OutlinePanel { if event.down.button == MouseButton::Right || event.down.first_mouse { return; } - outline_panel.open_entry(&clicked_entry, cx); + let change_selection = event.down.click_count > 1; + outline_panel.open_entry(&clicked_entry, change_selection, cx); }) }) .on_secondary_mouse_down(cx.listener( @@ -1530,7 +1769,7 @@ impl OutlinePanel { cx.stop_propagation(); outline_panel.deploy_context_menu( event.position, - rendered_entry.to_ref_entry(), + rendered_entry.clone(), cx, ) }, @@ -1582,7 +1821,6 @@ impl OutlinePanel { &mut self, active_editor: &View, new_entries: HashSet, - new_selected_entry: Option, debounce: Option, cx: &mut ViewContext, ) { @@ -1665,11 +1903,11 @@ impl OutlinePanel { match &worktree { Some(worktree) => { new_collapsed_entries - .insert(CollapsedEntry::File(worktree.id(), buffer_id)); + .remove(&CollapsedEntry::File(worktree.id(), buffer_id)); } None => { new_collapsed_entries - .insert(CollapsedEntry::ExternalFile(buffer_id)); + .remove(&CollapsedEntry::ExternalFile(buffer_id)); } } } @@ -1908,45 +2146,43 @@ impl OutlinePanel { 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); + outline_panel.update_non_fs_items(cx); + cx.notify(); }) .ok(); }); } - fn replace_visible_entries( + fn replace_active_editor( &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); + let buffer_search_subscription = cx.subscribe( + &new_active_editor, + |outline_panel: &mut Self, _, _: &SearchEvent, cx: &mut ViewContext<'_, Self>| { + outline_panel.update_search_matches(cx); + outline_panel.autoscroll(cx); + }, + ); self.active_item = Some(ActiveItem { - item_id: new_active_editor.item_id(), + _buffer_search_subscription: buffer_search_subscription, _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, - ); + self.selected_entry.invalidate(); + self.update_fs_entries(&new_active_editor, new_entries, 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.selected_entry = SelectedEntry::None; self.fs_entries_update_task = Task::ready(()); self.cached_entries_update_task = Task::ready(()); self.active_item = None; @@ -1955,14 +2191,17 @@ impl OutlinePanel { self.fs_children_count.clear(); self.outline_fetch_tasks.clear(); self.excerpts.clear(); - self.cached_entries_with_depth = Vec::new(); + self.cached_entries = Vec::new(); + self.search_matches.clear(); + self.search = None; + self.pinned = false; } fn location_for_editor_selection( &mut self, editor: &View, cx: &mut ViewContext, - ) -> Option { + ) -> Option { let selection = editor .read(cx) .selections @@ -1979,6 +2218,64 @@ impl OutlinePanel { let buffer_id = buffer.read(cx).remote_id(); let selection_display_point = selection.to_display_point(&editor_snapshot); + match self.mode { + ItemsDisplayMode::Search => self + .search_matches + .iter() + .rev() + .min_by_key(|&match_range| { + let match_display_range = + match_range.clone().to_display_points(&editor_snapshot); + let start_distance = if selection_display_point < match_display_range.start { + match_display_range.start - selection_display_point + } else { + selection_display_point - match_display_range.start + }; + let end_distance = if selection_display_point < match_display_range.end { + match_display_range.end - selection_display_point + } else { + selection_display_point - match_display_range.end + }; + start_distance + end_distance + }) + .and_then(|closest_range| { + self.cached_entries.iter().find_map(|cached_entry| { + if let PanelEntry::Search(SearchEntry { + match_range, + same_line_matches, + .. + }) = &cached_entry.entry + { + if match_range == closest_range + || same_line_matches.contains(&closest_range) + { + Some(cached_entry.entry.clone()) + } else { + None + } + } else { + None + } + }) + }), + ItemsDisplayMode::Outline => self.outline_location( + buffer_id, + excerpt_id, + multi_buffer_snapshot, + editor_snapshot, + selection_display_point, + ), + } + } + + fn outline_location( + &mut self, + buffer_id: BufferId, + excerpt_id: ExcerptId, + multi_buffer_snapshot: editor::MultiBufferSnapshot, + editor_snapshot: editor::EditorSnapshot, + selection_display_point: DisplayPoint, + ) -> Option { let excerpt_outlines = self .excerpts .get(&buffer_id) @@ -2068,31 +2365,37 @@ impl OutlinePanel { .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 + Some(outline) => { + PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline)) + } + None => { + self.cached_entries.iter().rev().find_map(|cached_entry| { + match &cached_entry.entry { + PanelEntry::Outline(OutlineEntry::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 + PanelEntry::Fs( + 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, } - _ => None, - })?, + })? + } }; Some(closest_container) } @@ -2143,20 +2446,9 @@ impl OutlinePanel { } 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) + self.active_editor().map_or(false, |active_editor| { + active_editor.read(cx).buffer().read(cx).is_singleton() + }) } fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { @@ -2227,7 +2519,7 @@ impl OutlinePanel { buffer_id: BufferId, cx: &AppContext, ) -> Option { - let editor = self.active_item.as_ref()?.active_editor.upgrade()?; + let editor = self.active_editor()?; Some( editor .read(cx) @@ -2239,9 +2531,9 @@ impl OutlinePanel { ) } - fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option { + fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option { match entry { - EntryOwned::Entry( + PanelEntry::Fs( FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _), ) => self .buffer_snapshot_for_id(*buffer_id, cx) @@ -2249,20 +2541,20 @@ impl OutlinePanel { let file = File::from_dyn(buffer_snapshot.file())?; file.worktree.read(cx).absolutize(&file.path).ok() }), - EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self + PanelEntry::Fs(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| { + PanelEntry::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, + PanelEntry::Search(_) | PanelEntry::Outline(..) => None, } } @@ -2282,6 +2574,10 @@ impl OutlinePanel { debounce: Option, cx: &mut ViewContext, ) { + if !self.active { + return; + } + 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 { @@ -2299,7 +2595,18 @@ impl OutlinePanel { 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; + outline_panel.cached_entries = new_cached_entries; + if outline_panel.selected_entry.is_invalidated() { + if let Some(new_selected_entry) = + outline_panel.active_editor().and_then(|active_editor| { + outline_panel.location_for_editor_selection(&active_editor, cx) + }) + { + outline_panel.select_entry(new_selected_entry, false, cx); + } + } + + outline_panel.autoscroll(cx); cx.notify(); }) .ok(); @@ -2405,9 +2712,9 @@ impl OutlinePanel { folded_dirs_entry = Some((folded_depth, folded_worktree_id, folded_dirs)) } else { - if parent_expanded || query.is_some() { + if !is_singleton && (parent_expanded || query.is_some()) { let new_folded_dirs = - EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs); + PanelEntry::FoldedDirs(folded_worktree_id, folded_dirs); outline_panel.push_entry( &mut entries, &mut match_candidates, @@ -2441,12 +2748,12 @@ impl OutlinePanel { .all(|entry| entry.path.as_ref() != *parent_path) }) .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded); - if parent_expanded || query.is_some() { + if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, - EntryOwned::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); @@ -2468,12 +2775,12 @@ impl OutlinePanel { .all(|entry| entry.path.as_ref() != *parent_path) }) .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded); - if parent_expanded || query.is_some() { + if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut entries, &mut match_candidates, track_matches, - EntryOwned::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); @@ -2509,85 +2816,71 @@ impl OutlinePanel { &mut entries, &mut match_candidates, track_matches, - EntryOwned::Entry(entry.clone()), + PanelEntry::Fs(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( + match outline_panel.mode { + ItemsDisplayMode::Search => { + if is_singleton || query.is_some() || (should_add && is_expanded) { + outline_panel.add_search_entries( + entry, + depth, + track_matches, + is_singleton, &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, - ); - } } } + ItemsDisplayMode::Outline => { + 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 { + outline_panel.add_excerpt_entries( + buffer_id, + entry_excerpts, + depth, + track_matches, + is_singleton, + query.as_deref(), + &mut entries, + &mut match_candidates, + cx, + ); + } + } + } + + if is_singleton + && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..)) + && !entries.iter().any(|item| { + matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_)) + }) + { + outline_panel.push_entry( + &mut entries, + &mut match_candidates, + track_matches, + PanelEntry::Fs(entry.clone()), + 0, + cx, + ); } } @@ -2606,7 +2899,7 @@ impl OutlinePanel { &mut entries, &mut match_candidates, track_matches, - EntryOwned::FoldedDirs(worktree_id, folded_dirs), + PanelEntry::FoldedDirs(worktree_id, folded_dirs), folded_depth, cx, ); @@ -2654,14 +2947,14 @@ impl OutlinePanel { entries: &mut Vec, match_candidates: &mut Vec, track_matches: bool, - entry: EntryOwned, + entry: PanelEntry, depth: usize, - cx: &AppContext, + cx: &mut WindowContext, ) { if track_matches { let id = entries.len(); match &entry { - EntryOwned::Entry(fs_entry) => { + PanelEntry::Fs(fs_entry) => { if let Some(file_name) = self.relative_path(fs_entry, cx).as_deref().map(file_name) { @@ -2672,7 +2965,7 @@ impl OutlinePanel { }); } } - EntryOwned::FoldedDirs(worktree_id, entries) => { + PanelEntry::FoldedDirs(worktree_id, entries) => { let dir_names = self.dir_names_string(entries, *worktree_id, cx); { match_candidates.push(StringMatchCandidate { @@ -2682,12 +2975,29 @@ impl OutlinePanel { }); } } - EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate { - id, - string: outline.text.clone(), - char_bag: outline.text.chars().collect(), - }), - EntryOwned::Excerpt(..) => {} + PanelEntry::Outline(outline_entry) => match outline_entry { + OutlineEntry::Outline(_, _, outline) => { + match_candidates.push(StringMatchCandidate { + id, + string: outline.text.clone(), + char_bag: outline.text.chars().collect(), + }); + } + OutlineEntry::Excerpt(..) => {} + }, + PanelEntry::Search(new_search_entry) => { + if let Some(search_data) = new_search_entry + .render_data + .as_ref() + .and_then(|data| data.get()) + { + match_candidates.push(StringMatchCandidate { + id, + char_bag: search_data.context_text.chars().collect(), + string: search_data.context_text.clone(), + }); + } + } } } entries.push(CachedEntry { @@ -2729,6 +3039,318 @@ impl OutlinePanel { }; !self.collapsed_entries.contains(&entry_to_check) } + + fn update_non_fs_items(&mut self, cx: &mut ViewContext) { + if !self.active { + return; + } + + self.update_search_matches(cx); + self.fetch_outdated_outlines(cx); + self.autoscroll(cx); + } + + fn update_search_matches(&mut self, cx: &mut ViewContext) { + if !self.active { + return; + } + + let active_editor = self.active_editor(); + let project_search = self.active_project_search(active_editor.as_ref(), cx); + let project_search_matches = project_search + .as_ref() + .map(|project_search| project_search.read(cx).get_matches(cx)) + .unwrap_or_default(); + + let buffer_search = active_editor + .as_ref() + .and_then(|active_editor| self.workspace.read(cx).pane_for(active_editor)) + .and_then(|pane| { + pane.read(cx) + .toolbar() + .read(cx) + .item_of_type::() + }); + let buffer_search_matches = active_editor + .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx))) + .unwrap_or_default(); + + let mut update_cached_entries = false; + if buffer_search_matches.is_empty() && project_search_matches.is_empty() { + self.search_matches.clear(); + self.search = None; + if self.mode == ItemsDisplayMode::Search { + self.mode = ItemsDisplayMode::Outline; + update_cached_entries = true; + } + } else { + let new_search_matches = if buffer_search_matches.is_empty() { + self.search = project_search.map(|project_search| { + ( + SearchKind::Project, + project_search.read(cx).search_query_text(cx), + ) + }); + project_search_matches + } else { + self.search = buffer_search + .map(|buffer_search| (SearchKind::Buffer, buffer_search.read(cx).query(cx))); + buffer_search_matches + }; + update_cached_entries = self.mode != ItemsDisplayMode::Search + || self.search_matches.is_empty() + || self.search_matches != new_search_matches; + self.search_matches = new_search_matches; + self.mode = ItemsDisplayMode::Search; + } + if update_cached_entries { + self.selected_entry.invalidate(); + self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx); + } + } + + fn active_project_search( + &mut self, + for_editor: Option<&View>, + cx: &mut ViewContext, + ) -> Option> { + let for_editor = for_editor?; + self.workspace + .read(cx) + .active_pane() + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .find(|project_search| { + let project_search_editor = project_search.boxed_clone().act_as::(cx); + Some(for_editor) == project_search_editor.as_ref() + }) + } + + #[allow(clippy::too_many_arguments)] + fn add_excerpt_entries( + &self, + buffer_id: BufferId, + entries_to_add: &[ExcerptId], + parent_depth: usize, + track_matches: bool, + is_singleton: bool, + query: Option<&str>, + entries: &mut Vec, + match_candidates: &mut Vec, + cx: &mut ViewContext, + ) { + if let Some(excerpts) = self.excerpts.get(&buffer_id) { + for &excerpt_id in entries_to_add { + let Some(excerpt) = excerpts.get(&excerpt_id) else { + continue; + }; + let excerpt_depth = parent_depth + 1; + self.push_entry( + entries, + match_candidates, + track_matches, + PanelEntry::Outline(OutlineEntry::Excerpt( + buffer_id, + excerpt_id, + 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() + && self + .collapsed_entries + .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)) + { + continue; + } + + for outline in excerpt.iter_outlines() { + self.push_entry( + entries, + match_candidates, + track_matches, + PanelEntry::Outline(OutlineEntry::Outline( + buffer_id, + excerpt_id, + outline.clone(), + )), + outline_base_depth + outline.depth, + cx, + ); + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn add_search_entries( + &self, + entry: &FsEntry, + parent_depth: usize, + track_matches: bool, + is_singleton: bool, + entries: &mut Vec, + match_candidates: &mut Vec, + cx: &mut ViewContext, + ) { + let related_excerpts = match entry { + FsEntry::Directory(_, _) => return, + FsEntry::ExternalFile(_, excerpts) => excerpts, + FsEntry::File(_, _, _, excerpts) => excerpts, + } + .iter() + .copied() + .collect::>(); + if related_excerpts.is_empty() || self.search_matches.is_empty() { + return; + } + let Some(kind) = self.search.as_ref().map(|&(kind, _)| kind) else { + return; + }; + + for match_range in &self.search_matches { + if related_excerpts.contains(&match_range.start.excerpt_id) + || related_excerpts.contains(&match_range.end.excerpt_id) + { + let depth = if is_singleton { 0 } else { parent_depth + 1 }; + let previous_search_entry = entries.last_mut().and_then(|entry| { + if let PanelEntry::Search(previous_search_entry) = &mut entry.entry { + Some(previous_search_entry) + } else { + None + } + }); + let mut new_search_entry = SearchEntry { + kind, + match_range: match_range.clone(), + same_line_matches: Vec::new(), + render_data: Some(OnceCell::new()), + }; + if self.init_search_data(previous_search_entry, &mut new_search_entry, cx) { + self.push_entry( + entries, + match_candidates, + track_matches, + PanelEntry::Search(new_search_entry), + depth, + cx, + ); + } + } + } + } + + fn active_editor(&self) -> Option> { + self.active_item.as_ref()?.active_editor.upgrade() + } + + fn should_replace_active_editor(&self, new_active_editor: &View) -> bool { + self.active_editor().map_or(true, |active_editor| { + !self.pinned && active_editor.item_id() != new_active_editor.item_id() + }) + } + + pub fn toggle_active_editor_pin( + &mut self, + _: &ToggleActiveEditorPin, + cx: &mut ViewContext, + ) { + self.pinned = !self.pinned; + if !self.pinned { + if let Some(active_editor) = workspace_active_editor(self.workspace.read(cx), cx) { + if self.should_replace_active_editor(&active_editor) { + self.replace_active_editor(active_editor, cx); + } + } + } + + cx.notify(); + } + + fn selected_entry(&self) -> Option<&PanelEntry> { + match &self.selected_entry { + SelectedEntry::Invalidated(entry) => entry.as_ref(), + SelectedEntry::Valid(entry) => Some(entry), + SelectedEntry::None => None, + } + } + + fn init_search_data( + &self, + previous_search_entry: Option<&mut SearchEntry>, + new_search_entry: &mut SearchEntry, + cx: &WindowContext, + ) -> bool { + let Some(active_editor) = self.active_editor() else { + return false; + }; + let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx); + let theme = cx.theme().syntax().clone(); + let previous_search_data = previous_search_entry.and_then(|previous_search_entry| { + let previous_search_data = previous_search_entry.render_data.as_mut()?; + previous_search_data.get_or_init(|| { + SearchData::new( + new_search_entry.kind, + &previous_search_entry.match_range, + &multi_buffer_snapshot, + &theme, + ) + }); + previous_search_data.get_mut() + }); + let new_search_data = new_search_entry.render_data.as_mut().and_then(|data| { + data.get_or_init(|| { + SearchData::new( + new_search_entry.kind, + &new_search_entry.match_range, + &multi_buffer_snapshot, + &theme, + ) + }); + data.get_mut() + }); + match (previous_search_data, new_search_data) { + (_, None) => false, + (None, Some(_)) => true, + (Some(previous_search_data), Some(new_search_data)) => { + if previous_search_data.context_range == new_search_data.context_range { + previous_search_data + .highlight_ranges + .append(&mut new_search_data.highlight_ranges); + previous_search_data + .search_match_indices + .append(&mut new_search_data.search_match_indices); + false + } else { + true + } + } + } + } + + fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext) { + if focus { + self.focus_handle.focus(cx); + } + self.selected_entry = SelectedEntry::Valid(entry); + self.autoscroll(cx); + cx.notify(); + } +} + +fn workspace_active_editor(workspace: &Workspace, cx: &AppContext) -> Option> { + workspace + .active_item(cx)? + .act_as::(cx) + .filter(|editor| editor.read(cx).mode() == EditorMode::Full) } fn back_to_common_visited_parent( @@ -2825,31 +3447,34 @@ impl Panel for OutlinePanel { } 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); + cx.spawn(|outline_panel, mut cx| async move { + outline_panel + .update(&mut cx, |outline_panel, cx| { + let old_active = outline_panel.active; + outline_panel.active = active; + if active && old_active != active { + if let Some(active_editor) = + workspace_active_editor(outline_panel.workspace.read(cx), cx) + { + if outline_panel.should_replace_active_editor(&active_editor) { + outline_panel.replace_active_editor(active_editor, cx); + } else { + outline_panel.update_fs_entries( + &active_editor, + HashSet::default(), + None, + cx, + ) + } + } else if !outline_panel.pinned { + outline_panel.clear_previous(cx); + } + } + outline_panel.serialize(cx); + }) + .ok(); + }) + .detach() } } @@ -2867,6 +3492,8 @@ 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 pinned = self.pinned; + let outline_panel = v_flex() .id("outline-panel") .size_full() @@ -2885,6 +3512,7 @@ impl Render for OutlinePanel { .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::toggle_active_editor_pin)) .on_action(cx.listener(Self::unfold_directory)) .on_action(cx.listener(Self::fold_directory)) .when(project.is_local_or_ssh(), |el| { @@ -2894,20 +3522,16 @@ impl Render for OutlinePanel { .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) + if let Some(entry) = outline_panel.selected_entry().cloned() { + outline_panel.deploy_context_menu(event.position, entry, cx) } else if let Some(entry) = outline_panel.fs_entries.first().cloned() { - outline_panel.deploy_context_menu( - event.position, - EntryRef::Entry(&entry), - cx, - ) + outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx) } }), ) .track_focus(&self.focus_handle); - if self.cached_entries_with_depth.is_empty() { + if self.cached_entries.is_empty() { let header = if self.updating_fs_entries { "Loading outlines" } else if query.is_some() { @@ -2945,57 +3569,89 @@ impl Render for OutlinePanel { ), ) } 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, + outline_panel + .when_some(self.search.as_ref(), |outline_panel, (_, search_query)| { + outline_panel.child( + div() + .mx_2() + .child( + Label::new(format!("Searching: '{search_query}'")) + .color(Color::Muted), + ) + .child(horizontal_separator(cx)), + ) + }) + .child({ + let items_len = self.cached_entries.len(); + uniform_list(cx.view().clone(), "entries", items_len, { + move |outline_panel, range, cx| { + let entries = outline_panel.cached_entries.get(range); + entries + .map(|entries| entries.to_vec()) + .unwrap_or_default() + .into_iter() + .filter_map(|cached_entry| match cached_entry.entry { + PanelEntry::Fs(entry) => Some(outline_panel.render_entry( + &entry, cached_entry.depth, cached_entry.string_match.as_ref(), cx, - )) - } - EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => { - outline_panel.render_excerpt( + )), + PanelEntry::FoldedDirs(worktree_id, entries) => { + Some(outline_panel.render_folded_dirs( + worktree_id, + &entries, + cached_entry.depth, + cached_entry.string_match.as_ref(), + cx, + )) + } + PanelEntry::Outline(OutlineEntry::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( + ), + PanelEntry::Outline(OutlineEntry::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() - } + )), + PanelEntry::Search(SearchEntry { + match_range, + render_data, + kind, + same_line_matches: _, + }) => render_data.as_ref().and_then(|search_data| { + let search_data = search_data.get()?; + Some(outline_panel.render_search_match( + &match_range, + search_data, + kind, + cached_entry.depth, + cached_entry.string_match.as_ref(), + cx, + )) + }), + }) + .collect() + } + }) + .size_full() + .track_scroll(self.scroll_handle.clone()) }) - .size_full() - .track_scroll(self.scroll_handle.clone()) - }) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -3007,9 +3663,27 @@ impl Render for OutlinePanel { .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())), + v_flex().child(horizontal_separator(cx)).child( + h_flex().p_2().child(self.filter_editor.clone()).child( + div().border_1().child( + IconButton::new( + "outline-panel-menu", + if pinned { + IconName::Unpin + } else { + IconName::Pin + }, + ) + .tooltip(move |cx| { + Tooltip::text(if pinned { "Unpin" } else { "Pin active editor" }, cx) + }) + .shape(IconButtonShape::Square) + .on_click(cx.listener(|outline_panel, _, cx| { + outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx); + })), + ), + ), + ), ) } } @@ -3030,7 +3704,6 @@ fn subscribe_for_editor_events( outline_panel.update_fs_entries( &editor, excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(), - None, debounce, cx, ); @@ -3043,15 +3716,15 @@ fn subscribe_for_editor_events( break; } } - outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx); + outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx); } EditorEvent::ExcerptsExpanded { ids } => { outline_panel.invalidate_outlines(ids); - outline_panel.fetch_outdated_outlines(cx) + outline_panel.update_non_fs_items(cx); } EditorEvent::ExcerptsEdited { ids } => { outline_panel.invalidate_outlines(ids); - outline_panel.fetch_outdated_outlines(cx); + outline_panel.update_non_fs_items(cx); } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { @@ -3059,7 +3732,7 @@ fn subscribe_for_editor_events( excerpt.invalidate_outlines(); } } - outline_panel.fetch_outdated_outlines(cx); + outline_panel.update_non_fs_items(cx); } _ => {} }, @@ -3073,3 +3746,7 @@ fn empty_icon() -> AnyElement { .flex_none() .into_any_element() } + +fn horizontal_separator(cx: &mut WindowContext) -> Div { + div().mx_2().border_primary(cx).border_t_1() +} diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index cfd4ee1d9c..82e6254981 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -988,7 +988,11 @@ impl BufferSearchBar { .searchable_items_with_matches .get(&active_searchable_item.downgrade()) .unwrap(); - active_searchable_item.update_matches(matches, cx); + if matches.is_empty() { + active_searchable_item.clear_matches(cx); + } else { + active_searchable_item.update_matches(matches, cx); + } let _ = done_tx.send(()); } cx.notify(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d0c24a6f8a..0dcc87a5e1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -503,6 +503,10 @@ impl Item for ProjectSearchView { } impl ProjectSearchView { + pub fn get_matches(&self, cx: &AppContext) -> Vec> { + self.model.read(cx).match_ranges.clone() + } + fn toggle_filters(&mut self, cx: &mut ViewContext) { self.filters_enabled = !self.filters_enabled; ActiveSettings::update_global(cx, |settings, cx| { @@ -836,6 +840,10 @@ impl ProjectSearchView { } } + pub fn search_query_text(&self, cx: &WindowContext) -> String { + self.query_editor.read(cx).text(cx) + } + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. let text = self.query_editor.read(cx).text(cx); diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 74c565988d..0d01a36e4e 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -212,6 +212,7 @@ pub enum IconName { PageUp, Pencil, Person, + Pin, Play, Plus, Public, @@ -263,6 +264,7 @@ pub enum IconName { Trash, TriangleRight, Undo, + Unpin, Update, WholeWord, XCircle, @@ -381,6 +383,7 @@ impl IconName { IconName::PageUp => "icons/page_up.svg", IconName::Pencil => "icons/pencil.svg", IconName::Person => "icons/person.svg", + IconName::Pin => "icons/pin.svg", IconName::Play => "icons/play.svg", IconName::Plus => "icons/plus.svg", IconName::Public => "icons/public.svg", @@ -431,6 +434,7 @@ impl IconName { IconName::TextSelect => "icons/text_select.svg", IconName::Trash => "icons/trash.svg", IconName::TriangleRight => "icons/triangle_right.svg", + IconName::Unpin => "icons/unpin.svg", IconName::Update => "icons/update.svg", IconName::Undo => "icons/undo.svg", IconName::WholeWord => "icons/word_search.svg", diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index b30b986c50..d8a690bee4 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -65,6 +65,9 @@ pub trait SearchableItem: Item + EventEmitter { fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext) {} + fn get_matches(&self, _: &mut WindowContext) -> Vec { + Vec::new() + } fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String;