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