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