search.rs

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