mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 10:34:53 +03:00
SearchableItem trait is completed and editor searches appear to be working
This commit is contained in:
parent
d59911df26
commit
91a5d0b036
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
link_go_to_definition::hide_link_definition, Anchor, Autoscroll, Editor, Event, ExcerptId,
|
||||
MultiBuffer, NavigationData, ToPoint as _,
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
|
||||
MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
@ -8,20 +9,26 @@ use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||
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 rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use text::{Point, Selection};
|
||||
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 MAX_TAB_TITLE_LEN: usize = 24;
|
||||
@ -483,6 +490,10 @@ impl Item for Editor {
|
||||
fn is_edit_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::BufferEdited)
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
position: Option<Point>,
|
||||
selected_count: usize,
|
||||
|
@ -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 {
|
||||
fn from(handle: &AnyViewHandle) -> Self {
|
||||
handle.clone()
|
||||
@ -5163,6 +5171,7 @@ impl<T> Hash for WeakViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
pub struct AnyWeakViewHandle {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
|
@ -1,21 +1,22 @@
|
||||
use crate::{
|
||||
active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
|
||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleWholeWord,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Autoscroll, Editor};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
actions, elements::*, impl_actions, platform::CursorStyle, Action, AnyViewHandle, AppContext,
|
||||
Entity, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
ViewHandle,
|
||||
};
|
||||
use language::OffsetRangeExt;
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
|
||||
use std::any::Any;
|
||||
use workspace::{
|
||||
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
||||
ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
|
||||
};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct Deploy {
|
||||
@ -59,10 +60,11 @@ fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableApp
|
||||
|
||||
pub struct BufferSearchBar {
|
||||
pub query_editor: ViewHandle<Editor>,
|
||||
active_editor: Option<ViewHandle<Editor>>,
|
||||
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
||||
active_match_index: Option<usize>,
|
||||
active_editor_subscription: Option<Subscription>,
|
||||
editors_with_matches: HashMap<WeakViewHandle<Editor>, Vec<Range<Anchor>>>,
|
||||
active_searchable_item_subscription: Option<Subscription>,
|
||||
seachable_items_with_matches:
|
||||
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
|
||||
pending_search: Option<Task<()>>,
|
||||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
@ -103,22 +105,26 @@ impl View for BufferSearchBar {
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.active_editor.as_ref().and_then(|editor| {
|
||||
let matches = self.editors_with_matches.get(&editor.downgrade())?;
|
||||
let message = if let Some(match_ix) = self.active_match_index {
|
||||
format!("{}/{}", match_ix + 1, matches.len())
|
||||
} else {
|
||||
"No matches".to_string()
|
||||
};
|
||||
.with_children(self.active_searchable_item.as_ref().and_then(
|
||||
|searchable_item| {
|
||||
let matches = self
|
||||
.seachable_items_with_matches
|
||||
.get(&searchable_item.downgrade())?;
|
||||
let message = if let Some(match_ix) = self.active_match_index {
|
||||
format!("{}/{}", match_ix + 1, matches.len())
|
||||
} else {
|
||||
"No matches".to_string()
|
||||
};
|
||||
|
||||
Some(
|
||||
Label::new(message, theme.search.match_index.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
Some(
|
||||
Label::new(message, theme.search.match_index.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.search.match_index.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(editor_container)
|
||||
.aligned()
|
||||
@ -158,19 +164,25 @@ impl ToolbarItemView for BufferSearchBar {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_editor_subscription.take();
|
||||
self.active_editor.take();
|
||||
self.active_searchable_item_subscription.take();
|
||||
self.active_searchable_item.take();
|
||||
self.pending_search.take();
|
||||
|
||||
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
if editor.read(cx).searchable() {
|
||||
self.active_editor_subscription =
|
||||
Some(cx.subscribe(&editor, Self::on_active_editor_event));
|
||||
self.active_editor = Some(editor);
|
||||
self.update_matches(false, cx);
|
||||
if !self.dismissed {
|
||||
return ToolbarItemLocation::Secondary;
|
||||
}
|
||||
if let Some(searchable_item_handle) = item.and_then(|item| item.as_searchable(cx)) {
|
||||
let handle = cx.weak_handle();
|
||||
self.active_searchable_item_subscription = Some(searchable_item_handle.subscribe(
|
||||
cx,
|
||||
Box::new(move |search_event, cx| {
|
||||
if let Some(this) = handle.upgrade(cx) {
|
||||
this.update(cx, |this, cx| this.on_active_editor_event(search_event, cx));
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
||||
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,
|
||||
_: &AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
if self.active_editor.is_some() && !self.dismissed {
|
||||
if self.active_searchable_item.is_some() && !self.dismissed {
|
||||
ToolbarItemLocation::Secondary
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
@ -201,10 +213,10 @@ impl BufferSearchBar {
|
||||
|
||||
Self {
|
||||
query_editor,
|
||||
active_editor: None,
|
||||
active_editor_subscription: None,
|
||||
active_searchable_item: None,
|
||||
active_searchable_item_subscription: None,
|
||||
active_match_index: None,
|
||||
editors_with_matches: Default::default(),
|
||||
seachable_items_with_matches: Default::default(),
|
||||
case_sensitive: false,
|
||||
whole_word: false,
|
||||
regex: false,
|
||||
@ -216,14 +228,14 @@ impl BufferSearchBar {
|
||||
|
||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
self.dismissed = true;
|
||||
for editor in self.editors_with_matches.keys() {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
for searchable_item in self.seachable_items_with_matches.keys() {
|
||||
if let Some(searchable_item) =
|
||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), 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.emit(Event::UpdateLocation);
|
||||
@ -231,14 +243,14 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
|
||||
let editor = if let Some(editor) = self.active_editor.clone() {
|
||||
editor
|
||||
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
||||
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if suggest_query {
|
||||
let text = query_suggestion_for_editor(&editor, cx);
|
||||
let text = searchable_item.query_suggestion(cx);
|
||||
if !text.is_empty() {
|
||||
self.set_query(&text, cx);
|
||||
}
|
||||
@ -369,7 +381,7 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -403,23 +415,13 @@ impl BufferSearchBar {
|
||||
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
if let Some(editor) = self.active_editor.as_ref() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
|
||||
let new_index = match_index_for_direction(
|
||||
ranges,
|
||||
&editor.selections.newest_anchor().head(),
|
||||
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])
|
||||
});
|
||||
}
|
||||
});
|
||||
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
||||
if let Some(matches) = self
|
||||
.seachable_items_with_matches
|
||||
.get(&searchable_item.downgrade())
|
||||
{
|
||||
searchable_item.select_next_match_in_direction(index, direction, matches, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -458,46 +460,44 @@ impl BufferSearchBar {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_active_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
|
||||
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
|
||||
_ => {}
|
||||
SearchEvent::ContentsUpdated => self.update_matches(false, cx),
|
||||
SearchEvent::SelectionsChanged => self.update_match_index(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut active_editor_matches = None;
|
||||
for (editor, ranges) in self.editors_with_matches.drain() {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
if Some(&editor) == self.active_editor.as_ref() {
|
||||
active_editor_matches = Some((editor.downgrade(), ranges));
|
||||
for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
|
||||
if let Some(searchable_item) =
|
||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
||||
{
|
||||
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 {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
searchable_item.clear_highlights(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>) {
|
||||
let query = self.query_editor.read(cx).text(cx);
|
||||
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() {
|
||||
self.active_match_index.take();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
active_searchable_item.clear_highlights(cx);
|
||||
} else {
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let query = if self.regex {
|
||||
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
|
||||
Ok(query) => query,
|
||||
@ -511,66 +511,36 @@ impl BufferSearchBar {
|
||||
SearchQuery::text(query, self.whole_word, self.case_sensitive)
|
||||
};
|
||||
|
||||
let ranges = 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
|
||||
});
|
||||
let matches = active_searchable_item.matches(query, cx);
|
||||
|
||||
let editor = editor.downgrade();
|
||||
let active_searchable_item = active_searchable_item.downgrade();
|
||||
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
let ranges = ranges.await;
|
||||
if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) {
|
||||
let matches = matches.await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.editors_with_matches
|
||||
.insert(editor.downgrade(), ranges.clone());
|
||||
this.update_match_index(cx);
|
||||
if !this.dismissed {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(
|
||||
active_searchable_item.as_ref(),
|
||||
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 let Some(match_ix) = this.active_match_index {
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::Fit),
|
||||
active_searchable_item.select_match_by_index(
|
||||
match_ix,
|
||||
this.seachable_items_with_matches
|
||||
.get(&active_searchable_item.downgrade())
|
||||
.unwrap(),
|
||||
cx,
|
||||
|s| s.select_ranges([ranges[match_ix].clone()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editor.highlight_background::<Self>(
|
||||
ranges,
|
||||
|theme| theme.search.match_background,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}));
|
||||
@ -579,15 +549,15 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let new_index = self.active_editor.as_ref().and_then(|editor| {
|
||||
let ranges = self.editors_with_matches.get(&editor.downgrade())?;
|
||||
let editor = editor.read(cx);
|
||||
active_match_index(
|
||||
ranges,
|
||||
&editor.selections.newest_anchor().head(),
|
||||
&editor.buffer().read(cx).snapshot(cx),
|
||||
)
|
||||
});
|
||||
let new_index = self
|
||||
.active_searchable_item
|
||||
.as_ref()
|
||||
.and_then(|searchable_item| {
|
||||
let matches = self
|
||||
.seachable_items_with_matches
|
||||
.get(&searchable_item.downgrade())?;
|
||||
searchable_item.active_match_index(matches, cx)
|
||||
});
|
||||
if new_index != self.active_match_index {
|
||||
self.active_match_index = new_index;
|
||||
cx.notify();
|
||||
|
@ -1,10 +1,12 @@
|
||||
use crate::{
|
||||
active_match_index, match_index_for_direction, query_suggestion_for_editor, Direction,
|
||||
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleWholeWord,
|
||||
};
|
||||
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::{
|
||||
actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,
|
||||
Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
|
||||
@ -21,6 +23,7 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
searchable::{Direction, SearchableItemHandle},
|
||||
Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
@ -429,7 +432,7 @@ impl ProjectSearchView {
|
||||
|
||||
let query = workspace.active_item(cx).and_then(|item| {
|
||||
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() {
|
||||
None
|
||||
} else {
|
||||
|
@ -1,11 +1,6 @@
|
||||
pub use buffer_search::BufferSearchBar;
|
||||
use editor::{display_map::ToDisplayPoint, Anchor, Bias, Editor, MultiBufferSnapshot};
|
||||
use gpui::{actions, Action, MutableAppContext, ViewHandle};
|
||||
use gpui::{actions, Action, MutableAppContext};
|
||||
pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
pub mod buffer_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()
|
||||
}
|
||||
}
|
||||
|
198
crates/workspace/src/searchable.rs
Normal file
198
crates/workspace/src/searchable.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
/// specific locations.
|
||||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
pub mod searchable;
|
||||
pub mod sidebar;
|
||||
mod status_bar;
|
||||
mod toolbar;
|
||||
@ -36,6 +37,7 @@ pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
use postage::prelude::Stream;
|
||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||
use searchable::SearchableItemHandle;
|
||||
use serde::Deserialize;
|
||||
use settings::{Autosave, Settings};
|
||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||
@ -325,6 +327,9 @@ pub trait Item: View {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProjectItem: Item {
|
||||
@ -438,6 +443,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn workspace_deactivated(&self, cx: &mut MutableAppContext);
|
||||
fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
|
||||
fn id(&self) -> usize;
|
||||
fn window_id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool;
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool;
|
||||
@ -458,10 +464,12 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
cx: &mut MutableAppContext,
|
||||
callback: Box<dyn FnOnce(&mut MutableAppContext)>,
|
||||
) -> gpui::Subscription;
|
||||
fn as_searchable(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn window_id(&self) -> usize;
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
|
||||
}
|
||||
|
||||
@ -670,6 +678,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn window_id(&self) -> usize {
|
||||
self.window_id()
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
@ -728,6 +740,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
) -> gpui::Subscription {
|
||||
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 {
|
||||
@ -753,6 +769,10 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn window_id(&self) -> usize {
|
||||
self.window_id()
|
||||
}
|
||||
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user