search.rs

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