search.rs

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