search.rs

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