search.rs

  1use editor::Editor;
  2use gpui::{actions, impl_actions, impl_internal_actions, Context, Window};
  3use language::Point;
  4use schemars::JsonSchema;
  5use search::{buffer_search, BufferSearchBar, SearchOptions};
  6use serde_derive::Deserialize;
  7use std::{iter::Peekable, str::Chars, time::Duration};
  8use util::serde::default_true;
  9use workspace::{notifications::NotifyResultExt, searchable::Direction};
 10
 11use crate::{
 12    command::CommandRange,
 13    motion::Motion,
 14    state::{Mode, SearchState},
 15    Vim,
 16};
 17
 18#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 19#[serde(deny_unknown_fields)]
 20pub(crate) struct MoveToNext {
 21    #[serde(default = "default_true")]
 22    case_sensitive: bool,
 23    #[serde(default)]
 24    partial_word: bool,
 25    #[serde(default = "default_true")]
 26    regex: bool,
 27}
 28
 29#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 30#[serde(deny_unknown_fields)]
 31pub(crate) struct MoveToPrev {
 32    #[serde(default = "default_true")]
 33    case_sensitive: bool,
 34    #[serde(default)]
 35    partial_word: bool,
 36    #[serde(default = "default_true")]
 37    regex: bool,
 38}
 39
 40#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 41#[serde(deny_unknown_fields)]
 42pub(crate) struct Search {
 43    #[serde(default)]
 44    backwards: bool,
 45    #[serde(default = "default_true")]
 46    regex: bool,
 47}
 48
 49#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 50#[serde(deny_unknown_fields)]
 51pub struct FindCommand {
 52    pub query: String,
 53    pub backwards: bool,
 54}
 55
 56#[derive(Clone, Debug, PartialEq)]
 57pub struct ReplaceCommand {
 58    pub(crate) range: CommandRange,
 59    pub(crate) replacement: Replacement,
 60}
 61
 62#[derive(Clone, Debug, PartialEq)]
 63pub(crate) struct Replacement {
 64    search: String,
 65    replacement: String,
 66    should_replace_all: bool,
 67    is_case_sensitive: bool,
 68}
 69
 70actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
 71impl_actions!(vim, [FindCommand, Search, MoveToPrev, MoveToNext]);
 72impl_internal_actions!(vim, [ReplaceCommand]);
 73
 74pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 75    Vim::action(editor, cx, Vim::move_to_next);
 76    Vim::action(editor, cx, Vim::move_to_prev);
 77    Vim::action(editor, cx, Vim::move_to_next_match);
 78    Vim::action(editor, cx, Vim::move_to_prev_match);
 79    Vim::action(editor, cx, Vim::search);
 80    Vim::action(editor, cx, Vim::search_deploy);
 81    Vim::action(editor, cx, Vim::find_command);
 82    Vim::action(editor, cx, Vim::replace_command);
 83}
 84
 85impl Vim {
 86    fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context<Self>) {
 87        self.move_to_internal(
 88            Direction::Next,
 89            action.case_sensitive,
 90            !action.partial_word,
 91            action.regex,
 92            window,
 93            cx,
 94        )
 95    }
 96
 97    fn move_to_prev(&mut self, action: &MoveToPrev, window: &mut Window, cx: &mut Context<Self>) {
 98        self.move_to_internal(
 99            Direction::Prev,
100            action.case_sensitive,
101            !action.partial_word,
102            action.regex,
103            window,
104            cx,
105        )
106    }
107
108    fn move_to_next_match(
109        &mut self,
110        _: &MoveToNextMatch,
111        window: &mut Window,
112        cx: &mut Context<Self>,
113    ) {
114        self.move_to_match_internal(self.search.direction, window, cx)
115    }
116
117    fn move_to_prev_match(
118        &mut self,
119        _: &MoveToPrevMatch,
120        window: &mut Window,
121        cx: &mut Context<Self>,
122    ) {
123        self.move_to_match_internal(self.search.direction.opposite(), window, cx)
124    }
125
126    fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context<Self>) {
127        let Some(pane) = self.pane(window, cx) else {
128            return;
129        };
130        let direction = if action.backwards {
131            Direction::Prev
132        } else {
133            Direction::Next
134        };
135        let count = Vim::take_count(cx).unwrap_or(1);
136        let prior_selections = self.editor_selections(window, cx);
137        pane.update(cx, |pane, cx| {
138            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
139                search_bar.update(cx, |search_bar, cx| {
140                    if !search_bar.show(window, cx) {
141                        return;
142                    }
143                    let query = search_bar.query(cx);
144
145                    search_bar.select_query(window, cx);
146                    cx.focus_self(window);
147
148                    search_bar.set_replacement(None, cx);
149                    let mut options = SearchOptions::NONE;
150                    if action.regex {
151                        options |= SearchOptions::REGEX;
152                    }
153                    search_bar.set_search_options(options, cx);
154                    let prior_mode = if self.temp_mode {
155                        Mode::Insert
156                    } else {
157                        self.mode
158                    };
159
160                    self.search = SearchState {
161                        direction,
162                        count,
163                        initial_query: query,
164                        prior_selections,
165                        prior_operator: self.operator_stack.last().cloned(),
166                        prior_mode,
167                    }
168                });
169            }
170        })
171    }
172
173    // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
174    fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
175        self.search = Default::default();
176        cx.propagate();
177    }
178
179    pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
180        self.store_visual_marks(window, cx);
181        let Some(pane) = self.pane(window, cx) else {
182            return;
183        };
184        let result = pane.update(cx, |pane, cx| {
185            let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
186            search_bar.update(cx, |search_bar, cx| {
187                let mut count = self.search.count;
188                let direction = self.search.direction;
189                // in the case that the query has changed, the search bar
190                // will have selected the next match already.
191                if (search_bar.query(cx) != self.search.initial_query)
192                    && self.search.direction == Direction::Next
193                {
194                    count = count.saturating_sub(1)
195                }
196                self.search.count = 1;
197                search_bar.select_match(direction, count, window, cx);
198                search_bar.focus_editor(&Default::default(), window, cx);
199
200                let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
201                let prior_mode = self.search.prior_mode;
202                let prior_operator = self.search.prior_operator.take();
203
204                let query = search_bar.query(cx).into();
205                Vim::globals(cx).registers.insert('/', query);
206                Some((prior_selections, prior_mode, prior_operator))
207            })
208        });
209
210        let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
211            return;
212        };
213
214        let new_selections = self.editor_selections(window, cx);
215
216        // If the active editor has changed during a search, don't panic.
217        if prior_selections.iter().any(|s| {
218            self.update_editor(window, cx, |_, editor, window, cx| {
219                !s.start
220                    .is_valid(&editor.snapshot(window, cx).buffer_snapshot)
221            })
222            .unwrap_or(true)
223        }) {
224            prior_selections.clear();
225        }
226
227        if prior_mode != self.mode {
228            self.switch_mode(prior_mode, true, window, cx);
229        }
230        if let Some(operator) = prior_operator {
231            self.push_operator(operator, window, cx);
232        };
233        self.search_motion(
234            Motion::ZedSearchResult {
235                prior_selections,
236                new_selections,
237            },
238            window,
239            cx,
240        );
241    }
242
243    pub fn move_to_match_internal(
244        &mut self,
245        direction: Direction,
246        window: &mut Window,
247        cx: &mut Context<Self>,
248    ) {
249        let Some(pane) = self.pane(window, cx) else {
250            return;
251        };
252        let count = Vim::take_count(cx).unwrap_or(1);
253        let prior_selections = self.editor_selections(window, cx);
254
255        let success = pane.update(cx, |pane, cx| {
256            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
257                return false;
258            };
259            search_bar.update(cx, |search_bar, cx| {
260                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
261                    return false;
262                }
263                search_bar.select_match(direction, count, window, cx);
264                true
265            })
266        });
267        if !success {
268            return;
269        }
270
271        let new_selections = self.editor_selections(window, cx);
272        self.search_motion(
273            Motion::ZedSearchResult {
274                prior_selections,
275                new_selections,
276            },
277            window,
278            cx,
279        );
280    }
281
282    pub fn move_to_internal(
283        &mut self,
284        direction: Direction,
285        case_sensitive: bool,
286        whole_word: bool,
287        regex: bool,
288        window: &mut Window,
289        cx: &mut Context<Self>,
290    ) {
291        let Some(pane) = self.pane(window, cx) else {
292            return;
293        };
294        let count = Vim::take_count(cx).unwrap_or(1);
295        let prior_selections = self.editor_selections(window, cx);
296        let vim = cx.entity().clone();
297
298        let searched = pane.update(cx, |pane, cx| {
299            self.search.direction = direction;
300            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
301                return false;
302            };
303            let search = search_bar.update(cx, |search_bar, cx| {
304                let mut options = SearchOptions::NONE;
305                if case_sensitive {
306                    options |= SearchOptions::CASE_SENSITIVE;
307                }
308                if regex {
309                    options |= SearchOptions::REGEX;
310                }
311                if whole_word {
312                    options |= SearchOptions::WHOLE_WORD;
313                }
314                if !search_bar.show(window, cx) {
315                    return None;
316                }
317                let Some(query) = search_bar.query_suggestion(window, cx) else {
318                    drop(search_bar.search("", None, window, cx));
319                    return None;
320                };
321                let query = regex::escape(&query);
322                Some(search_bar.search(&query, Some(options), window, cx))
323            });
324
325            let Some(search) = search else { return false };
326
327            let search_bar = search_bar.downgrade();
328            cx.spawn_in(window, |_, mut cx| async move {
329                search.await?;
330                search_bar.update_in(&mut cx, |search_bar, window, cx| {
331                    search_bar.select_match(direction, count, window, cx);
332
333                    vim.update(cx, |vim, cx| {
334                        let new_selections = vim.editor_selections(window, cx);
335                        vim.search_motion(
336                            Motion::ZedSearchResult {
337                                prior_selections,
338                                new_selections,
339                            },
340                            window,
341                            cx,
342                        )
343                    });
344                })?;
345                anyhow::Ok(())
346            })
347            .detach_and_log_err(cx);
348            true
349        });
350        if !searched {
351            self.clear_operator(window, cx)
352        }
353
354        if self.mode.is_visual() {
355            self.switch_mode(Mode::Normal, false, window, cx)
356        }
357    }
358
359    fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
360        let Some(pane) = self.pane(window, cx) else {
361            return;
362        };
363        pane.update(cx, |pane, cx| {
364            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
365                let search = search_bar.update(cx, |search_bar, cx| {
366                    if !search_bar.show(window, cx) {
367                        return None;
368                    }
369                    let mut query = action.query.clone();
370                    if query.is_empty() {
371                        query = search_bar.query(cx);
372                    };
373
374                    let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
375                    if search_bar.should_use_smartcase_search(cx) {
376                        options.set(
377                            SearchOptions::CASE_SENSITIVE,
378                            search_bar.is_contains_uppercase(&query),
379                        );
380                    }
381
382                    Some(search_bar.search(&query, Some(options), window, cx))
383                });
384                let Some(search) = search else { return };
385                let search_bar = search_bar.downgrade();
386                let direction = if action.backwards {
387                    Direction::Prev
388                } else {
389                    Direction::Next
390                };
391                cx.spawn_in(window, |_, mut cx| async move {
392                    search.await?;
393                    search_bar.update_in(&mut cx, |search_bar, window, cx| {
394                        search_bar.select_match(direction, 1, window, cx)
395                    })?;
396                    anyhow::Ok(())
397                })
398                .detach_and_log_err(cx);
399            }
400        })
401    }
402
403    fn replace_command(
404        &mut self,
405        action: &ReplaceCommand,
406        window: &mut Window,
407        cx: &mut Context<Self>,
408    ) {
409        let replacement = action.replacement.clone();
410        let Some(((pane, workspace), editor)) = self
411            .pane(window, cx)
412            .zip(self.workspace(window))
413            .zip(self.editor())
414        else {
415            return;
416        };
417        if let Some(result) = self.update_editor(window, cx, |vim, editor, window, cx| {
418            let range = action.range.buffer_range(vim, editor, window, cx)?;
419            let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
420            let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
421            let range = snapshot.anchor_before(Point::new(range.start.0, 0))
422                ..snapshot.anchor_after(end_point);
423            editor.set_search_within_ranges(&[range], cx);
424            anyhow::Ok(())
425        }) {
426            workspace.update(cx, |workspace, cx| {
427                result.notify_err(workspace, cx);
428            })
429        }
430        let vim = cx.entity().clone();
431        pane.update(cx, |pane, cx| {
432            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
433                return;
434            };
435            let search = search_bar.update(cx, |search_bar, cx| {
436                if !search_bar.show(window, cx) {
437                    return None;
438                }
439
440                let mut options = SearchOptions::REGEX;
441                if replacement.is_case_sensitive {
442                    options.set(SearchOptions::CASE_SENSITIVE, true)
443                }
444                let search = if replacement.search.is_empty() {
445                    search_bar.query(cx)
446                } else {
447                    replacement.search
448                };
449                if search_bar.should_use_smartcase_search(cx) {
450                    options.set(
451                        SearchOptions::CASE_SENSITIVE,
452                        search_bar.is_contains_uppercase(&search),
453                    );
454                }
455                search_bar.set_replacement(Some(&replacement.replacement), cx);
456                Some(search_bar.search(&search, Some(options), window, cx))
457            });
458            let Some(search) = search else { return };
459            let search_bar = search_bar.downgrade();
460            cx.spawn_in(window, |_, mut cx| async move {
461                search.await?;
462                search_bar.update_in(&mut cx, |search_bar, window, cx| {
463                    if replacement.should_replace_all {
464                        search_bar.select_last_match(window, cx);
465                        search_bar.replace_all(&Default::default(), window, cx);
466                        cx.spawn(|_, mut cx| async move {
467                            cx.background_executor()
468                                .timer(Duration::from_millis(200))
469                                .await;
470                            editor
471                                .update(&mut cx, |editor, cx| editor.clear_search_within_ranges(cx))
472                                .ok();
473                        })
474                        .detach();
475                        vim.update(cx, |vim, cx| {
476                            vim.move_cursor(
477                                Motion::StartOfLine {
478                                    display_lines: false,
479                                },
480                                None,
481                                window,
482                                cx,
483                            )
484                        });
485                    }
486                })?;
487                anyhow::Ok(())
488            })
489            .detach_and_log_err(cx);
490        })
491    }
492}
493
494impl Replacement {
495    // convert a vim query into something more usable by zed.
496    // we don't attempt to fully convert between the two regex syntaxes,
497    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
498    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
499    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
500        let delimiter = chars
501            .next()
502            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
503
504        let mut search = String::new();
505        let mut replacement = String::new();
506        let mut flags = String::new();
507
508        let mut buffer = &mut search;
509
510        let mut escaped = false;
511        // 0 - parsing search
512        // 1 - parsing replacement
513        // 2 - parsing flags
514        let mut phase = 0;
515
516        for c in chars {
517            if escaped {
518                escaped = false;
519                if phase == 1 && c.is_ascii_digit() {
520                    buffer.push('$')
521                // unescape escaped parens
522                } else if phase == 0 && c == '(' || c == ')' {
523                } else if c != delimiter {
524                    buffer.push('\\')
525                }
526                buffer.push(c)
527            } else if c == '\\' {
528                escaped = true;
529            } else if c == delimiter {
530                if phase == 0 {
531                    buffer = &mut replacement;
532                    phase = 1;
533                } else if phase == 1 {
534                    buffer = &mut flags;
535                    phase = 2;
536                } else {
537                    break;
538                }
539            } else {
540                // escape unescaped parens
541                if phase == 0 && c == '(' || c == ')' {
542                    buffer.push('\\')
543                }
544                buffer.push(c)
545            }
546        }
547
548        let mut replacement = Replacement {
549            search,
550            replacement,
551            should_replace_all: true,
552            is_case_sensitive: true,
553        };
554
555        for c in flags.chars() {
556            match c {
557                'g' | 'I' => {}
558                'c' | 'n' => replacement.should_replace_all = false,
559                'i' => replacement.is_case_sensitive = false,
560                _ => {}
561            }
562        }
563
564        Some(replacement)
565    }
566}
567
568#[cfg(test)]
569mod test {
570    use std::time::Duration;
571
572    use crate::{
573        state::Mode,
574        test::{NeovimBackedTestContext, VimTestContext},
575    };
576    use editor::EditorSettings;
577    use editor::{display_map::DisplayRow, DisplayPoint};
578
579    use indoc::indoc;
580    use search::BufferSearchBar;
581    use settings::SettingsStore;
582
583    #[gpui::test]
584    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
585        let mut cx = VimTestContext::new(cx, true).await;
586        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
587
588        cx.simulate_keystrokes("*");
589        cx.run_until_parked();
590        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
591
592        cx.simulate_keystrokes("*");
593        cx.run_until_parked();
594        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
595
596        cx.simulate_keystrokes("#");
597        cx.run_until_parked();
598        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
599
600        cx.simulate_keystrokes("#");
601        cx.run_until_parked();
602        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
603
604        cx.simulate_keystrokes("2 *");
605        cx.run_until_parked();
606        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
607
608        cx.simulate_keystrokes("g *");
609        cx.run_until_parked();
610        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
611
612        cx.simulate_keystrokes("n");
613        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
614
615        cx.simulate_keystrokes("g #");
616        cx.run_until_parked();
617        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
618    }
619
620    #[gpui::test]
621    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
622        let mut cx = VimTestContext::new(cx, true).await;
623
624        cx.update_global(|store: &mut SettingsStore, cx| {
625            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
626        });
627
628        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
629
630        cx.simulate_keystrokes("*");
631        cx.run_until_parked();
632        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
633
634        cx.simulate_keystrokes("*");
635        cx.run_until_parked();
636        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
637
638        cx.simulate_keystrokes("#");
639        cx.run_until_parked();
640        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
641
642        cx.simulate_keystrokes("3 *");
643        cx.run_until_parked();
644        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
645
646        cx.simulate_keystrokes("g *");
647        cx.run_until_parked();
648        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
649
650        cx.simulate_keystrokes("n");
651        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
652
653        cx.simulate_keystrokes("g #");
654        cx.run_until_parked();
655        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
656    }
657
658    #[gpui::test]
659    async fn test_search(cx: &mut gpui::TestAppContext) {
660        let mut cx = VimTestContext::new(cx, true).await;
661
662        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
663        cx.simulate_keystrokes("/ c c");
664
665        let search_bar = cx.workspace(|workspace, _, cx| {
666            workspace
667                .active_pane()
668                .read(cx)
669                .toolbar()
670                .read(cx)
671                .item_of_type::<BufferSearchBar>()
672                .expect("Buffer search bar should be deployed")
673        });
674
675        cx.update_entity(search_bar, |bar, _window, cx| {
676            assert_eq!(bar.query(cx), "cc");
677        });
678
679        cx.run_until_parked();
680
681        cx.update_editor(|editor, window, cx| {
682            let highlights = editor.all_text_background_highlights(window, cx);
683            assert_eq!(3, highlights.len());
684            assert_eq!(
685                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
686                highlights[0].0
687            )
688        });
689
690        cx.simulate_keystrokes("enter");
691        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
692
693        // n to go to next/N to go to previous
694        cx.simulate_keystrokes("n");
695        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
696        cx.simulate_keystrokes("shift-n");
697        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
698
699        // ?<enter> to go to previous
700        cx.simulate_keystrokes("? enter");
701        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
702        cx.simulate_keystrokes("? enter");
703        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
704
705        // /<enter> to go to next
706        cx.simulate_keystrokes("/ enter");
707        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
708
709        // ?{search}<enter> to search backwards
710        cx.simulate_keystrokes("? b enter");
711        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
712
713        // works with counts
714        cx.simulate_keystrokes("4 / c");
715        cx.simulate_keystrokes("enter");
716        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
717
718        // check that searching resumes from cursor, not previous match
719        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
720        cx.simulate_keystrokes("/ d");
721        cx.simulate_keystrokes("enter");
722        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
723        cx.update_editor(|editor, window, cx| {
724            editor.move_to_beginning(&Default::default(), window, cx)
725        });
726        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
727        cx.simulate_keystrokes("/ b");
728        cx.simulate_keystrokes("enter");
729        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
730
731        // check that searching switches to normal mode if in visual mode
732        cx.set_state("ˇone two one", Mode::Normal);
733        cx.simulate_keystrokes("v l l");
734        cx.assert_editor_state("«oneˇ» two one");
735        cx.simulate_keystrokes("*");
736        cx.assert_state("one two ˇone", Mode::Normal);
737
738        // check that searching with unable search wrap
739        cx.update_global(|store: &mut SettingsStore, cx| {
740            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
741        });
742        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
743        cx.simulate_keystrokes("/ c c enter");
744
745        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
746
747        // n to go to next/N to go to previous
748        cx.simulate_keystrokes("n");
749        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
750        cx.simulate_keystrokes("shift-n");
751        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
752
753        // ?<enter> to go to previous
754        cx.simulate_keystrokes("? enter");
755        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
756        cx.simulate_keystrokes("? enter");
757        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
758    }
759
760    #[gpui::test]
761    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
762        let mut cx = VimTestContext::new(cx, false).await;
763        cx.cx.set_state("ˇone one one one");
764        cx.simulate_keystrokes("cmd-f");
765        cx.run_until_parked();
766
767        cx.assert_editor_state("«oneˇ» one one one");
768        cx.simulate_keystrokes("enter");
769        cx.assert_editor_state("one «oneˇ» one one");
770        cx.simulate_keystrokes("shift-enter");
771        cx.assert_editor_state("«oneˇ» one one one");
772    }
773
774    #[gpui::test]
775    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
776        let mut cx = NeovimBackedTestContext::new(cx).await;
777
778        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
779        cx.simulate_shared_keystrokes("v 3 l *").await;
780        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
781    }
782
783    #[gpui::test]
784    async fn test_d_search(cx: &mut gpui::TestAppContext) {
785        let mut cx = NeovimBackedTestContext::new(cx).await;
786
787        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
788        cx.simulate_shared_keystrokes("d / c d").await;
789        cx.simulate_shared_keystrokes("enter").await;
790        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
791    }
792
793    #[gpui::test]
794    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
795        let mut cx = NeovimBackedTestContext::new(cx).await;
796
797        cx.set_shared_state("ˇa b a b a b a").await;
798        cx.simulate_shared_keystrokes("*").await;
799        cx.simulate_shared_keystrokes("n").await;
800        cx.shared_state().await.assert_eq("a b a b ˇa b a");
801        cx.simulate_shared_keystrokes("#").await;
802        cx.shared_state().await.assert_eq("a b ˇa b a b a");
803        cx.simulate_shared_keystrokes("n").await;
804        cx.shared_state().await.assert_eq("ˇa b a b a b a");
805    }
806
807    #[gpui::test]
808    async fn test_v_search(cx: &mut gpui::TestAppContext) {
809        let mut cx = NeovimBackedTestContext::new(cx).await;
810
811        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
812        cx.simulate_shared_keystrokes("v / c d").await;
813        cx.simulate_shared_keystrokes("enter").await;
814        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
815
816        cx.set_shared_state("a a aˇ a a a").await;
817        cx.simulate_shared_keystrokes("v / a").await;
818        cx.simulate_shared_keystrokes("enter").await;
819        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
820        cx.simulate_shared_keystrokes("/ enter").await;
821        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
822        cx.simulate_shared_keystrokes("? enter").await;
823        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
824        cx.simulate_shared_keystrokes("? enter").await;
825        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
826        cx.simulate_shared_keystrokes("/ enter").await;
827        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
828        cx.simulate_shared_keystrokes("/ enter").await;
829        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
830    }
831
832    #[gpui::test]
833    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
834        let mut cx = NeovimBackedTestContext::new(cx).await;
835
836        cx.set_shared_state(indoc! {
837            "ˇone two
838             three four
839             five six
840             "
841        })
842        .await;
843        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
844        cx.simulate_shared_keystrokes("enter").await;
845        cx.shared_state().await.assert_eq(indoc! {
846            "«one twoˇ»
847             «three fˇ»our
848             five six
849             "
850        });
851    }
852
853    // cargo test -p vim --features neovim test_replace_with_range_at_start
854    #[gpui::test]
855    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
856        let mut cx = NeovimBackedTestContext::new(cx).await;
857
858        cx.set_shared_state(indoc! {
859            "ˇa
860            a
861            a
862            a
863            a
864            a
865            a
866             "
867        })
868        .await;
869        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
870        cx.simulate_shared_keystrokes("enter").await;
871        cx.shared_state().await.assert_eq(indoc! {
872            "a
873            ba
874            ba
875            ba
876            ˇba
877            a
878            a
879             "
880        });
881        cx.executor().advance_clock(Duration::from_millis(250));
882        cx.run_until_parked();
883
884        cx.simulate_shared_keystrokes("/ a enter").await;
885        cx.shared_state().await.assert_eq(indoc! {
886            "a
887                ba
888                ba
889                ba
890                bˇa
891                a
892                a
893                 "
894        });
895    }
896
897    // cargo test -p vim --features neovim test_replace_with_range
898    #[gpui::test]
899    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
900        let mut cx = NeovimBackedTestContext::new(cx).await;
901
902        cx.set_shared_state(indoc! {
903            "ˇa
904            a
905            a
906            a
907            a
908            a
909            a
910             "
911        })
912        .await;
913        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
914        cx.simulate_shared_keystrokes("enter").await;
915        cx.shared_state().await.assert_eq(indoc! {
916            "a
917            b
918            b
919            b
920            ˇb
921            a
922            a
923             "
924        });
925        cx.executor().advance_clock(Duration::from_millis(250));
926        cx.run_until_parked();
927
928        cx.simulate_shared_keystrokes("/ a enter").await;
929        cx.shared_state().await.assert_eq(indoc! {
930            "a
931                b
932                b
933                b
934                b
935                ˇa
936                a
937                 "
938        });
939    }
940}