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