
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  * For buffer search , search matches are displayed one under another  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:  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
335 lines
10 KiB
Rust
335 lines
10 KiB
Rust
use std::{any::Any, sync::Arc};
|
|
|
|
use any_vec::AnyVec;
|
|
use gpui::{
|
|
AnyView, AnyWeakView, AppContext, EventEmitter, Subscription, Task, View, ViewContext,
|
|
WeakView, WindowContext,
|
|
};
|
|
use project::search::SearchQuery;
|
|
|
|
use crate::{
|
|
item::{Item, WeakItemHandle},
|
|
ItemHandle,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum SearchEvent {
|
|
MatchesInvalidated,
|
|
ActiveMatchChanged,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
|
pub enum Direction {
|
|
Prev,
|
|
#[default]
|
|
Next,
|
|
}
|
|
|
|
impl Direction {
|
|
pub fn opposite(&self) -> Self {
|
|
match self {
|
|
Direction::Prev => Direction::Next,
|
|
Direction::Next => Direction::Prev,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct SearchOptions {
|
|
pub case: bool,
|
|
pub word: bool,
|
|
pub regex: bool,
|
|
/// Specifies whether the supports search & replace.
|
|
pub replacement: bool,
|
|
pub selection: bool,
|
|
}
|
|
|
|
pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
|
type Match: Any + Sync + Send + Clone;
|
|
|
|
fn supported_options() -> SearchOptions {
|
|
SearchOptions {
|
|
case: true,
|
|
word: true,
|
|
regex: true,
|
|
replacement: true,
|
|
selection: true,
|
|
}
|
|
}
|
|
|
|
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
|
|
|
|
fn has_filtered_search_ranges(&mut self) -> bool {
|
|
Self::supported_options().selection
|
|
}
|
|
|
|
fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext<Self>) {}
|
|
|
|
fn get_matches(&self, _: &mut WindowContext) -> Vec<Self::Match> {
|
|
Vec::new()
|
|
}
|
|
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
|
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
|
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
|
fn activate_match(&mut self, index: usize, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
|
fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
|
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
|
|
fn replace_all(
|
|
&mut self,
|
|
matches: &mut dyn Iterator<Item = &Self::Match>,
|
|
query: &SearchQuery,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
for item in matches {
|
|
self.replace(item, query, cx);
|
|
}
|
|
}
|
|
fn match_index_for_direction(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
current_index: usize,
|
|
direction: Direction,
|
|
count: usize,
|
|
_: &mut ViewContext<Self>,
|
|
) -> usize {
|
|
match direction {
|
|
Direction::Prev => {
|
|
let count = count % matches.len();
|
|
if current_index >= count {
|
|
current_index - count
|
|
} else {
|
|
matches.len() - (count - current_index)
|
|
}
|
|
}
|
|
Direction::Next => (current_index + count) % matches.len(),
|
|
}
|
|
}
|
|
fn find_matches(
|
|
&mut self,
|
|
query: Arc<SearchQuery>,
|
|
cx: &mut ViewContext<Self>,
|
|
) -> Task<Vec<Self::Match>>;
|
|
fn active_match_index(
|
|
&mut self,
|
|
matches: &[Self::Match],
|
|
cx: &mut ViewContext<Self>,
|
|
) -> Option<usize>;
|
|
}
|
|
|
|
pub trait SearchableItemHandle: ItemHandle {
|
|
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
|
|
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
|
|
fn supported_options(&self) -> SearchOptions;
|
|
fn subscribe_to_search_events(
|
|
&self,
|
|
cx: &mut WindowContext,
|
|
handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
|
|
) -> Subscription;
|
|
fn clear_matches(&self, cx: &mut WindowContext);
|
|
fn update_matches(&self, matches: &AnyVec<dyn Send>, cx: &mut WindowContext);
|
|
fn query_suggestion(&self, cx: &mut WindowContext) -> String;
|
|
fn activate_match(&self, index: usize, matches: &AnyVec<dyn Send>, cx: &mut WindowContext);
|
|
fn select_matches(&self, matches: &AnyVec<dyn Send>, cx: &mut WindowContext);
|
|
fn replace(
|
|
&self,
|
|
_: any_vec::element::ElementRef<'_, dyn Send>,
|
|
_: &SearchQuery,
|
|
_: &mut WindowContext,
|
|
);
|
|
fn replace_all(
|
|
&self,
|
|
matches: &mut dyn Iterator<Item = any_vec::element::ElementRef<'_, dyn Send>>,
|
|
query: &SearchQuery,
|
|
cx: &mut WindowContext,
|
|
);
|
|
fn match_index_for_direction(
|
|
&self,
|
|
matches: &AnyVec<dyn Send>,
|
|
current_index: usize,
|
|
direction: Direction,
|
|
count: usize,
|
|
cx: &mut WindowContext,
|
|
) -> usize;
|
|
fn find_matches(
|
|
&self,
|
|
query: Arc<SearchQuery>,
|
|
cx: &mut WindowContext,
|
|
) -> Task<AnyVec<dyn Send>>;
|
|
fn active_match_index(
|
|
&self,
|
|
matches: &AnyVec<dyn Send>,
|
|
cx: &mut WindowContext,
|
|
) -> Option<usize>;
|
|
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
|
|
|
|
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext);
|
|
}
|
|
|
|
impl<T: SearchableItem> SearchableItemHandle for View<T> {
|
|
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
|
|
Box::new(self.downgrade())
|
|
}
|
|
|
|
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
|
|
Box::new(self.clone())
|
|
}
|
|
|
|
fn supported_options(&self) -> SearchOptions {
|
|
T::supported_options()
|
|
}
|
|
|
|
fn subscribe_to_search_events(
|
|
&self,
|
|
cx: &mut WindowContext,
|
|
handler: Box<dyn Fn(&SearchEvent, &mut WindowContext) + Send>,
|
|
) -> Subscription {
|
|
cx.subscribe(self, move |_, event: &SearchEvent, cx| handler(event, cx))
|
|
}
|
|
|
|
fn clear_matches(&self, cx: &mut WindowContext) {
|
|
self.update(cx, |this, cx| this.clear_matches(cx));
|
|
}
|
|
fn update_matches(&self, matches: &AnyVec<dyn Send>, cx: &mut WindowContext) {
|
|
let matches = matches.downcast_ref().unwrap();
|
|
self.update(cx, |this, cx| this.update_matches(matches.as_slice(), cx));
|
|
}
|
|
fn query_suggestion(&self, cx: &mut WindowContext) -> String {
|
|
self.update(cx, |this, cx| this.query_suggestion(cx))
|
|
}
|
|
fn activate_match(&self, index: usize, matches: &AnyVec<dyn Send>, cx: &mut WindowContext) {
|
|
let matches = matches.downcast_ref().unwrap();
|
|
self.update(cx, |this, cx| {
|
|
this.activate_match(index, matches.as_slice(), cx)
|
|
});
|
|
}
|
|
|
|
fn select_matches(&self, matches: &AnyVec<dyn Send>, cx: &mut WindowContext) {
|
|
let matches = matches.downcast_ref().unwrap();
|
|
self.update(cx, |this, cx| this.select_matches(matches.as_slice(), cx));
|
|
}
|
|
|
|
fn match_index_for_direction(
|
|
&self,
|
|
matches: &AnyVec<dyn Send>,
|
|
current_index: usize,
|
|
direction: Direction,
|
|
count: usize,
|
|
cx: &mut WindowContext,
|
|
) -> usize {
|
|
let matches = matches.downcast_ref().unwrap();
|
|
self.update(cx, |this, cx| {
|
|
this.match_index_for_direction(matches.as_slice(), current_index, direction, count, cx)
|
|
})
|
|
}
|
|
fn find_matches(
|
|
&self,
|
|
query: Arc<SearchQuery>,
|
|
cx: &mut WindowContext,
|
|
) -> Task<AnyVec<dyn Send>> {
|
|
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
|
|
cx.spawn(|_| async {
|
|
let matches = matches.await;
|
|
let mut any_matches = AnyVec::with_capacity::<T::Match>(matches.len());
|
|
{
|
|
let mut any_matches = any_matches.downcast_mut::<T::Match>().unwrap();
|
|
for mat in matches {
|
|
any_matches.push(mat);
|
|
}
|
|
}
|
|
any_matches
|
|
})
|
|
}
|
|
fn active_match_index(
|
|
&self,
|
|
matches: &AnyVec<dyn Send>,
|
|
cx: &mut WindowContext,
|
|
) -> Option<usize> {
|
|
let matches = matches.downcast_ref()?;
|
|
self.update(cx, |this, cx| {
|
|
this.active_match_index(matches.as_slice(), cx)
|
|
})
|
|
}
|
|
|
|
fn replace(
|
|
&self,
|
|
mat: any_vec::element::ElementRef<'_, dyn Send>,
|
|
query: &SearchQuery,
|
|
cx: &mut WindowContext,
|
|
) {
|
|
let mat = mat.downcast_ref().unwrap();
|
|
self.update(cx, |this, cx| this.replace(mat, query, cx))
|
|
}
|
|
|
|
fn replace_all(
|
|
&self,
|
|
matches: &mut dyn Iterator<Item = any_vec::element::ElementRef<'_, dyn Send>>,
|
|
query: &SearchQuery,
|
|
cx: &mut WindowContext,
|
|
) {
|
|
self.update(cx, |this, cx| {
|
|
this.replace_all(&mut matches.map(|m| m.downcast_ref().unwrap()), query, cx);
|
|
})
|
|
}
|
|
|
|
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext) {
|
|
self.update(cx, |this, cx| {
|
|
this.search_bar_visibility_changed(visible, cx)
|
|
});
|
|
}
|
|
|
|
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext) {
|
|
self.update(cx, |this, cx| {
|
|
this.toggle_filtered_search_ranges(enabled, cx)
|
|
});
|
|
}
|
|
}
|
|
|
|
impl From<Box<dyn SearchableItemHandle>> for AnyView {
|
|
fn from(this: Box<dyn SearchableItemHandle>) -> Self {
|
|
this.to_any().clone()
|
|
}
|
|
}
|
|
|
|
impl From<&Box<dyn SearchableItemHandle>> for AnyView {
|
|
fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
|
|
this.to_any().clone()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Box<dyn SearchableItemHandle> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.item_id() == other.item_id()
|
|
}
|
|
}
|
|
|
|
impl Eq for Box<dyn SearchableItemHandle> {}
|
|
|
|
pub trait WeakSearchableItemHandle: WeakItemHandle {
|
|
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
|
|
|
fn into_any(self) -> AnyWeakView;
|
|
}
|
|
|
|
impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
|
|
fn upgrade(&self, _cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
|
|
Some(Box::new(self.upgrade()?))
|
|
}
|
|
|
|
fn into_any(self) -> AnyWeakView {
|
|
self.into()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.id() == other.id()
|
|
}
|
|
}
|
|
|
|
impl Eq for Box<dyn WeakSearchableItemHandle> {}
|
|
|
|
impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
self.id().hash(state)
|
|
}
|
|
}
|