search.rs

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