SearchableItem trait is completed and editor searches appear to be working

This commit is contained in:
K Simmons 2022-08-30 15:37:54 -07:00
parent d59911df26
commit 91a5d0b036
7 changed files with 575 additions and 250 deletions

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
link_go_to_definition::hide_link_definition, Anchor, Autoscroll, Editor, Event, ExcerptId, display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
MultiBuffer, NavigationData, ToPoint as _, movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
MultiBufferSnapshot, NavigationData, ToPoint as _,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::FutureExt; use futures::FutureExt;
@ -8,20 +9,26 @@ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
}; };
use language::{Bias, Buffer, File as _, SelectionGoal}; use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
use project::{File, Project, ProjectEntryId, ProjectPath}; use project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
any::Any,
borrow::Cow, borrow::Cow,
cmp::{self, Ordering},
fmt::Write, fmt::Write,
ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration, time::Duration,
}; };
use text::{Point, Selection}; use text::{Point, Selection};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView}; use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
};
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub const MAX_TAB_TITLE_LEN: usize = 24; pub const MAX_TAB_TITLE_LEN: usize = 24;
@ -483,6 +490,10 @@ impl Item for Editor {
fn is_edit_event(event: &Self::Event) -> bool { fn is_edit_event(event: &Self::Event) -> bool {
matches!(event, Event::BufferEdited) matches!(event, Event::BufferEdited)
} }
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
} }
impl ProjectItem for Editor { impl ProjectItem for Editor {
@ -497,6 +508,215 @@ impl ProjectItem for Editor {
} }
} }
enum BufferSearchHighlights {}
impl SearchableItem for Editor {
fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
match event {
Event::BufferEdited => Some(SearchEvent::ContentsUpdated),
Event::SelectionsChanged { .. } => Some(SearchEvent::SelectionsChanged),
_ => None,
}
}
fn clear_highlights(&mut self, cx: &mut ViewContext<Self>) {
self.clear_background_highlights::<BufferSearchHighlights>(cx);
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
let display_map = self.snapshot(cx).display_snapshot;
let selection = self.selections.newest::<usize>(cx);
if selection.start == selection.end {
let point = selection.start.to_display_point(&display_map);
let range = surrounding_word(&display_map, point);
let range = range.start.to_offset(&display_map, Bias::Left)
..range.end.to_offset(&display_map, Bias::Right);
let text: String = display_map.buffer_snapshot.text_for_range(range).collect();
if text.trim().is_empty() {
String::new()
} else {
text
}
} else {
display_map
.buffer_snapshot
.text_for_range(selection.start..selection.end)
.collect()
}
}
fn select_next_match_in_direction(
&mut self,
index: usize,
direction: Direction,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
) {
if let Some(matches) = matches
.iter()
.map(|range| range.downcast_ref::<Range<Anchor>>().cloned())
.collect::<Option<Vec<_>>>()
{
let new_index: usize = match_index_for_direction(
matches.as_slice(),
&self.selections.newest_anchor().head(),
index,
direction,
&self.buffer().read(cx).snapshot(cx),
);
let range_to_select = matches[new_index].clone();
self.unfold_ranges([range_to_select.clone()], false, cx);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([range_to_select])
});
} else {
log::error!("Select next match in direction called with unexpected type matches");
}
}
fn select_match_by_index(
&mut self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
) {
if let Some(matches) = matches
.iter()
.map(|range| range.downcast_ref::<Range<Anchor>>().cloned())
.collect::<Option<Vec<_>>>()
{
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([matches[index].clone()])
});
self.highlight_background::<BufferSearchHighlights>(
matches,
|theme| theme.search.match_background,
cx,
);
} else {
log::error!("Select next match in direction called with unexpected type matches");
}
}
fn matches(
&mut self,
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Box<dyn Any + Send>>> {
let buffer = self.buffer().read(cx).snapshot(cx);
cx.background().spawn(async move {
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend(
query
.search(excerpt_buffer.as_rope())
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
}),
);
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
ranges.extend(query.search(&rope).await.into_iter().map(|range| {
let start = excerpt
.buffer
.anchor_after(excerpt_range.start + range.start);
let end = excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
}));
}
}
ranges
.into_iter()
.map::<Box<dyn Any + Send>, _>(|range| Box::new(range))
.collect()
})
}
fn active_match_index(
&mut self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
if let Some(matches) = matches
.iter()
.map(|range| range.downcast_ref::<Range<Anchor>>().cloned())
.collect::<Option<Vec<_>>>()
{
active_match_index(
&matches,
&self.selections.newest_anchor().head(),
&self.buffer().read(cx).snapshot(cx),
)
} else {
None
}
}
}
pub fn match_index_for_direction(
ranges: &[Range<Anchor>],
cursor: &Anchor,
mut index: usize,
direction: Direction,
buffer: &MultiBufferSnapshot,
) -> usize {
if ranges[index].start.cmp(cursor, buffer).is_gt() {
if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
}
} else if ranges[index].end.cmp(cursor, buffer).is_lt() {
if direction == Direction::Next {
index = 0;
}
} else if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
} else if direction == Direction::Next {
if index == ranges.len() - 1 {
index = 0
} else {
index += 1;
}
};
index
}
pub fn active_match_index(
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
) -> Option<usize> {
if ranges.is_empty() {
None
} else {
match ranges.binary_search_by(|probe| {
if probe.end.cmp(cursor, &*buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(cursor, &*buffer).is_gt() {
Ordering::Greater
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
}
}
}
pub struct CursorPosition { pub struct CursorPosition {
position: Option<Point>, position: Option<Point>,
selected_count: usize, selected_count: usize,

View File

@ -4938,6 +4938,14 @@ impl Clone for AnyViewHandle {
} }
} }
impl PartialEq for AnyViewHandle {
fn eq(&self, other: &Self) -> bool {
self.window_id == other.window_id
&& self.view_id == other.view_id
&& self.view_type == other.view_type
}
}
impl From<&AnyViewHandle> for AnyViewHandle { impl From<&AnyViewHandle> for AnyViewHandle {
fn from(handle: &AnyViewHandle) -> Self { fn from(handle: &AnyViewHandle) -> Self {
handle.clone() handle.clone()
@ -5163,6 +5171,7 @@ impl<T> Hash for WeakViewHandle<T> {
} }
} }
#[derive(Eq, PartialEq, Hash)]
pub struct AnyWeakViewHandle { pub struct AnyWeakViewHandle {
window_id: usize, window_id: usize,
view_id: usize, view_id: usize,

View File

@ -1,21 +1,22 @@
use crate::{ use crate::{
active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord, ToggleWholeWord,
}; };
use collections::HashMap; use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor}; use editor::Editor;
use gpui::{ use gpui::{
actions, elements::*, impl_actions, platform::CursorStyle, Action, AnyViewHandle, AppContext, actions, elements::*, impl_actions, platform::CursorStyle, Action, AnyViewHandle, AppContext,
Entity, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, Entity, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
ViewHandle, WeakViewHandle, ViewHandle,
}; };
use language::OffsetRangeExt;
use project::search::SearchQuery; use project::search::SearchQuery;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::ops::Range; use std::any::Any;
use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView}; use workspace::{
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
};
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
pub struct Deploy { pub struct Deploy {
@ -59,10 +60,11 @@ fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableApp
pub struct BufferSearchBar { pub struct BufferSearchBar {
pub query_editor: ViewHandle<Editor>, pub query_editor: ViewHandle<Editor>,
active_editor: Option<ViewHandle<Editor>>, active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>, active_match_index: Option<usize>,
active_editor_subscription: Option<Subscription>, active_searchable_item_subscription: Option<Subscription>,
editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>, seachable_items_with_matches:
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
pending_search: Option<Task<()>>, pending_search: Option<Task<()>>,
case_sensitive: bool, case_sensitive: bool,
whole_word: bool, whole_word: bool,
@ -103,22 +105,26 @@ impl View for BufferSearchBar {
.flex(1., true) .flex(1., true)
.boxed(), .boxed(),
) )
.with_children(self.active_editor.as_ref().and_then(|editor| { .with_children(self.active_searchable_item.as_ref().and_then(
let matches = self.editors_with_matches.get(&editor.downgrade())?; |searchable_item| {
let message = if let Some(match_ix) = self.active_match_index { let matches = self
format!("{}/{}", match_ix + 1, matches.len()) .seachable_items_with_matches
} else { .get(&searchable_item.downgrade())?;
"No matches".to_string() let message = if let Some(match_ix) = self.active_match_index {
}; format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some( Some(
Label::new(message, theme.search.match_index.text.clone()) Label::new(message, theme.search.match_index.text.clone())
.contained() .contained()
.with_style(theme.search.match_index.container) .with_style(theme.search.match_index.container)
.aligned() .aligned()
.boxed(), .boxed(),
) )
})) },
))
.contained() .contained()
.with_style(editor_container) .with_style(editor_container)
.aligned() .aligned()
@ -158,19 +164,25 @@ impl ToolbarItemView for BufferSearchBar {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation { ) -> ToolbarItemLocation {
cx.notify(); cx.notify();
self.active_editor_subscription.take(); self.active_searchable_item_subscription.take();
self.active_editor.take(); self.active_searchable_item.take();
self.pending_search.take(); self.pending_search.take();
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) { if let Some(searchable_item_handle) = item.and_then(|item| item.as_searchable(cx)) {
if editor.read(cx).searchable() { let handle = cx.weak_handle();
self.active_editor_subscription = self.active_searchable_item_subscription = Some(searchable_item_handle.subscribe(
Some(cx.subscribe(&editor, Self::on_active_editor_event)); cx,
self.active_editor = Some(editor); Box::new(move |search_event, cx| {
self.update_matches(false, cx); if let Some(this) = handle.upgrade(cx) {
if !self.dismissed { this.update(cx, |this, cx| this.on_active_editor_event(search_event, cx));
return ToolbarItemLocation::Secondary; }
} }),
));
self.active_searchable_item = Some(searchable_item_handle);
self.update_matches(false, cx);
if !self.dismissed {
return ToolbarItemLocation::Secondary;
} }
} }
@ -183,7 +195,7 @@ impl ToolbarItemView for BufferSearchBar {
_: ToolbarItemLocation, _: ToolbarItemLocation,
_: &AppContext, _: &AppContext,
) -> ToolbarItemLocation { ) -> ToolbarItemLocation {
if self.active_editor.is_some() && !self.dismissed { if self.active_searchable_item.is_some() && !self.dismissed {
ToolbarItemLocation::Secondary ToolbarItemLocation::Secondary
} else { } else {
ToolbarItemLocation::Hidden ToolbarItemLocation::Hidden
@ -201,10 +213,10 @@ impl BufferSearchBar {
Self { Self {
query_editor, query_editor,
active_editor: None, active_searchable_item: None,
active_editor_subscription: None, active_searchable_item_subscription: None,
active_match_index: None, active_match_index: None,
editors_with_matches: Default::default(), seachable_items_with_matches: Default::default(),
case_sensitive: false, case_sensitive: false,
whole_word: false, whole_word: false,
regex: false, regex: false,
@ -216,14 +228,14 @@ impl BufferSearchBar {
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) { fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.dismissed = true; self.dismissed = true;
for editor in self.editors_with_matches.keys() { for searchable_item in self.seachable_items_with_matches.keys() {
if let Some(editor) = editor.upgrade(cx) { if let Some(searchable_item) =
editor.update(cx, |editor, cx| { WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
editor.clear_background_highlights::<Self>(cx) {
}); searchable_item.clear_highlights(cx);
} }
} }
if let Some(active_editor) = self.active_editor.as_ref() { if let Some(active_editor) = self.active_searchable_item.as_ref() {
cx.focus(active_editor); cx.focus(active_editor);
} }
cx.emit(Event::UpdateLocation); cx.emit(Event::UpdateLocation);
@ -231,14 +243,14 @@ impl BufferSearchBar {
} }
fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool { fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
let editor = if let Some(editor) = self.active_editor.clone() { let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
editor SearchableItemHandle::boxed_clone(searchable_item.as_ref())
} else { } else {
return false; return false;
}; };
if suggest_query { if suggest_query {
let text = query_suggestion_for_editor(&editor, cx); let text = searchable_item.query_suggestion(cx);
if !text.is_empty() { if !text.is_empty() {
self.set_query(&text, cx); self.set_query(&text, cx);
} }
@ -369,7 +381,7 @@ impl BufferSearchBar {
} }
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) { fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_editor.as_ref() { if let Some(active_editor) = self.active_searchable_item.as_ref() {
cx.focus(active_editor); cx.focus(active_editor);
} }
} }
@ -403,23 +415,13 @@ impl BufferSearchBar {
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) { fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index { if let Some(index) = self.active_match_index {
if let Some(editor) = self.active_editor.as_ref() { if let Some(searchable_item) = self.active_searchable_item.as_ref() {
editor.update(cx, |editor, cx| { if let Some(matches) = self
if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { .seachable_items_with_matches
let new_index = match_index_for_direction( .get(&searchable_item.downgrade())
ranges, {
&editor.selections.newest_anchor().head(), searchable_item.select_next_match_in_direction(index, direction, matches, cx);
index, }
direction,
&editor.buffer().read(cx).snapshot(cx),
);
let range_to_select = ranges[new_index].clone();
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([range_to_select])
});
}
});
} }
} }
} }
@ -458,46 +460,44 @@ impl BufferSearchBar {
} }
} }
fn on_active_editor_event( fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
&mut self,
_: ViewHandle<Editor>,
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
match event { match event {
editor::Event::BufferEdited { .. } => self.update_matches(false, cx), SearchEvent::ContentsUpdated => self.update_matches(false, cx),
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx), SearchEvent::SelectionsChanged => self.update_match_index(cx),
_ => {}
} }
} }
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) { fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
let mut active_editor_matches = None; let mut active_editor_matches = None;
for (editor, ranges) in self.editors_with_matches.drain() { for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
if let Some(editor) = editor.upgrade(cx) { if let Some(searchable_item) =
if Some(&editor) == self.active_editor.as_ref() { WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
active_editor_matches = Some((editor.downgrade(), ranges)); {
if self
.active_searchable_item
.as_ref()
.map(|active_item| active_item == &searchable_item)
.unwrap_or(false)
{
active_editor_matches = Some((searchable_item.downgrade(), matches));
} else { } else {
editor.update(cx, |editor, cx| { searchable_item.clear_highlights(cx);
editor.clear_background_highlights::<Self>(cx)
});
} }
} }
} }
self.editors_with_matches.extend(active_editor_matches);
self.seachable_items_with_matches
.extend(active_editor_matches);
} }
fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) { fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
let query = self.query_editor.read(cx).text(cx); let query = self.query_editor.read(cx).text(cx);
self.pending_search.take(); self.pending_search.take();
if let Some(editor) = self.active_editor.as_ref() { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if query.is_empty() { if query.is_empty() {
self.active_match_index.take(); self.active_match_index.take();
editor.update(cx, |editor, cx| { active_searchable_item.clear_highlights(cx);
editor.clear_background_highlights::<Self>(cx)
});
} else { } else {
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let query = if self.regex { let query = if self.regex {
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) { match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
Ok(query) => query, Ok(query) => query,
@ -511,66 +511,36 @@ impl BufferSearchBar {
SearchQuery::text(query, self.whole_word, self.case_sensitive) SearchQuery::text(query, self.whole_word, self.case_sensitive)
}; };
let ranges = cx.background().spawn(async move { let matches = active_searchable_item.matches(query, cx);
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend(
query
.search(excerpt_buffer.as_rope())
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start)
..buffer.anchor_before(range.end)
}),
);
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
ranges.extend(query.search(&rope).await.into_iter().map(|range| {
let start = excerpt
.buffer
.anchor_after(excerpt_range.start + range.start);
let end = excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(excerpt.id.clone(), start)
..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
}));
}
}
ranges
});
let editor = editor.downgrade(); let active_searchable_item = active_searchable_item.downgrade();
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
let ranges = ranges.await; let matches = matches.await;
if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.editors_with_matches if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(
.insert(editor.downgrade(), ranges.clone()); active_searchable_item.as_ref(),
this.update_match_index(cx); cx,
if !this.dismissed { ) {
editor.update(cx, |editor, cx| { this.seachable_items_with_matches
.insert(active_searchable_item.downgrade(), matches);
this.update_match_index(cx);
if !this.dismissed {
if select_closest_match { if select_closest_match {
if let Some(match_ix) = this.active_match_index { if let Some(match_ix) = this.active_match_index {
editor.change_selections( active_searchable_item.select_match_by_index(
Some(Autoscroll::Fit), match_ix,
this.seachable_items_with_matches
.get(&active_searchable_item.downgrade())
.unwrap(),
cx, cx,
|s| s.select_ranges([ranges[match_ix].clone()]),
); );
} }
} }
}
editor.highlight_background::<Self>( cx.notify();
ranges,
|theme| theme.search.match_background,
cx,
);
});
} }
cx.notify();
}); });
} }
})); }));
@ -579,15 +549,15 @@ impl BufferSearchBar {
} }
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) { fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
let new_index = self.active_editor.as_ref().and_then(|editor| { let new_index = self
let ranges = self.editors_with_matches.get(&editor.downgrade())?; .active_searchable_item
let editor = editor.read(cx); .as_ref()
active_match_index( .and_then(|searchable_item| {
ranges, let matches = self
&editor.selections.newest_anchor().head(), .seachable_items_with_matches
&editor.buffer().read(cx).snapshot(cx), .get(&searchable_item.downgrade())?;
) searchable_item.active_match_index(matches, cx)
}); });
if new_index != self.active_match_index { if new_index != self.active_match_index {
self.active_match_index = new_index; self.active_match_index = new_index;
cx.notify(); cx.notify();

View File

@ -1,10 +1,12 @@
use crate::{ use crate::{
active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord, ToggleWholeWord,
}; };
use collections::HashMap; use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN}; use editor::{
items::{active_match_index, match_index_for_direction},
Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
};
use gpui::{ use gpui::{
actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,
Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
@ -21,6 +23,7 @@ use std::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ use workspace::{
searchable::{Direction, SearchableItemHandle},
Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
}; };
@ -429,7 +432,7 @@ impl ProjectSearchView {
let query = workspace.active_item(cx).and_then(|item| { let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?; let editor = item.act_as::<Editor>(cx)?;
let query = query_suggestion_for_editor(&editor, cx); let query = editor.query_suggestion(cx);
if query.is_empty() { if query.is_empty() {
None None
} else { } else {

View File

@ -1,11 +1,6 @@
pub use buffer_search::BufferSearchBar; pub use buffer_search::BufferSearchBar;
use editor::{display_map::ToDisplayPoint, Anchor, Bias, Editor, MultiBufferSnapshot}; use gpui::{actions, Action, MutableAppContext};
use gpui::{actions, Action, MutableAppContext, ViewHandle};
pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub use project_search::{ProjectSearchBar, ProjectSearchView};
use std::{
cmp::{self, Ordering},
ops::Range,
};
pub mod buffer_search; pub mod buffer_search;
pub mod project_search; pub mod project_search;
@ -50,93 +45,3 @@ impl SearchOption {
} }
} }
} }
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
pub(crate) fn active_match_index(
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
) -> Option<usize> {
if ranges.is_empty() {
None
} else {
match ranges.binary_search_by(|probe| {
if probe.end.cmp(cursor, &*buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(cursor, &*buffer).is_gt() {
Ordering::Greater
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
}
}
}
pub(crate) fn match_index_for_direction(
ranges: &[Range<Anchor>],
cursor: &Anchor,
mut index: usize,
direction: Direction,
buffer: &MultiBufferSnapshot,
) -> usize {
if ranges[index].start.cmp(cursor, buffer).is_gt() {
if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
}
} else if ranges[index].end.cmp(cursor, buffer).is_lt() {
if direction == Direction::Next {
index = 0;
}
} else if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
} else if direction == Direction::Next {
if index == ranges.len() - 1 {
index = 0
} else {
index += 1;
}
};
index
}
pub(crate) fn query_suggestion_for_editor(
editor: &ViewHandle<Editor>,
cx: &mut MutableAppContext,
) -> String {
let display_map = editor
.update(cx, |editor, cx| editor.snapshot(cx))
.display_snapshot;
let selection = editor.read(cx).selections.newest::<usize>(cx);
if selection.start == selection.end {
let point = selection.start.to_display_point(&display_map);
let range = editor::movement::surrounding_word(&display_map, point);
let range = range.start.to_offset(&display_map, Bias::Left)
..range.end.to_offset(&display_map, Bias::Right);
let text: String = display_map.buffer_snapshot.text_for_range(range).collect();
if text.trim().is_empty() {
String::new()
} else {
text
}
} else {
display_map
.buffer_snapshot
.text_for_range(selection.start..selection.end)
.collect()
}
}

View File

@ -0,0 +1,198 @@
use std::any::Any;
use gpui::{
AnyViewHandle, AnyWeakViewHandle, AppContext, MutableAppContext, Subscription, Task,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::search::SearchQuery;
use crate::{Item, ItemHandle, WeakItemHandle};
pub enum SearchEvent {
ContentsUpdated,
SelectionsChanged,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
pub trait SearchableItem: Item {
fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
fn clear_highlights(&mut self, cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
fn select_next_match_in_direction(
&mut self,
index: usize,
direction: Direction,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
);
fn select_match_by_index(
&mut self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
);
fn matches(
&mut self,
query: SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Box<dyn Any + Send>>>;
fn active_match_index(
&mut self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut ViewContext<Self>,
) -> Option<usize>;
}
pub trait SearchableItemHandle: ItemHandle {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
fn subscribe(
&self,
cx: &mut MutableAppContext,
handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
) -> Subscription;
fn clear_highlights(&self, cx: &mut MutableAppContext);
fn query_suggestion(&self, cx: &mut MutableAppContext) -> String;
fn select_next_match_in_direction(
&self,
index: usize,
direction: Direction,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
);
fn select_match_by_index(
&self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
);
fn matches(
&self,
query: SearchQuery,
cx: &mut MutableAppContext,
) -> Task<Vec<Box<dyn Any + Send>>>;
fn active_match_index(
&self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
) -> Option<usize>;
}
impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
Box::new(self.downgrade())
}
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
Box::new(self.clone())
}
fn subscribe(
&self,
cx: &mut MutableAppContext,
handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
) -> Subscription {
cx.subscribe(self, move |_, event, cx| {
if let Some(search_event) = T::to_search_event(event) {
handler(search_event, cx)
}
})
}
fn clear_highlights(&self, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| this.clear_highlights(cx));
}
fn query_suggestion(&self, cx: &mut MutableAppContext) -> String {
self.update(cx, |this, cx| this.query_suggestion(cx))
}
fn select_next_match_in_direction(
&self,
index: usize,
direction: Direction,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
) {
self.update(cx, |this, cx| {
this.select_next_match_in_direction(index, direction, matches, cx)
});
}
fn select_match_by_index(
&self,
index: usize,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
) {
self.update(cx, |this, cx| {
this.select_match_by_index(index, matches, cx)
});
}
fn matches(
&self,
query: SearchQuery,
cx: &mut MutableAppContext,
) -> Task<Vec<Box<dyn Any + Send>>> {
self.update(cx, |this, cx| this.matches(query, cx))
}
fn active_match_index(
&self,
matches: &Vec<Box<dyn Any + Send>>,
cx: &mut MutableAppContext,
) -> Option<usize> {
self.update(cx, |this, cx| this.active_match_index(matches, cx))
}
}
impl From<Box<dyn SearchableItemHandle>> for AnyViewHandle {
fn from(this: Box<dyn SearchableItemHandle>) -> Self {
this.to_any()
}
}
impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
this.to_any()
}
}
impl PartialEq for Box<dyn SearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window_id() == other.window_id()
}
}
impl Eq for Box<dyn SearchableItemHandle> {}
pub trait WeakSearchableItemHandle: WeakItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
fn to_any(self) -> AnyWeakViewHandle;
}
impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.upgrade(cx)?))
}
fn to_any(self) -> AnyWeakViewHandle {
self.into()
}
}
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window_id() == other.window_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(), self.window_id()).hash(state)
}
}

