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};
  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 mut options = SearchOptions::REGEX;
449
450            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
451                return;
452            };
453            let search = search_bar.update(cx, |search_bar, cx| {
454                if !search_bar.show(window, cx) {
455                    return None;
456                }
457
458                if replacement.is_case_sensitive {
459                    options.set(SearchOptions::CASE_SENSITIVE, true)
460                }
461                let search = if replacement.search.is_empty() {
462                    search_bar.query(cx)
463                } else {
464                    replacement.search
465                };
466                if search_bar.should_use_smartcase_search(cx) {
467                    options.set(
468                        SearchOptions::CASE_SENSITIVE,
469                        search_bar.is_contains_uppercase(&search),
470                    );
471                }
472
473                if !replacement.should_replace_all {
474                    options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
475                }
476
477                search_bar.set_replacement(Some(&replacement.replacement), cx);
478                Some(search_bar.search(&search, Some(options), window, cx))
479            });
480            let Some(search) = search else { return };
481            let search_bar = search_bar.downgrade();
482            cx.spawn_in(window, async move |_, cx| {
483                search.await?;
484                search_bar.update_in(cx, |search_bar, window, cx| {
485                    search_bar.select_last_match(window, cx);
486                    search_bar.replace_all(&Default::default(), window, cx);
487                    editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
488                    let _ = search_bar.search(&search_bar.query(cx), None, window, cx);
489                    vim.update(cx, |vim, cx| {
490                        vim.move_cursor(
491                            Motion::StartOfLine {
492                                display_lines: false,
493                            },
494                            None,
495                            window,
496                            cx,
497                        )
498                    });
499
500                    // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
501                    // this is not properly supported outside of vim mode, and
502                    // not disabling it makes the "Replace All Matches" button
503                    // actually replace only the first match on each line.
504                    options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
505                    search_bar.set_search_options(options, cx);
506                })?;
507                anyhow::Ok(())
508            })
509            .detach_and_log_err(cx);
510        })
511    }
512}
513
514impl Replacement {
515    // convert a vim query into something more usable by zed.
516    // we don't attempt to fully convert between the two regex syntaxes,
517    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
518    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
519    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
520        let delimiter = chars
521            .next()
522            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
523
524        let mut search = String::new();
525        let mut replacement = String::new();
526        let mut flags = String::new();
527
528        let mut buffer = &mut search;
529
530        let mut escaped = false;
531        // 0 - parsing search
532        // 1 - parsing replacement
533        // 2 - parsing flags
534        let mut phase = 0;
535
536        for c in chars {
537            if escaped {
538                escaped = false;
539                if phase == 1 && c.is_ascii_digit() {
540                    buffer.push('$')
541                // unescape escaped parens
542                } else if phase == 0 && c == '(' || c == ')' {
543                } else if c != delimiter {
544                    buffer.push('\\')
545                }
546                buffer.push(c)
547            } else if c == '\\' {
548                escaped = true;
549            } else if c == delimiter {
550                if phase == 0 {
551                    buffer = &mut replacement;
552                    phase = 1;
553                } else if phase == 1 {
554                    buffer = &mut flags;
555                    phase = 2;
556                } else {
557                    break;
558                }
559            } else {
560                // escape unescaped parens
561                if phase == 0 && c == '(' || c == ')' {
562                    buffer.push('\\')
563                }
564                buffer.push(c)
565            }
566        }
567
568        let mut replacement = Replacement {
569            search,
570            replacement,
571            should_replace_all: false,
572            is_case_sensitive: true,
573        };
574
575        for c in flags.chars() {
576            match c {
577                'g' => replacement.should_replace_all = true,
578                'c' | 'n' => replacement.should_replace_all = false,
579                'i' => replacement.is_case_sensitive = false,
580                'I' => replacement.is_case_sensitive = true,
581                _ => {}
582            }
583        }
584
585        Some(replacement)
586    }
587}
588
589#[cfg(test)]
590mod test {
591    use std::time::Duration;
592
593    use crate::{
594        state::Mode,
595        test::{NeovimBackedTestContext, VimTestContext},
596    };
597    use editor::EditorSettings;
598    use editor::{DisplayPoint, display_map::DisplayRow};
599
600    use indoc::indoc;
601    use search::BufferSearchBar;
602    use settings::SettingsStore;
603
604    #[gpui::test]
605    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
606        let mut cx = VimTestContext::new(cx, true).await;
607        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
608
609        cx.simulate_keystrokes("*");
610        cx.run_until_parked();
611        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
612
613        cx.simulate_keystrokes("*");
614        cx.run_until_parked();
615        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
616
617        cx.simulate_keystrokes("#");
618        cx.run_until_parked();
619        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
620
621        cx.simulate_keystrokes("#");
622        cx.run_until_parked();
623        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
624
625        cx.simulate_keystrokes("2 *");
626        cx.run_until_parked();
627        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
628
629        cx.simulate_keystrokes("g *");
630        cx.run_until_parked();
631        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
632
633        cx.simulate_keystrokes("n");
634        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
635
636        cx.simulate_keystrokes("g #");
637        cx.run_until_parked();
638        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
639    }
640
641    #[gpui::test]
642    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
643        let mut cx = VimTestContext::new(cx, true).await;
644
645        cx.update_global(|store: &mut SettingsStore, cx| {
646            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
647        });
648
649        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
650
651        cx.simulate_keystrokes("*");
652        cx.run_until_parked();
653        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
654
655        cx.simulate_keystrokes("*");
656        cx.run_until_parked();
657        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
658
659        cx.simulate_keystrokes("#");
660        cx.run_until_parked();
661        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
662
663        cx.simulate_keystrokes("3 *");
664        cx.run_until_parked();
665        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
666
667        cx.simulate_keystrokes("g *");
668        cx.run_until_parked();
669        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
670
671        cx.simulate_keystrokes("n");
672        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
673
674        cx.simulate_keystrokes("g #");
675        cx.run_until_parked();
676        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
677    }
678
679    #[gpui::test]
680    async fn test_search(cx: &mut gpui::TestAppContext) {
681        let mut cx = VimTestContext::new(cx, true).await;
682
683        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
684        cx.simulate_keystrokes("/ c c");
685
686        let search_bar = cx.workspace(|workspace, _, cx| {
687            workspace
688                .active_pane()
689                .read(cx)
690                .toolbar()
691                .read(cx)
692                .item_of_type::<BufferSearchBar>()
693                .expect("Buffer search bar should be deployed")
694        });
695
696        cx.update_entity(search_bar, |bar, _window, cx| {
697            assert_eq!(bar.query(cx), "cc");
698        });
699
700        cx.run_until_parked();
701
702        cx.update_editor(|editor, window, cx| {
703            let highlights = editor.all_text_background_highlights(window, cx);
704            assert_eq!(3, highlights.len());
705            assert_eq!(
706                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
707                highlights[0].0
708            )
709        });
710
711        cx.simulate_keystrokes("enter");
712        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
713
714        // n to go to next/N to go to previous
715        cx.simulate_keystrokes("n");
716        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
717        cx.simulate_keystrokes("shift-n");
718        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
719
720        // ?<enter> to go to previous
721        cx.simulate_keystrokes("? enter");
722        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
723        cx.simulate_keystrokes("? enter");
724        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
725
726        // /<enter> to go to next
727        cx.simulate_keystrokes("/ enter");
728        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
729
730        // ?{search}<enter> to search backwards
731        cx.simulate_keystrokes("? b enter");
732        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
733
734        // works with counts
735        cx.simulate_keystrokes("4 / c");
736        cx.simulate_keystrokes("enter");
737        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
738
739        // check that searching resumes from cursor, not previous match
740        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
741        cx.simulate_keystrokes("/ d");
742        cx.simulate_keystrokes("enter");
743        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
744        cx.update_editor(|editor, window, cx| {
745            editor.move_to_beginning(&Default::default(), window, cx)
746        });
747        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
748        cx.simulate_keystrokes("/ b");
749        cx.simulate_keystrokes("enter");
750        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
751
752        // check that searching switches to normal mode if in visual mode
753        cx.set_state("ˇone two one", Mode::Normal);
754        cx.simulate_keystrokes("v l l");
755        cx.assert_editor_state("«oneˇ» two one");
756        cx.simulate_keystrokes("*");
757        cx.assert_state("one two ˇone", Mode::Normal);
758
759        // check that a backward search after last match works correctly
760        cx.set_state("aa\naa\nbbˇ", Mode::Normal);
761        cx.simulate_keystrokes("? a a");
762        cx.simulate_keystrokes("enter");
763        cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
764
765        // check that searching with unable search wrap
766        cx.update_global(|store: &mut SettingsStore, cx| {
767            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
768        });
769        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
770        cx.simulate_keystrokes("/ c c enter");
771
772        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
773
774        // n to go to next/N to go to previous
775        cx.simulate_keystrokes("n");
776        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
777        cx.simulate_keystrokes("shift-n");
778        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
779
780        // ?<enter> to go to previous
781        cx.simulate_keystrokes("? enter");
782        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
783        cx.simulate_keystrokes("? enter");
784        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
785    }
786
787    #[gpui::test]
788    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
789        let mut cx = VimTestContext::new(cx, false).await;
790        cx.cx.set_state("ˇone one one one");
791        cx.run_until_parked();
792        cx.simulate_keystrokes("cmd-f");
793        cx.run_until_parked();
794
795        cx.assert_editor_state("«oneˇ» one one one");
796        cx.simulate_keystrokes("enter");
797        cx.assert_editor_state("one «oneˇ» one one");
798        cx.simulate_keystrokes("shift-enter");
799        cx.assert_editor_state("«oneˇ» one one one");
800    }
801
802    #[gpui::test]
803    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
804        let mut cx = NeovimBackedTestContext::new(cx).await;
805
806        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
807        cx.simulate_shared_keystrokes("v 3 l *").await;
808        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
809    }
810
811    #[gpui::test]
812    async fn test_d_search(cx: &mut gpui::TestAppContext) {
813        let mut cx = NeovimBackedTestContext::new(cx).await;
814
815        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
816        cx.simulate_shared_keystrokes("d / c d").await;
817        cx.simulate_shared_keystrokes("enter").await;
818        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
819    }
820
821    #[gpui::test]
822    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
823        let mut cx = NeovimBackedTestContext::new(cx).await;
824
825        cx.set_shared_state("ˇa b a b a b a").await;
826        cx.simulate_shared_keystrokes("*").await;
827        cx.simulate_shared_keystrokes("n").await;
828        cx.shared_state().await.assert_eq("a b a b ˇa b a");
829        cx.simulate_shared_keystrokes("#").await;
830        cx.shared_state().await.assert_eq("a b ˇa b a b a");
831        cx.simulate_shared_keystrokes("n").await;
832        cx.shared_state().await.assert_eq("ˇa b a b a b a");
833    }
834
835    #[gpui::test]
836    async fn test_v_search(cx: &mut gpui::TestAppContext) {
837        let mut cx = NeovimBackedTestContext::new(cx).await;
838
839        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
840        cx.simulate_shared_keystrokes("v / c d").await;
841        cx.simulate_shared_keystrokes("enter").await;
842        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
843
844        cx.set_shared_state("a a aˇ a a a").await;
845        cx.simulate_shared_keystrokes("v / a").await;
846        cx.simulate_shared_keystrokes("enter").await;
847        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
848        cx.simulate_shared_keystrokes("/ enter").await;
849        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
850        cx.simulate_shared_keystrokes("? enter").await;
851        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
852        cx.simulate_shared_keystrokes("? enter").await;
853        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
854        cx.simulate_shared_keystrokes("/ enter").await;
855        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
856        cx.simulate_shared_keystrokes("/ enter").await;
857        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
858    }
859
860    #[gpui::test]
861    async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
862        let mut cx = NeovimBackedTestContext::new(cx).await;
863
864        cx.set_shared_state("ˇaa aa").await;
865        cx.simulate_shared_keystrokes("v / a a").await;
866        cx.simulate_shared_keystrokes("enter").await;
867        cx.shared_state().await.assert_eq("«aa aˇ»a");
868    }
869
870    #[gpui::test]
871    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
872        let mut cx = NeovimBackedTestContext::new(cx).await;
873
874        cx.set_shared_state(indoc! {
875            "ˇone two
876             three four
877             five six
878             "
879        })
880        .await;
881        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
882        cx.simulate_shared_keystrokes("enter").await;
883        cx.shared_state().await.assert_eq(indoc! {
884            "«one twoˇ»
885             «three fˇ»our
886             five six
887             "
888        });
889    }
890
891    // cargo test -p vim --features neovim test_replace_with_range_at_start
892    #[gpui::test]
893    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
894        let mut cx = NeovimBackedTestContext::new(cx).await;
895
896        cx.set_shared_state(indoc! {
897            "ˇa
898            a
899            a
900            a
901            a
902            a
903            a
904             "
905        })
906        .await;
907        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
908        cx.simulate_shared_keystrokes("enter").await;
909        cx.shared_state().await.assert_eq(indoc! {
910            "a
911            ba
912            ba
913            ba
914            ˇba
915            a
916            a
917             "
918        });
919
920        cx.simulate_shared_keystrokes("/ a").await;
921        cx.simulate_shared_keystrokes("enter").await;
922        cx.shared_state().await.assert_eq(indoc! {
923            "a
924                ba
925                ba
926                ba
927                bˇa
928                a
929                a
930                 "
931        });
932    }
933
934    #[gpui::test]
935    async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
936        let mut cx = NeovimBackedTestContext::new(cx).await;
937        cx.set_shared_state(indoc! {
938            "ˇaa aa aa"
939        })
940        .await;
941
942        cx.simulate_shared_keystrokes("/ a a").await;
943        cx.simulate_shared_keystrokes("enter").await;
944
945        cx.shared_state().await.assert_eq(indoc! {
946            "aa ˇaa aa"
947        });
948
949        cx.simulate_shared_keystrokes("left / a a").await;
950        cx.simulate_shared_keystrokes("enter").await;
951
952        cx.shared_state().await.assert_eq(indoc! {
953            "aa ˇaa aa"
954        });
955    }
956
957    #[gpui::test]
958    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
959        let mut cx = NeovimBackedTestContext::new(cx).await;
960
961        cx.set_shared_state(indoc! {
962            "ˇa
963            a
964            a
965            a
966            a
967            a
968            a
969             "
970        })
971        .await;
972        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
973        cx.simulate_shared_keystrokes("enter").await;
974        cx.shared_state().await.assert_eq(indoc! {
975            "a
976            b
977            b
978            b
979            ˇb
980            a
981            a
982             "
983        });
984        cx.executor().advance_clock(Duration::from_millis(250));
985        cx.run_until_parked();
986
987        cx.simulate_shared_keystrokes("/ a enter").await;
988        cx.shared_state().await.assert_eq(indoc! {
989            "a
990                b
991                b
992                b
993                b
994                ˇa
995                a
996                 "
997        });
998    }
999}