Finished terminal search

Mikayla Maki created

Change summary

crates/editor/src/items.rs                     |  91 +++++-------
crates/language/src/syntax_map.rs              |   1 
crates/search/src/buffer_search.rs             | 100 +++++++++----
crates/search/src/project_search.rs            |  22 +-
crates/terminal/src/terminal.rs                | 143 +++++++------------
crates/terminal/src/terminal_container_view.rs |  59 +++++---
crates/terminal/src/terminal_element.rs        |   6 
crates/workspace/src/searchable.rs             |  93 ++++++++----
8 files changed, 270 insertions(+), 245 deletions(-)

Detailed changes

crates/editor/src/items.rs 🔗

@@ -553,37 +553,53 @@ impl SearchableItem for Editor {
         }
     }
 
-    fn activate_next_match(
+    fn activate_match(
         &mut self,
         index: usize,
-        direction: Direction,
         matches: Vec<Range<Anchor>>,
         cx: &mut ViewContext<Self>,
     ) {
-        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.unfold_ranges([matches[index].clone()], false, cx);
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            s.select_ranges([range_to_select])
+            s.select_ranges([matches[index].clone()])
         });
     }
 
-    fn activate_match_at_index(
+    fn match_index_for_direction(
         &mut self,
-        index: usize,
-        matches: Vec<Range<Anchor>>,
+        matches: &Vec<Range<Anchor>>,
+        mut current_index: usize,
+        direction: Direction,
         cx: &mut ViewContext<Self>,
-    ) {
-        self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            s.select_ranges([matches[index].clone()])
-        });
+    ) -> usize {
+        let buffer = self.buffer().read(cx).snapshot(cx);
+        let cursor = self.selections.newest_anchor().head();
+        if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
+            if direction == Direction::Prev {
+                if current_index == 0 {
+                    current_index = matches.len() - 1;
+                } else {
+                    current_index -= 1;
+                }
+            }
+        } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
+            if direction == Direction::Next {
+                current_index = 0;
+            }
+        } else if direction == Direction::Prev {
+            if current_index == 0 {
+                current_index = matches.len() - 1;
+            } else {
+                current_index -= 1;
+            }
+        } else if direction == Direction::Next {
+            if current_index == matches.len() - 1 {
+                current_index = 0
+            } else {
+                current_index += 1;
+            }
+        };
+        current_index
     }
 
     fn find_matches(
@@ -637,41 +653,6 @@ impl SearchableItem for Editor {
     }
 }
 
-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,

crates/language/src/syntax_map.rs 🔗

@@ -233,7 +233,6 @@ impl SyntaxSnapshot {
             };
             let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text);
 
