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.model().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.model().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    use indoc::indoc;
577    use search::BufferSearchBar;
578    use settings::SettingsStore;
579
580    #[gpui::test]
581    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
582        let mut cx = VimTestContext::new(cx, true).await;
583        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
584
585        cx.simulate_keystrokes("*");
586        cx.run_until_parked();
587        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
588
589        cx.simulate_keystrokes("*");
590        cx.run_until_parked();
591        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
592
593        cx.simulate_keystrokes("#");
594        cx.run_until_parked();
595        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
596
597        cx.simulate_keystrokes("#");
598        cx.run_until_parked();
599        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
600
601        cx.simulate_keystrokes("2 *");
602        cx.run_until_parked();
603        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
604
605        cx.simulate_keystrokes("g *");
606        cx.run_until_parked();
607        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
608
609        cx.simulate_keystrokes("n");
610        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
611
612        cx.simulate_keystrokes("g #");
613        cx.run_until_parked();
614        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
615    }
616
617    #[gpui::test]
618    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
619        let mut cx = VimTestContext::new(cx, true).await;
620
621        cx.update_global(|store: &mut SettingsStore, cx| {
622            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
623        });
624
625        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
626
627        cx.simulate_keystrokes("*");
628        cx.run_until_parked();
629        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
630
631        cx.simulate_keystrokes("*");
632        cx.run_until_parked();
633        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
634
635        cx.simulate_keystrokes("#");
636        cx.run_until_parked();
637        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
638
639        cx.simulate_keystrokes("3 *");
640        cx.run_until_parked();
641        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
642
643        cx.simulate_keystrokes("g *");
644        cx.run_until_parked();
645        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
646
647        cx.simulate_keystrokes("n");
648        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
649
650        cx.simulate_keystrokes("g #");
651        cx.run_until_parked();
652        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
653    }
654
655    #[gpui::test]
656    async fn test_search(cx: &mut gpui::TestAppContext) {
657        let mut cx = VimTestContext::new(cx, true).await;
658
659        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
660        cx.simulate_keystrokes("/ c c");
661
662        let search_bar = cx.workspace(|workspace, _, cx| {
663            workspace
664                .active_pane()
665                .read(cx)
666                .toolbar()
667                .read(cx)
668                .item_of_type::<BufferSearchBar>()
669                .expect("Buffer search bar should be deployed")
670        });
671
672        cx.update_model(search_bar, |bar, _window, cx| {
673            assert_eq!(bar.query(cx), "cc");
674        });
675
676        cx.run_until_parked();
677
678        cx.update_editor(|editor, window, cx| {
679            let highlights = editor.all_text_background_highlights(window, cx);
680            assert_eq!(3, highlights.len());
681            assert_eq!(
682                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
683                highlights[0].0
684            )
685        });
686
687        cx.simulate_keystrokes("enter");
688        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
689
690        // n to go to next/N to go to previous
691        cx.simulate_keystrokes("n");
692        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
693        cx.simulate_keystrokes("shift-n");
694        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
695
696        // ?<enter> to go to previous
697        cx.simulate_keystrokes("? enter");
698        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
699        cx.simulate_keystrokes("? enter");
700        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
701
702        // /<enter> to go to next
703        cx.simulate_keystrokes("/ enter");
704        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
705
706        // ?{search}<enter> to search backwards
707        cx.simulate_keystrokes("? b enter");
708        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
709
710        // works with counts
711        cx.simulate_keystrokes("4 / c");
712        cx.simulate_keystrokes("enter");
713        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
714
715        // check that searching resumes from cursor, not previous match
716        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
717        cx.simulate_keystrokes("/ d");
718        cx.simulate_keystrokes("enter");
719        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
720        cx.update_editor(|editor, window, cx| {
721            editor.move_to_beginning(&Default::default(), window, cx)
722        });
723        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
724        cx.simulate_keystrokes("/ b");
725        cx.simulate_keystrokes("enter");
726        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
727
728        // check that searching switches to normal mode if in visual mode
729        cx.set_state("ˇone two one", Mode::Normal);
730        cx.simulate_keystrokes("v l l");
731        cx.assert_editor_state("«oneˇ» two one");
732        cx.simulate_keystrokes("*");
733        cx.assert_state("one two ˇone", Mode::Normal);
734
735        // check that searching with unable search wrap
736        cx.update_global(|store: &mut SettingsStore, cx| {
737            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
738        });
739        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
740        cx.simulate_keystrokes("/ c c enter");
741
742        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
743
744        // n to go to next/N to go to previous
745        cx.simulate_keystrokes("n");
746        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
747        cx.simulate_keystrokes("shift-n");
748        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
749
750        // ?<enter> to go to previous
751        cx.simulate_keystrokes("? enter");
752        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
753        cx.simulate_keystrokes("? enter");
754        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
755    }
756
757    #[gpui::test]
758    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
759        let mut cx = VimTestContext::new(cx, false).await;
760        cx.cx.set_state("ˇone one one one");
761        cx.simulate_keystrokes("cmd-f");
762        cx.run_until_parked();
763
764        cx.assert_editor_state("«oneˇ» one one one");
765        cx.simulate_keystrokes("enter");
766        cx.assert_editor_state("one «oneˇ» one one");
767        cx.simulate_keystrokes("shift-enter");
768        cx.assert_editor_state("«oneˇ» one one one");
769    }
770
771    #[gpui::test]
772    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
773        let mut cx = NeovimBackedTestContext::new(cx).await;
774
775        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
776        cx.simulate_shared_keystrokes("v 3 l *").await;
777        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
778    }
779
780    #[gpui::test]
781    async fn test_d_search(cx: &mut gpui::TestAppContext) {
782        let mut cx = NeovimBackedTestContext::new(cx).await;
783
784        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
785        cx.simulate_shared_keystrokes("d / c d").await;
786        cx.simulate_shared_keystrokes("enter").await;
787        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
788    }
789
790    #[gpui::test]
791    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
792        let mut cx = NeovimBackedTestContext::new(cx).await;
793
794        cx.set_shared_state("ˇa b a b a b a").await;
795        cx.simulate_shared_keystrokes("*").await;
796        cx.simulate_shared_keystrokes("n").await;
797        cx.shared_state().await.assert_eq("a b a b ˇa b a");
798        cx.simulate_shared_keystrokes("#").await;
799        cx.shared_state().await.assert_eq("a b ˇa b a b a");
800        cx.simulate_shared_keystrokes("n").await;
801        cx.shared_state().await.assert_eq("ˇa b a b a b a");
802    }
803
804    #[gpui::test]
805    async fn test_v_search(cx: &mut gpui::TestAppContext) {
806        let mut cx = NeovimBackedTestContext::new(cx).await;
807
808        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
809        cx.simulate_shared_keystrokes("v / c d").await;
810        cx.simulate_shared_keystrokes("enter").await;
811        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
812
813        cx.set_shared_state("a a aˇ a a a").await;
814        cx.simulate_shared_keystrokes("v / a").await;
815        cx.simulate_shared_keystrokes("enter").await;
816        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
817        cx.simulate_shared_keystrokes("/ enter").await;
818        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
819        cx.simulate_shared_keystrokes("? enter").await;
820        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
821        cx.simulate_shared_keystrokes("? enter").await;
822        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
823        cx.simulate_shared_keystrokes("/ enter").await;
824        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
825        cx.simulate_shared_keystrokes("/ enter").await;
826        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
827    }
828
829    #[gpui::test]
830    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
831        let mut cx = NeovimBackedTestContext::new(cx).await;
832
833        cx.set_shared_state(indoc! {
834            "ˇone two
835             three four
836             five six
837             "
838        })
839        .await;
840        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
841        cx.simulate_shared_keystrokes("enter").await;
842        cx.shared_state().await.assert_eq(indoc! {
843            "«one twoˇ»
844             «three fˇ»our
845             five six
846             "
847        });
848    }
849
850    // cargo test -p vim --features neovim test_replace_with_range_at_start
851    #[gpui::test]
852    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
853        let mut cx = NeovimBackedTestContext::new(cx).await;
854
855        cx.set_shared_state(indoc! {
856            "ˇa
857            a
858            a
859            a
860            a
861            a
862            a
863             "
864        })
865        .await;
866        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
867        cx.simulate_shared_keystrokes("enter").await;
868        cx.shared_state().await.assert_eq(indoc! {
869            "a
870            ba
871            ba
872            ba
873            ˇba
874            a
875            a
876             "
877        });
878        cx.executor().advance_clock(Duration::from_millis(250));
879        cx.run_until_parked();
880
881        cx.simulate_shared_keystrokes("/ a enter").await;
882        cx.shared_state().await.assert_eq(indoc! {
883            "a
884                ba
885                ba
886                ba
887                bˇa
888                a
889                a
890                 "
891        });
892    }
893
894    // cargo test -p vim --features neovim test_replace_with_range
895    #[gpui::test]
896    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
897        let mut cx = NeovimBackedTestContext::new(cx).await;
898
899        cx.set_shared_state(indoc! {
900            "ˇa
901            a
902            a
903            a
904            a
905            a
906            a
907             "
908        })
909        .await;
910        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
911        cx.simulate_shared_keystrokes("enter").await;
912        cx.shared_state().await.assert_eq(indoc! {
913            "a
914            b
915            b
916            b
917            ˇb
918            a
919            a
920             "
921        });
922        cx.executor().advance_clock(Duration::from_millis(250));
923        cx.run_until_parked();
924
925        cx.simulate_shared_keystrokes("/ a enter").await;
926        cx.shared_state().await.assert_eq(indoc! {
927            "a
928                b
929                b
930                b
931                b
932                ˇa
933                a
934                 "
935        });
936    }
937}