search.rs

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