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