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