From 28271a9a36e4fc86a26c7ea38d1643da39bd5294 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 25 Aug 2024 21:40:02 +0300 Subject: [PATCH] Display buffer/project search entries in the outline panel (#16589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prototypes a way to display new entities in the outline panel, making it less outline. The design is not final and might be adjusted, but the workflow seems to be solid enough to keep and iron it out. * Now, when any project search buffer is activated (multi buffer mode), or buffer search is open (singleton buffer mode, but is available for search usages multi buffer too — in that case buffer search overrides multi buffer's contents display), outline panel displays all search matches instead of the outline items. Outline items are not displayed at all during those cases, unless the buffer search is closed, or a new buffer gets opened, of an active buffer search matches zero items. https://github.com/user-attachments/assets/4a3e4faa-7f75-4522-96bb-3761872c753a * For the multi buffer mode, search matches are grouped under directories and files, same as outline items ![Screenshot 2024-08-21 at 14 55 01](https://github.com/user-attachments/assets/6dac75e4-be4e-4338-917b-37a32c285b71) * For buffer search , search matches are displayed one under another ![image](https://github.com/user-attachments/assets/9efcff85-d4c7-4462-9ef5-f76b08e59f20) For both cases, the entire match line is taken and rendered, with the hover tooltip showing the line number. So far it does not look very bad, but I am certain there are bad cases with long lines and bad indents where it looks not optimal — this part most probably will be redesigned after some trial. Or, maybe, it's ok to leave the current state if the horizontal scrollbar is added? Clicking the item navigates to the item's position in the editor. Search item lines are also possible to filter with the outline panel's filter input. * Inline panel is now possible to "pin" to track a currently active editor, to display outlines/search results for that editor even if another item is activated afterwards: ![image](https://github.com/user-attachments/assets/75fb78c3-0e5f-47b4-ba3a-485c71d7e342) This is useful in combination with project search results display: now it's possible to leave the search results pinned in the outline panel and jump to every search result and back. If the item the panel was pinned to gets closed, the panel gets back to its regular state, showing outlines/search results for a currently active editor. Release Notes: - Added a way to display buffer/project search entries in the outline panel --- Cargo.lock | 2 + assets/icons/pin.svg | 1 + assets/icons/unpin.svg | 1 + assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- crates/editor/src/items.rs | 23 +- crates/outline_panel/Cargo.toml | 2 + crates/outline_panel/src/outline_panel.rs | 1879 ++++++++++++++------- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 8 + crates/ui/src/components/icon.rs | 4 + crates/workspace/src/searchable.rs | 3 + 12 files changed, 1328 insertions(+), 605 deletions(-) create mode 100644 assets/icons/pin.svg create mode 100644 assets/icons/unpin.svg 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;