-
             // Ignore edits that end before the start of this layer, and don't consider them
             // for any subsequent layers at this same depth.
             loop {

crates/search/src/buffer_search.rs 🔗

@@ -95,6 +95,12 @@ impl View for BufferSearchBar {
         } else {
             theme.search.editor.input.container
         };
+        let supported_options = self
+            .active_searchable_item
+            .as_ref()
+            .map(|active_searchable_item| active_searchable_item.supported_options())
+            .unwrap_or_default();
+
         Flex::row()
             .with_child(
                 Flex::row()
@@ -143,9 +149,24 @@ impl View for BufferSearchBar {
             )
             .with_child(
                 Flex::row()
-                    .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
-                    .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
-                    .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
+                    .with_children(self.render_search_option(
+                        supported_options.case,
+                        "Case",
+                        SearchOption::CaseSensitive,
+                        cx,
+                    ))
+                    .with_children(self.render_search_option(
+                        supported_options.word,
+                        "Word",
+                        SearchOption::WholeWord,
+                        cx,
+                    ))
+                    .with_children(self.render_search_option(
+                        supported_options.regex,
+                        "Regex",
+                        SearchOption::Regex,
+                        cx,
+                    ))
                     .contained()
                     .with_style(theme.search.option_button_group)
                     .aligned()
@@ -234,7 +255,7 @@ impl BufferSearchBar {
             if let Some(searchable_item) =
                 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
             {
-                searchable_item.clear_highlights(cx);
+                searchable_item.clear_matches(cx);
             }
         }
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
@@ -283,36 +304,43 @@ impl BufferSearchBar {
 
     fn render_search_option(
         &self,
+        option_supported: bool,
         icon: &str,
         option: SearchOption,
         cx: &mut RenderContext<Self>,
-    ) -> ElementBox {
+    ) -> Option<ElementBox> {
+        if !option_supported {
+            return None;
+        }
+
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let is_active = self.is_search_option_enabled(option);
-        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
-            let style = &cx
-                .global::<Settings>()
-                .theme
-                .search
-                .option_button
-                .style_for(state, is_active);
-            Label::new(icon.to_string(), style.text.clone())
-                .contained()
-                .with_style(style.container)
-                .boxed()
-        })
-        .on_click(MouseButton::Left, move |_, cx| {
-            cx.dispatch_any_action(option.to_toggle_action())
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<Self, _>(
-            option as usize,
-            format!("Toggle {}", option.label()),
-            Some(option.to_toggle_action()),
-            tooltip_style,
-            cx,
+        Some(
+            MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+                let style = &cx
+                    .global::<Settings>()
+                    .theme
+                    .search
+                    .option_button
+                    .style_for(state, is_active);
+                Label::new(icon.to_string(), style.text.clone())
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            })
+            .on_click(MouseButton::Left, move |_, cx| {
+                cx.dispatch_any_action(option.to_toggle_action())
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .with_tooltip::<Self, _>(
+                option as usize,
+                format!("Toggle {}", option.label()),
+                Some(option.to_toggle_action()),
+                tooltip_style,
+                cx,
+            )
+            .boxed(),
         )
-        .boxed()
     }
 
     fn render_nav_button(
@@ -422,8 +450,10 @@ impl BufferSearchBar {
                     .seachable_items_with_matches
                     .get(&searchable_item.downgrade())
                 {
-                    searchable_item.select_next_match_in_direction(index, direction, matches, cx);
-                    searchable_item.highlight_matches(matches, cx);
+                    let new_match_index =
+                        searchable_item.match_index_for_direction(matches, index, direction, cx);
+                    searchable_item.update_matches(matches, cx);
+                    searchable_item.activate_match(new_match_index, matches, cx);
                 }
             }
         }
@@ -479,7 +509,7 @@ impl BufferSearchBar {
                 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
                     active_item_matches = Some((searchable_item.downgrade(), matches));
                 } else {
-                    searchable_item.clear_highlights(cx);
+                    searchable_item.clear_matches(cx);
                 }
             }
         }
@@ -494,7 +524,7 @@ impl BufferSearchBar {
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
-                active_searchable_item.clear_highlights(cx);
+                active_searchable_item.clear_matches(cx);
             } else {
                 let query = if self.regex {
                     match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
@@ -509,7 +539,7 @@ impl BufferSearchBar {
                     SearchQuery::text(query, self.whole_word, self.case_sensitive)
                 };
 
-                let matches = active_searchable_item.matches(query, cx);
+                let matches = active_searchable_item.find_matches(query, cx);
 
                 let active_searchable_item = active_searchable_item.downgrade();
                 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
@@ -529,13 +559,13 @@ impl BufferSearchBar {
                                         .seachable_items_with_matches
                                         .get(&active_searchable_item.downgrade())
                                         .unwrap();
+                                    active_searchable_item.update_matches(matches, cx);
                                     if select_closest_match {
                                         if let Some(match_ix) = this.active_match_index {
                                             active_searchable_item
-                                                .select_match_by_index(match_ix, matches, cx);
+                                                .activate_match(match_ix, matches, cx);
                                         }
                                     }
-                                    active_searchable_item.highlight_matches(matches, cx);
                                 }
                                 cx.notify();
                             }

crates/search/src/project_search.rs 🔗

@@ -4,8 +4,8 @@ use crate::{
 };
 use collections::HashMap;
 use editor::{
-    items::{active_match_index, match_index_for_direction},
-    Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
+    items::active_match_index, Anchor, Autoscroll, Editor, MultiBuffer, SelectAll,
+    MAX_TAB_TITLE_LEN,
 };
 use gpui::{
     actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,
@@ -23,7 +23,7 @@ use std::{
 };
 use util::ResultExt as _;
 use workspace::{
-    searchable::{Direction, SearchableItemHandle},
+    searchable::{Direction, SearchableItem, SearchableItemHandle},
     Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
 
@@ -486,16 +486,12 @@ impl ProjectSearchView {
 
     fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
-            let model = self.model.read(cx);
-            let results_editor = self.results_editor.read(cx);
-            let new_index = match_index_for_direction(
-                &model.match_ranges,
-                &results_editor.selections.newest_anchor().head(),
-                index,
-                direction,
-                &results_editor.buffer().read(cx).snapshot(cx),
-            );
-            let range_to_select = model.match_ranges[new_index].clone();
+            let match_ranges = self.model.read(cx).match_ranges.clone();
+            let new_index = self.results_editor.update(cx, |editor, cx| {
+                editor.match_index_for_direction(&match_ranges, index, direction, cx)
+            });
+
+            let range_to_select = match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
                 editor.unfold_ranges([range_to_select.clone()], false, cx);
                 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {

crates/terminal/src/terminal.rs 🔗

@@ -10,7 +10,7 @@ use alacritty_terminal::{
     event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
     event_loop::{EventLoop, Msg, Notifier},
     grid::{Dimensions, Scroll as AlacScroll},
-    index::{Column, Direction, Line, Point},
+    index::{Column, Direction as AlacDirection, Line, Point},
     selection::{Selection, SelectionRange, SelectionType},
     sync::FairMutex,
     term::{
@@ -84,6 +84,7 @@ pub enum Event {
     Bell,
     Wakeup,
     BlinkChanged,
+    SelectionsChanged,
 }
 
 #[derive(Clone)]
@@ -93,7 +94,8 @@ enum InternalEvent {
     Clear,
     // FocusNextMatch,
     Scroll(AlacScroll),
-    SetSelection(Option<Selection>),
+    ScrollToPoint(Point),
+    SetSelection(Option<(Selection, Point)>),
     UpdateSelection(Vector2F),
     Copy,
 }
@@ -384,6 +386,7 @@ impl TerminalBuilder {
             matches: Vec::new(),
             last_synced: Instant::now(),
             sync_task: None,
+            selection_head: None,
         };
 
         Ok(TerminalBuilder {
@@ -494,12 +497,13 @@ pub struct Terminal {
     events: VecDeque<InternalEvent>,
     default_title: String,
     title: String,
-    last_mouse: Option<(Point, Direction)>,
+    last_mouse: Option<(Point, AlacDirection)>,
     pub matches: Vec<RangeInclusive<Point>>,
     cur_size: TerminalSize,
     last_content: TerminalContent,
     last_synced: Instant,
     sync_task: Option<Task<()>>,
+    selection_head: Option<Point>,
 }
 
 impl Terminal {
@@ -576,33 +580,14 @@ impl Terminal {
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
             }
-            // InternalEvent::FocusNextMatch => {
-            //     if let Some((Some(searcher), _origin)) = &self.searcher {
-            //         match term.search_next(
-            //             searcher,
-            //             Point {
-            //                 line: Line(0),
-            //                 column: Column(0),
-            //             },
-            //             SEARCH_FORWARD,
-            //             Direction::Left,
-            //             None,
-            //         ) {
-            //             Some(regex_match) => {
-            //                 term.scroll_to_point(*regex_match.start());
-
-            //                 //Focus is done with selections in zed
-            //                 let focus = make_selection(*regex_match.start(), *regex_match.end());
-            //                 term.selection = Some(focus);
-            //             }
-            //             None => {
-            //                 //Clear focused match
-            //                 term.selection = None;
-            //             }
-            //         }
-            //     }
-            // }
-            InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
+            InternalEvent::SetSelection(selection) => {
+                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
+
+                if let Some((_, head)) = selection {
+                    self.selection_head = Some(*head);
+                }
+                cx.emit(Event::SelectionsChanged)
+            }
             InternalEvent::UpdateSelection(position) => {
                 if let Some(mut selection) = term.selection.take() {
                     let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
@@ -610,6 +595,9 @@ impl Terminal {
 
                     selection.update(point, side);
                     term.selection = Some(selection);
+
+                    self.selection_head = Some(point);
+                    cx.emit(Event::SelectionsChanged)
                 }
             }
 
@@ -618,6 +606,7 @@ impl Terminal {
                     cx.write_to_clipboard(ClipboardItem::new(txt))
                 }
             }
+            InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
         }
     }
 
@@ -625,53 +614,24 @@ impl Terminal {
         &self.last_content
     }
 
-    fn begin_select(&mut self, sel: Selection) {
-        self.events
-            .push_back(InternalEvent::SetSelection(Some(sel)));
-    }
+    //To test:
+    //- Activate match on terminal (scrolling and selection)
+    //- Editor search snapping behavior
 
-    fn continue_selection(&mut self, location: Vector2F) {
-        self.events
-            .push_back(InternalEvent::UpdateSelection(location))
-    }
+    pub fn activate_match(&mut self, index: usize) {
+        if let Some(search_match) = self.matches.get(index).cloned() {
+            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
 
-    fn end_select(&mut self) {
-        self.events.push_back(InternalEvent::SetSelection(None));
+            self.events
+                .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
+        }
     }
 
-    fn scroll(&mut self, scroll: AlacScroll) {
-        self.events.push_back(InternalEvent::Scroll(scroll));
+    fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
+        self.events
+            .push_back(InternalEvent::SetSelection(selection));
     }
 
-    // fn focus_next_match(&mut self) {
-    //     self.events.push_back(InternalEvent::FocusNextMatch);
-    // }
-
-    // pub fn search(&mut self, search: &str) {
-    //     let new_searcher = RegexSearch::new(search).ok();
-    //     self.searcher = match (new_searcher, &self.searcher) {
-    //         //Nothing to do :(
-    //         (None, None) => None,
-    //         //No existing search, start a new one
-    //         (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())),
-    //         //Existing search, carry over origin
-    //         (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)),
-    //     };
-
-    //     if let Some((Some(_), _)) = self.searcher {
-    //         self.focus_next_match();
-    //     }
-    // }
-
-    // fn viewport_origin(&mut self) -> Point {
-    //     let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1;
-    //     Point::new(viewport_top, alacritty_terminal::index::Column(0))
-    // }
-
-    // pub fn end_search(&mut self) {
-    //     self.searcher = None;
-    // }
-
     pub fn copy(&mut self) {
         self.events.push_back(InternalEvent::Copy);
     }
@@ -691,8 +651,10 @@ impl Terminal {
     }
 
     pub fn input(&mut self, input: String) {
-        self.scroll(AlacScroll::Bottom);
-        self.end_select();
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
         self.write_to_pty(input);
     }
 
@@ -790,7 +752,7 @@ impl Terminal {
         }
     }
 
-    pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
+    pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
         match self.last_mouse {
             Some((old_point, old_side)) => {
                 if old_point == point && old_side == side {
@@ -830,7 +792,8 @@ impl Terminal {
         if !self.mouse_mode(e.shift) {
             // Alacritty has the same ordering, of first updating the selection
             // then scrolling 15ms later
-            self.continue_selection(position);
+            self.events
+                .push_back(InternalEvent::UpdateSelection(position));
 
             // Doesn't make sense to scroll the alt screen
             if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
@@ -840,8 +803,11 @@ impl Terminal {
                 };
 
                 let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
-                self.scroll(AlacScroll::Delta(scroll_lines));
-                self.continue_selection(position)
+
+                self.events
+                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
+                self.events
+                    .push_back(InternalEvent::UpdateSelection(position))
             }
         }
     }
@@ -870,7 +836,10 @@ impl Terminal {
                 self.pty_tx.notify(bytes);
             }
         } else if e.button == MouseButton::Left {
-            self.begin_select(Selection::new(SelectionType::Simple, point, side));
+            self.events.push_back(InternalEvent::SetSelection(Some((
+                Selection::new(SelectionType::Simple, point, side),
+                point,
+            ))));
         }
     }
 
@@ -893,7 +862,8 @@ impl Terminal {
                 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
 
             if let Some(sel) = selection {
-                self.begin_select(sel);
+                self.events
+                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
             }
         }
     }
@@ -951,7 +921,8 @@ impl Terminal {
                 ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
             if scroll_lines != 0 {
                 let scroll = AlacScroll::Delta(scroll_lines);
-                self.scroll(scroll);
+
+                self.events.push_back(InternalEvent::Scroll(scroll));
             }
         }
     }
@@ -994,11 +965,11 @@ impl Entity for Terminal {
     type Event = Event;
 }
 
-// fn make_selection(from: Point, to: Point) -> Selection {
-//     let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left);
-//     focus.update(to, Direction::Right);
-//     focus
-// }
+fn make_selection(range: &RangeInclusive<Point>) -> Selection {
+    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
+    selection.update(*range.end(), AlacDirection::Right);
+    selection
+}
 
 /// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
 /// Iterate over all visible regex matches.
@@ -1013,7 +984,7 @@ fn make_search_matches<'a, T>(
     start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
     end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
 
-    RegexIter::new(start, end, Direction::Right, term, regex)
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
         .skip_while(move |rm| rm.end().line < viewport_start)
         .take_while(move |rm| rm.start().line <= viewport_end)
 }

crates/terminal/src/terminal_container_view.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
     View, ViewContext, ViewHandle,
 };
-use workspace::searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle};
+use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
 use workspace::{Item, Workspace};
 
 use crate::TerminalSize;
@@ -340,11 +340,19 @@ impl Item for TerminalContainer {
 impl SearchableItem for TerminalContainer {
     type Match = RangeInclusive<Point>;
 
+    fn supported_options() -> SearchOptions {
+        SearchOptions {
+            case: false,
+            word: false,
+            regex: false,
+        }
+    }
+
     /// Convert events raised by this item into search-relevant events (if applicable)
     fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
         match event {
             Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
-            //TODO selection changed
+            Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
             _ => None,
         }
     }
@@ -380,25 +388,13 @@ impl SearchableItem for TerminalContainer {
         }
     }
 
-    /// Given an index, a set of matches for this index, and a direction,
-    /// get the next match (clicking the arrow)
-    fn activate_next_match(
-        &mut self,
-        _index: usize,
-        _direction: Direction,
-        _matches: Vec<Self::Match>,
-        _cx: &mut ViewContext<Self>,
-    ) {
-        // TODO:
-    }
-
     /// Focus match at given index into the Vec of matches
-    fn activate_match_at_index(
-        &mut self,
-        _index: usize,
-        _matches: Vec<Self::Match>,
-        _cx: &mut ViewContext<Self>,
-    ) {
+    fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal.update(cx, |term, _| term.activate_match(index));
+            cx.notify();
+        }
     }
 
     /// Get all of the matches for this query, should be done on the background
@@ -419,10 +415,27 @@ impl SearchableItem for TerminalContainer {
     fn active_match_index(
         &mut self,
         matches: Vec<Self::Match>,
-        _cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> Option<usize> {
-        if matches.len() > 0 {
-            Some(0)
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head {
+                // If selection head is contained in a match. Return that match
+                for (ix, search_match) in matches.iter().enumerate() {
+                    if search_match.contains(&selection_head) {
+                        return Some(ix);
+                    }
+
+                    // If not contained, return the next match after the selection head
+                    if search_match.start() > &selection_head {
+                        return Some(ix);
+                    }
+                }
+
+                // If no selection after selection head, return the last match
+                return Some(matches.len() - 1);
+            } else {
+                Some(0)
+            }
         } else {
             None
         }

crates/terminal/src/terminal_element.rs 🔗

@@ -579,12 +579,12 @@ impl Element for TerminalElement {
 
         // searches, highlights to a single range representations
         let mut relative_highlighted_ranges = Vec::new();
-        if let Some(selection) = selection {
-            relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
-        }
         for search_match in search_matches {
             relative_highlighted_ranges.push((search_match, match_color))
         }
+        if let Some(selection) = selection {
+            relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
+        }
 
         // then have that representation be converted to the appropriate highlight data structure
 

crates/workspace/src/searchable.rs 🔗

@@ -14,32 +14,64 @@ pub enum SearchEvent {
     ActiveMatchChanged,
 }
 
-#[derive(Clone, Copy, PartialEq, Eq)]
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum Direction {
     Prev,
     Next,
 }
 
+#[derive(Clone, Copy, Debug, Default)]
+pub struct SearchOptions {
+    pub case: bool,
+    pub word: bool,
+    pub regex: bool,
+}
+
 pub trait SearchableItem: Item {
     type Match: Any + Sync + Send + Clone;
 
+    fn supported_options() -> SearchOptions {
+        SearchOptions {
+            case: true,
+            word: true,
+            regex: true,
+        }
+    }
     fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
     fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
     fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
     fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
-    fn activate_next_match(
+    fn activate_match(
         &mut self,
         index: usize,
-        direction: Direction,
         matches: Vec<Self::Match>,
         cx: &mut ViewContext<Self>,
     );
-    fn activate_match_at_index(
+    fn match_index_for_direction(
         &mut self,
-        index: usize,
-        matches: Vec<Self::Match>,
-        cx: &mut ViewContext<Self>,
-    );
+        matches: &Vec<Self::Match>,
+        mut current_index: usize,
+        direction: Direction,
+        _: &mut ViewContext<Self>,
+    ) -> usize {
+        match direction {
+            Direction::Prev => {
+                if current_index == 0 {
+                    matches.len() - 1
+                } else {
+                    current_index - 1
+                }
+            }
+            Direction::Next => {
+                current_index += 1;
+                if current_index == matches.len() {
+                    0
+                } else {
+                    current_index
+                }
+            }
+        }
+    }
     fn find_matches(
         &mut self,
         query: SearchQuery,
@@ -55,28 +87,29 @@ pub trait SearchableItem: Item {
 pub trait SearchableItemHandle: ItemHandle {
     fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
     fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
+    fn supported_options(&self) -> SearchOptions;
     fn subscribe(
         &self,
         cx: &mut MutableAppContext,
         handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
     ) -> Subscription;
-    fn clear_highlights(&self, cx: &mut MutableAppContext);
-    fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
+    fn clear_matches(&self, cx: &mut MutableAppContext);
+    fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
     fn query_suggestion(&self, cx: &mut MutableAppContext) -> String;
-    fn select_next_match_in_direction(
+    fn activate_match(
         &self,
         index: usize,
-        direction: Direction,
         matches: &Vec<Box<dyn Any + Send>>,
         cx: &mut MutableAppContext,
     );
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &self,
-        index: usize,
         matches: &Vec<Box<dyn Any + Send>>,
+        current_index: usize,
+        direction: Direction,
         cx: &mut MutableAppContext,
-    );
-    fn matches(
+    ) -> usize;
+    fn find_matches(
         &self,
         query: SearchQuery,
         cx: &mut MutableAppContext,
@@ -97,6 +130,10 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
+    fn supported_options(&self) -> SearchOptions {
+        T::supported_options()
+    }
+
     fn subscribe(
         &self,
         cx: &mut MutableAppContext,
@@ -109,40 +146,38 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         })
     }
 
-    fn clear_highlights(&self, cx: &mut MutableAppContext) {
+    fn clear_matches(&self, cx: &mut MutableAppContext) {
         self.update(cx, |this, cx| this.clear_matches(cx));
     }
-    fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
+    fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| this.update_matches(matches, cx));
     }
     fn query_suggestion(&self, cx: &mut MutableAppContext) -> String {
         self.update(cx, |this, cx| this.query_suggestion(cx))
     }
-    fn select_next_match_in_direction(
+    fn activate_match(
         &self,
         index: usize,
-        direction: Direction,
         matches: &Vec<Box<dyn Any + Send>>,
         cx: &mut MutableAppContext,
     ) {
         let matches = downcast_matches(matches);
-        self.update(cx, |this, cx| {
-            this.activate_next_match(index, direction, matches, cx)
-        });
+        self.update(cx, |this, cx| this.activate_match(index, matches, cx));
     }
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &self,
-        index: usize,
         matches: &Vec<Box<dyn Any + Send>>,
+        current_index: usize,
+        direction: Direction,
         cx: &mut MutableAppContext,
-    ) {
+    ) -> usize {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| {
-            this.activate_match_at_index(index, matches, cx)
-        });
+            this.match_index_for_direction(&matches, current_index, direction, cx)
+        })
     }
-    fn matches(
+    fn find_matches(
         &self,
         query: SearchQuery,
         cx: &mut MutableAppContext,