search.rs

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