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