Detailed changes
@@ -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()
- };
-
- Some(
- Label::new(message, theme.search.match_index.text.clone())
- .contained()
- .with_style(theme.search.match_index.container)
- .aligned()
- .boxed(),
- )
- }))
+ .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(),
+ )
+ },
+ ))
.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()
- }
-}
@@ -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>)
}