search.rs

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