View File

@ -5,6 +5,7 @@
/// specific locations. /// specific locations.
pub mod pane; pub mod pane;
pub mod pane_group; pub mod pane_group;
pub mod searchable;
pub mod sidebar; pub mod sidebar;
mod status_bar; mod status_bar;
mod toolbar; mod toolbar;
@ -36,6 +37,7 @@ pub use pane::*;
pub use pane_group::*; pub use pane_group::*;
use postage::prelude::Stream; use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use searchable::SearchableItemHandle;
use serde::Deserialize; use serde::Deserialize;
use settings::{Autosave, Settings}; use settings::{Autosave, Settings};
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem}; use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
@ -325,6 +327,9 @@ pub trait Item: View {
None None
} }
} }
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
} }
pub trait ProjectItem: Item { pub trait ProjectItem: Item {
@ -438,6 +443,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn workspace_deactivated(&self, cx: &mut MutableAppContext); fn workspace_deactivated(&self, cx: &mut MutableAppContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool; fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
fn id(&self) -> usize; fn id(&self) -> usize;
fn window_id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle; fn to_any(&self) -> AnyViewHandle;
fn is_dirty(&self, cx: &AppContext) -> bool; fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool;
@ -458,10 +464,12 @@ pub trait ItemHandle: 'static + fmt::Debug {
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
callback: Box<dyn FnOnce(&mut MutableAppContext)>, callback: Box<dyn FnOnce(&mut MutableAppContext)>,
) -> gpui::Subscription; ) -> gpui::Subscription;
fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
} }
pub trait WeakItemHandle { pub trait WeakItemHandle {
fn id(&self) -> usize; fn id(&self) -> usize;
fn window_id(&self) -> usize;
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>; fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
} }
@ -670,6 +678,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.id() self.id()
} }
fn window_id(&self) -> usize {
self.window_id()
}
fn to_any(&self) -> AnyViewHandle { fn to_any(&self) -> AnyViewHandle {
self.into() self.into()
} }
@ -728,6 +740,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
) -> gpui::Subscription { ) -> gpui::Subscription {
cx.observe_release(self, move |_, cx| callback(cx)) cx.observe_release(self, move |_, cx| callback(cx))
} }
fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
self.read(cx).as_searchable(self)
}
} }
impl From<Box<dyn ItemHandle>> for AnyViewHandle { impl From<Box<dyn ItemHandle>> for AnyViewHandle {
@ -753,6 +769,10 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
self.id() self.id()
} }
fn window_id(&self) -> usize {
self.window_id()
}
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> { fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>) self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
} }