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