From 91a5d0b036c40d061b4971973a53cf97a6efe5c7 Mon Sep 17 00:00:00 2001 From: K Simmons Date: Tue, 30 Aug 2022 15:37:54 -0700 Subject: [PATCH] SearchableItem trait is completed and editor searches appear to be working --- crates/editor/src/items.rs | 228 +++++++++++++++++++++++- crates/gpui/src/app.rs | 9 + crates/search/src/buffer_search.rs | 264 ++++++++++++---------------- crates/search/src/project_search.rs | 9 +- crates/search/src/search.rs | 97 +--------- crates/workspace/src/searchable.rs | 198 +++++++++++++++++++++ crates/workspace/src/workspace.rs | 20 +++ 7 files changed, 575 insertions(+), 250 deletions(-) create mode 100644 crates/workspace/src/searchable.rs diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 04d649f910..00f83d79e1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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) -> Option> { + 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 { + match event { + Event::BufferEdited => Some(SearchEvent::ContentsUpdated), + Event::SelectionsChanged { .. } => Some(SearchEvent::SelectionsChanged), + _ => None, + } + } + + fn clear_highlights(&mut self, cx: &mut ViewContext) { + self.clear_background_highlights::(cx); + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + let display_map = self.snapshot(cx).display_snapshot; + let selection = self.selections.newest::(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>, + cx: &mut ViewContext, + ) { + if let Some(matches) = matches + .iter() + .map(|range| range.downcast_ref::>().cloned()) + .collect::>>() + { + 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>, + cx: &mut ViewContext, + ) { + if let Some(matches) = matches + .iter() + .map(|range| range.downcast_ref::>().cloned()) + .collect::>>() + { + self.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.select_ranges([matches[index].clone()]) + }); + self.highlight_background::( + 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, + ) -> Task>> { + 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::, _>(|range| Box::new(range)) + .collect() + }) + } + + fn active_match_index( + &mut self, + matches: &Vec>, + cx: &mut ViewContext, + ) -> Option { + if let Some(matches) = matches + .iter() + .map(|range| range.downcast_ref::>().cloned()) + .collect::>>() + { + 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], + 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], + cursor: &Anchor, + buffer: &MultiBufferSnapshot, +) -> Option { + 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, selected_count: usize, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4c8bed0491..3108186fb7 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -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 Hash for WeakViewHandle { } } +#[derive(Eq, PartialEq, Hash)] pub struct AnyWeakViewHandle { window_id: usize, view_id: usize, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 742d2acb64..942c7e354a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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(option: SearchOption, cx: &mut MutableApp pub struct BufferSearchBar { pub query_editor: ViewHandle, - active_editor: Option>, + active_searchable_item: Option>, active_match_index: Option, - active_editor_subscription: Option, - editors_with_matches: HashMap, Vec>>, + active_searchable_item_subscription: Option, + seachable_items_with_matches: + HashMap, Vec>>, pending_search: Option>, 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, ) -> 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::(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.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::(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) -> 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) { - 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) { 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, - event: &editor::Event, - cx: &mut ViewContext, - ) { + fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { 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) { 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::(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) { 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::(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::( - 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) { - 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(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6566722ce5..8c5723c30b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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::(cx)?; - let query = query_suggestion_for_editor(&editor, cx); + let query = editor.query_suggestion(cx); if query.is_empty() { None } else { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index a975ab0666..954aa0b500 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -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], - cursor: &Anchor, - buffer: &MultiBufferSnapshot, -) -> Option { - 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], - 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, - cx: &mut MutableAppContext, -) -> String { - let display_map = editor - .update(cx, |editor, cx| editor.snapshot(cx)) - .display_snapshot; - let selection = editor.read(cx).selections.newest::(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() - } -} diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs new file mode 100644 index 0000000000..3b252aedb8 --- /dev/null +++ b/crates/workspace/src/searchable.rs @@ -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; + fn clear_highlights(&mut self, cx: &mut ViewContext); + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; + fn select_next_match_in_direction( + &mut self, + index: usize, + direction: Direction, + matches: &Vec>, + cx: &mut ViewContext, + ); + fn select_match_by_index( + &mut self, + index: usize, + matches: &Vec>, + cx: &mut ViewContext, + ); + fn matches( + &mut self, + query: SearchQuery, + cx: &mut ViewContext, + ) -> Task>>; + fn active_match_index( + &mut self, + matches: &Vec>, + cx: &mut ViewContext, + ) -> Option; +} + +pub trait SearchableItemHandle: ItemHandle { + fn downgrade(&self) -> Box; + fn boxed_clone(&self) -> Box; + fn subscribe( + &self, + cx: &mut MutableAppContext, + handler: Box, + ) -> 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>, + cx: &mut MutableAppContext, + ); + fn select_match_by_index( + &self, + index: usize, + matches: &Vec>, + cx: &mut MutableAppContext, + ); + fn matches( + &self, + query: SearchQuery, + cx: &mut MutableAppContext, + ) -> Task>>; + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut MutableAppContext, + ) -> Option; +} + +impl SearchableItemHandle for ViewHandle { + fn downgrade(&self) -> Box { + Box::new(self.downgrade()) + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn subscribe( + &self, + cx: &mut MutableAppContext, + handler: Box, + ) -> 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>, + 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>, + 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>> { + self.update(cx, |this, cx| this.matches(query, cx)) + } + fn active_match_index( + &self, + matches: &Vec>, + cx: &mut MutableAppContext, + ) -> Option { + self.update(cx, |this, cx| this.active_match_index(matches, cx)) + } +} + +impl From> for AnyViewHandle { + fn from(this: Box) -> Self { + this.to_any() + } +} + +impl From<&Box> for AnyViewHandle { + fn from(this: &Box) -> Self { + this.to_any() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() && self.window_id() == other.window_id() + } +} + +impl Eq for Box {} + +pub trait WeakSearchableItemHandle: WeakItemHandle { + fn upgrade(&self, cx: &AppContext) -> Option>; + + fn to_any(self) -> AnyWeakViewHandle; +} + +impl WeakSearchableItemHandle for WeakViewHandle { + fn upgrade(&self, cx: &AppContext) -> Option> { + Some(Box::new(self.upgrade(cx)?)) + } + + fn to_any(self) -> AnyWeakViewHandle { + self.into() + } +} + +impl PartialEq for Box { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() && self.window_id() == other.window_id() + } +} + +impl Eq for Box {} + +impl std::hash::Hash for Box { + fn hash(&self, state: &mut H) { + (self.id(), self.window_id()).hash(state) + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f258589ba7..8f903f2b65 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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) -> Option> { + 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, 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, ) -> gpui::Subscription; + fn as_searchable(&self, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { fn id(&self) -> usize; + fn window_id(&self) -> usize; fn upgrade(&self, cx: &AppContext) -> Option>; } @@ -670,6 +678,10 @@ impl ItemHandle for ViewHandle { self.id() } + fn window_id(&self) -> usize { + self.window_id() + } + fn to_any(&self) -> AnyViewHandle { self.into() } @@ -728,6 +740,10 @@ impl ItemHandle for ViewHandle { ) -> gpui::Subscription { cx.observe_release(self, move |_, cx| callback(cx)) } + + fn as_searchable(&self, cx: &AppContext) -> Option> { + self.read(cx).as_searchable(self) + } } impl From> for AnyViewHandle { @@ -753,6 +769,10 @@ impl WeakItemHandle for WeakViewHandle { self.id() } + fn window_id(&self) -> usize { + self.window_id() + } + fn upgrade(&self, cx: &AppContext) -> Option> { self.upgrade(cx).map(|v| Box::new(v) as Box) }