SearchableItem trait is completed and editor searches appear to be working

K Simmons created

Change summary

crates/editor/src/items.rs          | 228 ++++++++++++++++++++++++++
crates/gpui/src/app.rs              |   9 +
crates/search/src/buffer_search.rs  | 266 +++++++++++++-----------------
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, 576 insertions(+), 251 deletions(-)

Detailed changes

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<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,

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<T> Hash for WeakViewHandle<T> {
     }
 }
 
+#[derive(Eq, PartialEq, Hash)]
 pub struct AnyWeakViewHandle {
     window_id: usize,
     view_id: usize,

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<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();

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::<Editor>(cx)?;
-            let query = query_suggestion_for_editor(&editor, cx);
+            let query = editor.query_suggestion(cx);
             if query.is_empty() {
                 None
             } else {

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<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()
-    }
-}

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<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)
+    }
+}

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<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>)
     }