WIP - Navigate to prev/next search result

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs |  18 ++++-
crates/editor/src/items.rs  |   2 
crates/find/src/find.rs     | 125 +++++++++++++++++++++++++++++++++++---
3 files changed, 127 insertions(+), 18 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -861,7 +861,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
-        let newest_selection = self.newest_selection_internal().unwrap().clone();
+        let newest_selection = self.newest_anchor_selection().unwrap().clone();
 
         let start;
         let end;
@@ -3358,10 +3358,10 @@ impl Editor {
         &self,
         snapshot: &MultiBufferSnapshot,
     ) -> Selection<D> {
-        self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot)
+        self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot)
     }
 
-    pub fn newest_selection_internal(&self) -> Option<&Selection<Anchor>> {
+    pub fn newest_anchor_selection(&self) -> Option<&Selection<Anchor>> {
         self.pending_selection
             .as_ref()
             .map(|s| &s.selection)
@@ -3377,7 +3377,7 @@ impl Editor {
         T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
     {
         let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_cursor_position = self.newest_selection_internal().map(|s| s.head());
+        let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
         selections.sort_unstable_by_key(|s| s.start);
 
         // Merge overlapping selections.
@@ -3511,6 +3511,7 @@ impl Editor {
                 buffer.set_active_selections(&self.selections, cx)
             });
         }
+        cx.emit(Event::SelectionsChanged);
     }
 
     pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
@@ -3751,7 +3752,7 @@ impl Editor {
     }
 
     #[cfg(feature = "test-support")]
-    pub fn highlighted_ranges(
+    pub fn all_highlighted_ranges(
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
@@ -3762,6 +3763,12 @@ impl Editor {
         self.highlighted_ranges_in_range(start..end, &snapshot)
     }
 
+    pub fn highlighted_ranges_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
+        self.highlighted_ranges
+            .get(&TypeId::of::<T>())
+            .map(|(color, ranges)| (*color, ranges.as_slice()))
+    }
+
     pub fn highlighted_ranges_in_range(
         &self,
         search_range: Range<Anchor>,
@@ -4011,6 +4018,7 @@ pub enum Event {
     Dirtied,
     Saved,
     TitleChanged,
+    SelectionsChanged,
     Closed,
 }
 

crates/editor/src/items.rs 🔗

@@ -141,7 +141,7 @@ impl ItemView for Editor {
     }
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(selection) = self.newest_selection_internal() {
+        if let Some(selection) = self.newest_anchor_selection() {
             self.push_to_nav_history(selection.head(), None, cx);
         }
     }

crates/find/src/find.rs 🔗

@@ -9,12 +9,19 @@ use gpui::{
 use postage::watch;
 use regex::RegexBuilder;
 use smol::future::yield_now;
-use std::{ops::Range, sync::Arc};
+use std::{cmp::Ordering, ops::Range, sync::Arc};
 use workspace::{ItemViewHandle, Settings, Toolbar, Workspace};
 
 action!(Deploy);
 action!(Cancel);
 action!(ToggleMode, SearchMode);
+action!(GoToMatch, Direction);
+
+#[derive(Clone, Copy)]
+pub enum Direction {
+    Prev,
+    Next,
+}
 
 #[derive(Clone, Copy)]
 pub enum SearchMode {
@@ -31,12 +38,14 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FindBar::deploy);
     cx.add_action(FindBar::cancel);
     cx.add_action(FindBar::toggle_mode);
+    cx.add_action(FindBar::go_to_match);
 }
 
 struct FindBar {
     settings: watch::Receiver<Settings>,
     query_editor: ViewHandle<Editor>,
     active_editor: Option<ViewHandle<Editor>>,
+    active_match_index: Option<usize>,
     active_editor_subscription: Option<Subscription>,
     highlighted_editors: HashSet<WeakViewHandle<Editor>>,
     pending_search: Option<Task<()>>,
@@ -84,6 +93,12 @@ impl View for FindBar {
                     .with_style(theme.mode_button_group)
                     .boxed(),
             )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_nav_button("<", Direction::Prev, theme, cx))
+                    .with_child(self.render_nav_button(">", Direction::Next, theme, cx))
+                    .boxed(),
+            )
             .contained()
             .with_style(theme.container)
             .boxed()
@@ -138,6 +153,7 @@ impl FindBar {
             query_editor,
             active_editor: None,
             active_editor_subscription: None,
+            active_match_index: None,
             highlighted_editors: Default::default(),
             case_sensitive_mode: false,
             whole_word_mode: false,
@@ -166,7 +182,7 @@ impl FindBar {
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         let is_active = self.is_mode_enabled(mode);
-        MouseEventHandler::new::<Self, _, _, _>(mode as usize, cx, |state, _| {
+        MouseEventHandler::new::<Self, _, _, _>((cx.view_id(), mode as usize), cx, |state, _| {
             let style = match (is_active, state.hovered) {
                 (false, false) => &theme.mode_button,
                 (false, true) => &theme.hovered_mode_button,
@@ -182,6 +198,32 @@ impl FindBar {
         .boxed()
     }
 
+    fn render_nav_button(
+        &self,
+        icon: &str,
+        direction: Direction,
+        theme: &theme::Find,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        MouseEventHandler::new::<Self, _, _, _>(
+            (cx.view_id(), 10 + direction as usize),
+            cx,
+            |state, _| {
+                let style = if state.hovered {
+                    &theme.hovered_mode_button
+                } else {
+                    &theme.mode_button
+                };
+                Label::new(icon.to_string(), style.text.clone())
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            },
+        )
+        .on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
+        .boxed()
+    }
+
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         let settings = workspace.settings();
         workspace.active_pane().update(cx, |pane, cx| {
@@ -217,6 +259,32 @@ impl FindBar {
         cx.notify();
     }
 
+    fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext<Self>) {
+        if let Some(mut index) = self.active_match_index {
+            if let Some(editor) = self.active_editor.as_ref() {
+                editor.update(cx, |editor, cx| {
+                    if let Some((_, ranges)) = editor.highlighted_ranges_for_type::<Self>() {
+                        match direction {
+                            Direction::Prev => {
+                                if index == 0 {
+                                    index = ranges.len() - 1;
+                                } else {
+                                    index -= 1;
+                                }
+                            }
+                            Direction::Next => {
+                                index += 1;
+                                if index >= ranges.len() {
+                                    index = 0;
+                                }
+                            }
+                        }
+                    }
+                });
+            }
+        }
+    }
+
     fn on_query_editor_event(
         &mut self,
         _: ViewHandle<Editor>,
@@ -244,12 +312,14 @@ impl FindBar {
 
     fn on_active_editor_event(
         &mut self,
-        _: ViewHandle<Editor>,
+        editor: ViewHandle<Editor>,
         event: &editor::Event,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
             editor::Event::Edited => self.update_matches(cx),
+            editor::Event::SelectionsChanged => self.update_match_index(cx),
+
             _ => {}
         }
     }
@@ -279,15 +349,16 @@ impl FindBar {
                         Ok(ranges) => {
                             if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
                                 this.update(&mut cx, |this, cx| {
-                                    let theme = &this.settings.borrow().theme.find;
                                     this.highlighted_editors.insert(editor.downgrade());
                                     editor.update(cx, |editor, cx| {
+                                        let theme = &this.settings.borrow().theme.find;
                                         editor.highlight_ranges::<Self>(
                                             ranges,
                                             theme.match_background,
                                             cx,
                                         )
                                     });
+                                    this.update_match_index(cx);
                                 });
                             }
                         }
@@ -302,6 +373,29 @@ impl FindBar {
             }
         }
     }
+
+    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+        self.active_match_index = self.active_match_index(cx);
+    }
+
+    fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
+        let editor = self.active_editor.as_ref()?;
+        let editor = editor.read(cx);
+        let position = editor.newest_anchor_selection()?.head();
+        let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
+        let buffer = editor.buffer().read(cx).read(cx);
+        match ranges.binary_search_by(|probe| {
+            if probe.end.cmp(&position, &*buffer).unwrap().is_lt() {
+                Ordering::Less
+            } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Equal
+            }
+        }) {
+            Ok(i) | Err(i) => Some(i),
+        }
+    }
 }
 
 const YIELD_INTERVAL: usize = 20000;
@@ -445,14 +539,15 @@ mod tests {
             find_bar
         });
 
-        // default: case-insensitive substring search.
+        // Search for a string that appears with different casing.
+        // By default, search is case-insensitive.
         find_bar.update(&mut cx, |find_bar, cx| {
             find_bar.set_query("us", cx);
         });
         editor.next_notification(&cx).await;
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
-                editor.highlighted_ranges(cx),
+                editor.all_highlighted_ranges(cx),
                 &[
                     (
                         DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
@@ -466,28 +561,30 @@ mod tests {
             );
         });
 
-        // switch to case sensitive search
+        // Switch to a case sensitive search.
         find_bar.update(&mut cx, |find_bar, cx| {
             find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx);
         });
         editor.next_notification(&cx).await;
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
-                editor.highlighted_ranges(cx),
+                editor.all_highlighted_ranges(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
-                ),]
+                )]
             );
         });
 
+        // Search for a string that appears both as a whole word and
+        // within other words. By default, all results are found.
         find_bar.update(&mut cx, |find_bar, cx| {
             find_bar.set_query("or", cx);
         });
         editor.next_notification(&cx).await;
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
-                editor.highlighted_ranges(cx),
+                editor.all_highlighted_ranges(cx),
                 &[
                     (
                         DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
@@ -521,14 +618,14 @@ mod tests {
             );
         });
 
-        // switch to whole word search
+        // Switch to a whole word search.
         find_bar.update(&mut cx, |find_bar, cx| {
             find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx);
         });
         editor.next_notification(&cx).await;
         editor.update(&mut cx, |editor, cx| {
             assert_eq!(
-                editor.highlighted_ranges(cx),
+                editor.all_highlighted_ranges(cx),
                 &[
                     (
                         DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
@@ -545,5 +642,9 @@ mod tests {
                 ]
             );
         });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+        });
     }
 }