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