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