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