search.rs

  1use gpui::{actions, impl_actions, AppContext, ViewContext};
  2use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
  3use serde_derive::Deserialize;
  4use workspace::{searchable::Direction, Pane, Workspace};
  5
  6use crate::{state::SearchState, 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]);
 29actions!(vim, [SearchSubmit]);
 30
 31pub(crate) fn init(cx: &mut AppContext) {
 32    cx.add_action(move_to_next);
 33    cx.add_action(move_to_prev);
 34    cx.add_action(search);
 35    cx.add_action(search_submit);
 36    cx.add_action(search_deploy);
 37}
 38
 39fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
 40    move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
 41}
 42
 43fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
 44    move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
 45}
 46
 47fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
 48    let pane = workspace.active_pane().clone();
 49    let direction = if action.backwards {
 50        Direction::Prev
 51    } else {
 52        Direction::Next
 53    };
 54    Vim::update(cx, |vim, cx| {
 55        let count = vim.pop_number_operator(cx).unwrap_or(1);
 56        pane.update(cx, |pane, cx| {
 57            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 58                search_bar.update(cx, |search_bar, cx| {
 59                    if !search_bar.show(cx) {
 60                        return;
 61                    }
 62                    let query = search_bar.query(cx);
 63
 64                    search_bar.select_query(cx);
 65                    cx.focus_self();
 66
 67                    if query.is_empty() {
 68                        search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
 69                        search_bar.activate_search_mode(SearchMode::Regex, cx);
 70                    }
 71                    vim.workspace_state.search = SearchState {
 72                        direction,
 73                        count,
 74                        initial_query: query.clone(),
 75                    };
 76                });
 77            }
 78        })
 79    })
 80}
 81
 82// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
 83fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
 84    Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
 85    cx.propagate_action();
 86}
 87
 88fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
 89    Vim::update(cx, |vim, cx| {
 90        let pane = workspace.active_pane().clone();
 91        pane.update(cx, |pane, cx| {
 92            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 93                search_bar.update(cx, |search_bar, cx| {
 94                    let state = &mut vim.workspace_state.search;
 95                    let mut count = state.count;
 96                    let direction = state.direction;
 97
 98                    // in the case that the query has changed, the search bar
 99                    // will have selected the next match already.
100                    if (search_bar.query(cx) != state.initial_query)
101                        && state.direction == Direction::Next
102                    {
103                        count = count.saturating_sub(1)
104                    }
105                    state.count = 1;
106                    search_bar.select_match(direction, count, cx);
107                    search_bar.focus_editor(&Default::default(), cx);
108                });
109            }
110        });
111    })
112}
113
114pub fn move_to_internal(
115    workspace: &mut Workspace,
116    direction: Direction,
117    whole_word: bool,
118    cx: &mut ViewContext<Workspace>,
119) {
120    Vim::update(cx, |vim, cx| {
121        let pane = workspace.active_pane().clone();
122        let count = vim.pop_number_operator(cx).unwrap_or(1);
123        pane.update(cx, |pane, cx| {
124            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
125                let search = search_bar.update(cx, |search_bar, cx| {
126                    let mut options = SearchOptions::CASE_SENSITIVE;
127                    options.set(SearchOptions::WHOLE_WORD, whole_word);
128                    if search_bar.show(cx) {
129                        search_bar
130                            .query_suggestion(cx)
131                            .map(|query| search_bar.search(&query, Some(options), cx))
132                    } else {
133                        None
134                    }
135                });
136
137                if let Some(search) = search {
138                    let search_bar = search_bar.downgrade();
139                    cx.spawn(|_, mut cx| async move {
140                        search.await?;
141                        search_bar.update(&mut cx, |search_bar, cx| {
142                            search_bar.select_match(direction, count, cx)
143                        })?;
144                        anyhow::Ok(())
145                    })
146                    .detach_and_log_err(cx);
147                }
148            }
149        });
150        vim.clear_operator(cx);
151    });
152}
153
154#[cfg(test)]
155mod test {
156    use std::sync::Arc;
157
158    use editor::DisplayPoint;
159    use search::BufferSearchBar;
160
161    use crate::{state::Mode, test::VimTestContext};
162
163    #[gpui::test]
164    async fn test_move_to_next(
165        cx: &mut gpui::TestAppContext,
166        deterministic: Arc<gpui::executor::Deterministic>,
167    ) {
168        let mut cx = VimTestContext::new(cx, true).await;
169        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
170
171        cx.simulate_keystrokes(["*"]);
172        deterministic.run_until_parked();
173        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
174
175        cx.simulate_keystrokes(["*"]);
176        deterministic.run_until_parked();
177        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
178
179        cx.simulate_keystrokes(["#"]);
180        deterministic.run_until_parked();
181        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
182
183        cx.simulate_keystrokes(["#"]);
184        deterministic.run_until_parked();
185        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
186
187        cx.simulate_keystrokes(["2", "*"]);
188        deterministic.run_until_parked();
189        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
190
191        cx.simulate_keystrokes(["g", "*"]);
192        deterministic.run_until_parked();
193        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
194
195        cx.simulate_keystrokes(["n"]);
196        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
197
198        cx.simulate_keystrokes(["g", "#"]);
199        deterministic.run_until_parked();
200        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
201    }
202
203    #[gpui::test]
204    async fn test_search(
205        cx: &mut gpui::TestAppContext,
206        deterministic: Arc<gpui::executor::Deterministic>,
207    ) {
208        let mut cx = VimTestContext::new(cx, true).await;
209
210        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
211        cx.simulate_keystrokes(["/", "c", "c"]);
212
213        let search_bar = cx.workspace(|workspace, cx| {
214            workspace
215                .active_pane()
216                .read(cx)
217                .toolbar()
218                .read(cx)
219                .item_of_type::<BufferSearchBar>()
220                .expect("Buffer search bar should be deployed")
221        });
222
223        search_bar.read_with(cx.cx, |bar, cx| {
224            assert_eq!(bar.query(cx), "cc");
225        });
226
227        deterministic.run_until_parked();
228
229        cx.update_editor(|editor, cx| {
230            let highlights = editor.all_background_highlights(cx);
231            assert_eq!(3, highlights.len());
232            assert_eq!(
233                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
234                highlights[0].0
235            )
236        });
237
238        cx.simulate_keystrokes(["enter"]);
239        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
240
241        // n to go to next/N to go to previous
242        cx.simulate_keystrokes(["n"]);
243        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
244        cx.simulate_keystrokes(["shift-n"]);
245        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
246
247        // ?<enter> to go to previous
248        cx.simulate_keystrokes(["?", "enter"]);
249        deterministic.run_until_parked();
250        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
251        cx.simulate_keystrokes(["?", "enter"]);
252        deterministic.run_until_parked();
253        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
254
255        // /<enter> to go to next
256        cx.simulate_keystrokes(["/", "enter"]);
257        deterministic.run_until_parked();
258        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
259
260        // ?{search}<enter> to search backwards
261        cx.simulate_keystrokes(["?", "b", "enter"]);
262        deterministic.run_until_parked();
263        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
264
265        // works with counts
266        cx.simulate_keystrokes(["4", "/", "c"]);
267        deterministic.run_until_parked();
268        cx.simulate_keystrokes(["enter"]);
269        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
270
271        // check that searching resumes from cursor, not previous match
272        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
273        cx.simulate_keystrokes(["/", "d"]);
274        deterministic.run_until_parked();
275        cx.simulate_keystrokes(["enter"]);
276        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
277        cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
278        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
279        cx.simulate_keystrokes(["/", "b"]);
280        deterministic.run_until_parked();
281        cx.simulate_keystrokes(["enter"]);
282        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
283    }
284
285    #[gpui::test]
286    async fn test_non_vim_search(
287        cx: &mut gpui::TestAppContext,
288        deterministic: Arc<gpui::executor::Deterministic>,
289    ) {
290        let mut cx = VimTestContext::new(cx, false).await;
291        cx.set_state("ˇone one one one", Mode::Normal);
292        cx.simulate_keystrokes(["cmd-f"]);
293        deterministic.run_until_parked();
294
295        cx.assert_editor_state("«oneˇ» one one one");
296        cx.simulate_keystrokes(["enter"]);
297        cx.assert_editor_state("one «oneˇ» one one");
298        cx.simulate_keystrokes(["shift-enter"]);
299        cx.assert_editor_state("«oneˇ» one one one");
300    }
301}