search.rs

  1use gpui::{impl_actions, AppContext, ViewContext};
  2use search::{BufferSearchBar, SearchOptions};
  3use serde_derive::Deserialize;
  4use workspace::{searchable::Direction, Workspace};
  5
  6use crate::Vim;
  7
  8#[derive(Clone, Deserialize, PartialEq)]
  9#[serde(rename_all = "camelCase")]
 10pub(crate) struct MoveToNext {
 11    #[serde(default)]
 12    partial_word: bool,
 13}
 14
 15#[derive(Clone, Deserialize, PartialEq)]
 16#[serde(rename_all = "camelCase")]
 17pub(crate) struct MoveToPrev {
 18    #[serde(default)]
 19    partial_word: bool,
 20}
 21
 22#[derive(Clone, Deserialize, PartialEq)]
 23pub(crate) struct Search {
 24    #[serde(default)]
 25    backwards: bool,
 26}
 27
 28impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
 29
 30pub(crate) fn init(cx: &mut AppContext) {
 31    cx.add_action(move_to_next);
 32    cx.add_action(move_to_prev);
 33    cx.add_action(search);
 34}
 35
 36fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
 37    move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
 38}
 39
 40fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
 41    move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
 42}
 43
 44fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
 45    let pane = workspace.active_pane().clone();
 46    pane.update(cx, |pane, cx| {
 47        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 48            search_bar.update(cx, |search_bar, cx| {
 49                let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
 50                let direction = if action.backwards {
 51                    Direction::Prev
 52                } else {
 53                    Direction::Next
 54                };
 55                search_bar.select_match(direction, cx);
 56                search_bar.show_with_options(true, false, options, cx);
 57            })
 58        }
 59    })
 60}
 61
 62pub fn move_to_internal(
 63    workspace: &mut Workspace,
 64    direction: Direction,
 65    whole_word: bool,
 66    cx: &mut ViewContext<Workspace>,
 67) {
 68    Vim::update(cx, |vim, cx| {
 69        let pane = workspace.active_pane().clone();
 70        pane.update(cx, |pane, cx| {
 71            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 72                search_bar.update(cx, |search_bar, cx| {
 73                    let mut options = SearchOptions::CASE_SENSITIVE;
 74                    options.set(SearchOptions::WHOLE_WORD, whole_word);
 75                    search_bar.select_word_under_cursor(direction, options, cx);
 76                });
 77            }
 78        });
 79        vim.clear_operator(cx);
 80    });
 81}
 82
 83#[cfg(test)]
 84mod test {
 85    use std::sync::Arc;
 86
 87    use editor::DisplayPoint;
 88    use search::BufferSearchBar;
 89
 90    use crate::{state::Mode, test::VimTestContext};
 91
 92    #[gpui::test]
 93    async fn test_move_to_next(
 94        cx: &mut gpui::TestAppContext,
 95        deterministic: Arc<gpui::executor::Deterministic>,
 96    ) {
 97        let mut cx = VimTestContext::new(cx, true).await;
 98        let search_bar = cx.workspace(|workspace, cx| {
 99            workspace
100                .active_pane()
101                .read(cx)
102                .toolbar()
103                .read(cx)
104                .item_of_type::<BufferSearchBar>()
105                .expect("Buffer search bar should be deployed")
106        });
107        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
108
109        cx.simulate_keystrokes(["*"]);
110        deterministic.run_until_parked();
111        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
112
113        cx.simulate_keystrokes(["*"]);
114        deterministic.run_until_parked();
115        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
116
117        cx.simulate_keystrokes(["#"]);
118        deterministic.run_until_parked();
119        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
120
121        cx.simulate_keystrokes(["#"]);
122        deterministic.run_until_parked();
123        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
124
125        cx.simulate_keystrokes(["g", "*"]);
126        deterministic.run_until_parked();
127        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
128
129        cx.simulate_keystrokes(["n"]);
130        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
131
132        cx.simulate_keystrokes(["g", "#"]);
133        deterministic.run_until_parked();
134        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
135    }
136
137    #[gpui::test]
138    async fn test_search(cx: &mut gpui::TestAppContext) {
139        let mut cx = VimTestContext::new(cx, true).await;
140
141        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
142        cx.simulate_keystrokes(["/", "c", "c"]);
143
144        let search_bar = cx.workspace(|workspace, cx| {
145            workspace
146                .active_pane()
147                .read(cx)
148                .toolbar()
149                .read(cx)
150                .item_of_type::<BufferSearchBar>()
151                .expect("Buffer search bar should be deployed")
152        });
153
154        search_bar.read_with(cx.cx, |bar, cx| {
155            assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
156        });
157
158        // wait for the query editor change event to fire.
159        search_bar.next_notification(&cx).await;
160
161        cx.update_editor(|editor, cx| {
162            let highlights = editor.all_background_highlights(cx);
163            assert_eq!(3, highlights.len());
164            assert_eq!(
165                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
166                highlights[0].0
167            )
168        });
169
170        cx.simulate_keystrokes(["enter"]);
171
172        // n to go to next/N to go to previous
173        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
174        cx.simulate_keystrokes(["n"]);
175        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
176        cx.simulate_keystrokes(["shift-n"]);
177
178        // ?<enter> to go to previous
179        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
180        cx.simulate_keystrokes(["?", "enter"]);
181        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
182        cx.simulate_keystrokes(["?", "enter"]);
183
184        // /<enter> to go to next
185        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
186        cx.simulate_keystrokes(["/", "enter"]);
187        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
188
189        // ?{search}<enter> to search backwards
190        cx.simulate_keystrokes(["?", "b", "enter"]);
191
192        // wait for the query editor change event to fire.
193        search_bar.next_notification(&cx).await;
194
195        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
196    }
197}