search.rs

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