search.rs

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