Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:37:32.578Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

search.rs

1610 lines · 53.7 KB · rust
1use editor::{Editor, EditorSettings};
2use gpui::{Action, Context, TaskExt, Window, actions};
3use language::Point;
4use schemars::JsonSchema;
5use search::{BufferSearchBar, SearchOptions, buffer_search};
6use serde::Deserialize;
7use settings::Settings;
8use std::{iter::Peekable, str::Chars};
9use util::serde::default_true;
10use workspace::{notifications::NotifyResultExt, searchable::Direction};
11
12use crate::{
13    Vim, VimSettings,
14    command::CommandRange,
15    motion::Motion,
16    state::{Mode, SearchState},
17};
18
19/// Moves to the next search match.
20#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
21#[action(namespace = vim)]
22#[serde(deny_unknown_fields)]
23pub(crate) struct MoveToNext {
24    #[serde(default = "default_true")]
25    case_sensitive: bool,
26    #[serde(default)]
27    partial_word: bool,
28    #[serde(default = "default_true")]
29    regex: bool,
30}
31
32/// Moves to the previous search match.
33#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
34#[action(namespace = vim)]
35#[serde(deny_unknown_fields)]
36pub(crate) struct MoveToPrevious {
37    #[serde(default = "default_true")]
38    case_sensitive: bool,
39    #[serde(default)]
40    partial_word: bool,
41    #[serde(default = "default_true")]
42    regex: bool,
43}
44
45/// Searches for the word under the cursor without moving.
46#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
47#[action(namespace = vim)]
48#[serde(deny_unknown_fields)]
49pub(crate) struct SearchUnderCursor {
50    #[serde(default = "default_true")]
51    case_sensitive: bool,
52    #[serde(default)]
53    partial_word: bool,
54    #[serde(default = "default_true")]
55    regex: bool,
56}
57
58/// Searches for the word under the cursor without moving (backwards).
59#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
60#[action(namespace = vim)]
61#[serde(deny_unknown_fields)]
62pub(crate) struct SearchUnderCursorPrevious {
63    #[serde(default = "default_true")]
64    case_sensitive: bool,
65    #[serde(default)]
66    partial_word: bool,
67    #[serde(default = "default_true")]
68    regex: bool,
69}
70
71/// Initiates a search operation with the specified parameters.
72#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
73#[action(namespace = vim)]
74#[serde(deny_unknown_fields)]
75pub(crate) struct Search {
76    #[serde(default)]
77    backwards: bool,
78    #[serde(default = "default_true")]
79    regex: bool,
80}
81
82/// Executes a find command to search for patterns in the buffer.
83#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
84#[action(namespace = vim)]
85#[serde(deny_unknown_fields)]
86pub struct FindCommand {
87    pub query: String,
88    pub backwards: bool,
89}
90
91/// Executes a search and replace command within the specified range.
92#[derive(Clone, Debug, PartialEq, Action)]
93#[action(namespace = vim, no_json, no_register)]
94pub struct ReplaceCommand {
95    pub(crate) range: CommandRange,
96    pub(crate) replacement: Replacement,
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub struct Replacement {
101    search: String,
102    replacement: String,
103    case_sensitive: Option<bool>,
104    flag_n: bool,
105    flag_g: bool,
106    flag_c: bool,
107}
108
109actions!(
110    vim,
111    [
112        /// Submits the current search query.
113        SearchSubmit,
114        /// Moves to the next search match.
115        MoveToNextMatch,
116        /// Moves to the previous search match.
117        MoveToPreviousMatch
118    ]
119);
120
121pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
122    Vim::action(editor, cx, Vim::move_to_next);
123    Vim::action(editor, cx, Vim::move_to_previous);
124    Vim::action(editor, cx, Vim::search_under_cursor);
125    Vim::action(editor, cx, Vim::search_under_cursor_previous);
126    Vim::action(editor, cx, Vim::move_to_next_match);
127    Vim::action(editor, cx, Vim::move_to_previous_match);
128    Vim::action(editor, cx, Vim::search);
129    Vim::action(editor, cx, Vim::search_deploy);
130    Vim::action(editor, cx, Vim::find_command);
131    Vim::action(editor, cx, Vim::replace_command);
132}
133
134impl Vim {
135    fn search_under_cursor(
136        &mut self,
137        action: &SearchUnderCursor,
138        window: &mut Window,
139        cx: &mut Context<Self>,
140    ) {
141        self.move_to_internal(
142            Direction::Next,
143            action.case_sensitive,
144            !action.partial_word,
145            action.regex,
146            false,
147            window,
148            cx,
149        )
150    }
151
152    fn search_under_cursor_previous(
153        &mut self,
154        action: &SearchUnderCursorPrevious,
155        window: &mut Window,
156        cx: &mut Context<Self>,
157    ) {
158        self.move_to_internal(
159            Direction::Prev,
160            action.case_sensitive,
161            !action.partial_word,
162            action.regex,
163            false,
164            window,
165            cx,
166        )
167    }
168
169    fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context<Self>) {
170        self.move_to_internal(
171            Direction::Next,
172            action.case_sensitive,
173            !action.partial_word,
174            action.regex,
175            true,
176            window,
177            cx,
178        )
179    }
180
181    fn move_to_previous(
182        &mut self,
183        action: &MoveToPrevious,
184        window: &mut Window,
185        cx: &mut Context<Self>,
186    ) {
187        self.move_to_internal(
188            Direction::Prev,
189            action.case_sensitive,
190            !action.partial_word,
191            action.regex,
192            true,
193            window,
194            cx,
195        )
196    }
197
198    fn move_to_next_match(
199        &mut self,
200        _: &MoveToNextMatch,
201        window: &mut Window,
202        cx: &mut Context<Self>,
203    ) {
204        self.move_to_match_internal(self.search.direction, window, cx)
205    }
206
207    fn move_to_previous_match(
208        &mut self,
209        _: &MoveToPreviousMatch,
210        window: &mut Window,
211        cx: &mut Context<Self>,
212    ) {
213        self.move_to_match_internal(self.search.direction.opposite(), window, cx)
214    }
215
216    fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context<Self>) {
217        let Some(pane) = self.pane(window, cx) else {
218            return;
219        };
220        let direction = if action.backwards {
221            Direction::Prev
222        } else {
223            Direction::Next
224        };
225        let count = Vim::take_count(cx).unwrap_or(1);
226        Vim::take_forced_motion(cx);
227        let prior_selections = self.editor_selections(window, cx);
228
229        let Some(search_bar) = pane
230            .read(cx)
231            .toolbar()
232            .read(cx)
233            .item_of_type::<BufferSearchBar>()
234        else {
235            return;
236        };
237
238        let shown = search_bar.update(cx, |search_bar, cx| {
239            if !search_bar.show(window, cx) {
240                return false;
241            }
242
243            search_bar.select_query(window, cx);
244            cx.focus_self(window);
245
246            search_bar.set_replacement(None, cx);
247            let mut options = SearchOptions::NONE;
248            if action.regex && VimSettings::get_global(cx).use_regex_search {
249                options |= SearchOptions::REGEX;
250            }
251            if action.backwards {
252                options |= SearchOptions::BACKWARDS;
253            }
254            if EditorSettings::get_global(cx).search.case_sensitive {
255                options |= SearchOptions::CASE_SENSITIVE;
256            }
257            search_bar.set_search_options(options, cx);
258            true
259        });
260
261        if !shown {
262            return;
263        }
264
265        let subscription = cx.subscribe_in(&search_bar, window, |vim, _, event, window, cx| {
266            if let buffer_search::Event::Dismissed = event {
267                if !vim.search.prior_selections.is_empty() {
268                    let prior_selections: Vec<_> = vim.search.prior_selections.drain(..).collect();
269                    vim.update_editor(cx, |_, editor, cx| {
270                        editor.change_selections(Default::default(), window, cx, |s| {
271                            s.select_ranges(prior_selections);
272                        });
273                    });
274                }
275            }
276        });
277
278        let prior_mode = if self.temp_mode {
279            Mode::Insert
280        } else {
281            self.mode
282        };
283
284        self.search = SearchState {
285            direction,
286            count,
287            cmd_f_search: false,
288            prior_selections,
289            prior_operator: self.operator_stack.last().cloned(),
290            prior_mode,
291            helix_select: false,
292            _dismiss_subscription: Some(subscription),
293        }
294    }
295
296    // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
297    fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
298        // Preserve the current mode when resetting search state
299        let current_mode = self.mode;
300        self.search = Default::default();
301        self.search.prior_mode = current_mode;
302        self.search.cmd_f_search = true;
303        cx.propagate();
304    }
305
306    pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
307        self.store_visual_marks(window, cx);
308        let Some(pane) = self.pane(window, cx) else {
309            return;
310        };
311        let new_selections = self.editor_selections(window, cx);
312        let result = pane.update(cx, |pane, cx| {
313            let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
314            if self.search.helix_select {
315                search_bar.update(cx, |search_bar, cx| {
316                    search_bar.select_all_matches(&Default::default(), window, cx)
317                });
318                return None;
319            }
320            search_bar.update(cx, |search_bar, cx| {
321                let mut count = self.search.count;
322                let direction = self.search.direction;
323                search_bar.has_active_match();
324                let new_head = new_selections.last()?.start;
325                let is_different_head = self
326                    .search
327                    .prior_selections
328                    .last()
329                    .is_none_or(|range| range.start != new_head);
330
331                if is_different_head {
332                    count = count.saturating_sub(1)
333                }
334                self.search.count = 1;
335                search_bar.select_match(direction, count, window, cx);
336                search_bar.focus_editor(&Default::default(), window, cx);
337
338                let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
339                let prior_mode = self.search.prior_mode;
340                let prior_operator = self.search.prior_operator.take();
341
342                let query = search_bar.query(cx).into();
343                Vim::globals(cx).registers.insert('/', query);
344                Some((prior_selections, prior_mode, prior_operator))
345            })
346        });
347
348        let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
349            return;
350        };
351
352        let new_selections = self.editor_selections(window, cx);
353
354        // If the active editor has changed during a search, don't panic.
355        if prior_selections.iter().any(|s| {
356            self.update_editor(cx, |_, editor, cx| {
357                !s.start
358                    .is_valid(&editor.snapshot(window, cx).buffer_snapshot())
359            })
360            .unwrap_or(true)
361        }) {
362            prior_selections.clear();
363        }
364
365        if prior_mode != self.mode {
366            self.switch_mode(prior_mode, true, window, cx);
367        }
368        if let Some(operator) = prior_operator {
369            self.push_operator(operator, window, cx);
370        };
371        self.search_motion(
372            Motion::ZedSearchResult {
373                prior_selections,
374                new_selections,
375            },
376            window,
377            cx,
378        );
379    }
380
381    pub fn move_to_match_internal(
382        &mut self,
383        direction: Direction,
384        window: &mut Window,
385        cx: &mut Context<Self>,
386    ) {
387        let Some(pane) = self.pane(window, cx) else {
388            return;
389        };
390        let count = Vim::take_count(cx).unwrap_or(1);
391        Vim::take_forced_motion(cx);
392
393        if self.search.cmd_f_search {
394            self.search.cmd_f_search = false;
395            if self.mode.is_visual() {
396                self.switch_mode(Mode::Normal, false, window, cx);
397            }
398            self.sync_vim_settings(window, cx);
399        }
400
401        let prior_selections = self.editor_selections(window, cx);
402
403        let success = pane.update(cx, |pane, cx| {
404            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
405                return false;
406            };
407            search_bar.update(cx, |search_bar, cx| {
408                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
409                    return false;
410                }
411                search_bar.select_match(direction, count, window, cx);
412                true
413            })
414        });
415        if !success {
416            return;
417        }
418
419        let new_selections = self.editor_selections(window, cx);
420        self.search_motion(
421            Motion::ZedSearchResult {
422                prior_selections,
423                new_selections,
424            },
425            window,
426            cx,
427        );
428    }
429
430    pub fn move_to_internal(
431        &mut self,
432        direction: Direction,
433        case_sensitive: bool,
434        whole_word: bool,
435        regex: bool,
436        move_cursor: bool,
437        window: &mut Window,
438        cx: &mut Context<Self>,
439    ) {
440        let Some(pane) = self.pane(window, cx) else {
441            return;
442        };
443        let count = Vim::take_count(cx).unwrap_or(1);
444        Vim::take_forced_motion(cx);
445
446        if self.search.cmd_f_search {
447            self.search.cmd_f_search = false;
448            if self.mode.is_visual() {
449                self.switch_mode(Mode::Normal, false, window, cx);
450            }
451            self.sync_vim_settings(window, cx);
452        }
453
454        let prior_selections = self.editor_selections(window, cx);
455        let vim = cx.entity();
456
457        let searched = pane.update(cx, |pane, cx| {
458            self.search.direction = direction;
459            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
460                return false;
461            };
462            let search = search_bar.update(cx, |search_bar, cx| {
463                let mut options = SearchOptions::NONE;
464                if case_sensitive {
465                    options |= SearchOptions::CASE_SENSITIVE;
466                }
467                if regex {
468                    options |= SearchOptions::REGEX;
469                }
470                if whole_word {
471                    options |= SearchOptions::WHOLE_WORD;
472                }
473                if !search_bar.show(window, cx) {
474                    return None;
475                }
476                let Some(query) = search_bar.query_suggestion(
477                    Some(settings::SeedQuerySetting::Always),
478                    window,
479                    cx,
480                ) else {
481                    drop(search_bar.search("", None, false, window, cx));
482                    return None;
483                };
484
485                let query = regex::escape(&query);
486                Some(search_bar.search(&query, Some(options), true, window, cx))
487            });
488
489            let Some(search) = search else { return false };
490
491            if move_cursor {
492                let search_bar = search_bar.downgrade();
493                cx.spawn_in(window, async move |_, cx| {
494                    search.await?;
495                    search_bar.update_in(cx, |search_bar, window, cx| {
496                        search_bar.select_match(direction, count, window, cx);
497
498                        vim.update(cx, |vim, cx| {
499                            let new_selections = vim.editor_selections(window, cx);
500                            vim.search_motion(
501                                Motion::ZedSearchResult {
502                                    prior_selections,
503                                    new_selections,
504                                },
505                                window,
506                                cx,
507                            )
508                        });
509                    })?;
510                    anyhow::Ok(())
511                })
512                .detach_and_log_err(cx);
513            }
514            true
515        });
516        if !searched {
517            self.clear_operator(window, cx)
518        }
519
520        if self.mode.is_visual() {
521            self.switch_mode(Mode::Normal, false, window, cx)
522        }
523    }
524
525    fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
526        let Some(pane) = self.pane(window, cx) else {
527            return;
528        };
529        pane.update(cx, |pane, cx| {
530            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
531                let search = search_bar.update(cx, |search_bar, cx| {
532                    if !search_bar.show(window, cx) {
533                        return None;
534                    }
535                    let mut query = action.query.clone();
536                    if query.is_empty() {
537                        query = search_bar.query(cx);
538                    };
539
540                    let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
541                    if search_bar.should_use_smartcase_search(cx) {
542                        options.set(
543                            SearchOptions::CASE_SENSITIVE,
544                            search_bar.is_contains_uppercase(&query),
545                        );
546                    }
547
548                    Some(search_bar.search(&query, Some(options), true, window, cx))
549                });
550                let Some(search) = search else { return };
551                let search_bar = search_bar.downgrade();
552                let direction = if action.backwards {
553                    Direction::Prev
554                } else {
555                    Direction::Next
556                };
557                cx.spawn_in(window, async move |_, cx| {
558                    search.await?;
559                    search_bar.update_in(cx, |search_bar, window, cx| {
560                        search_bar.select_match(direction, 1, window, cx)
561                    })?;
562                    anyhow::Ok(())
563                })
564                .detach_and_log_err(cx);
565            }
566        })
567    }
568
569    fn replace_command(
570        &mut self,
571        action: &ReplaceCommand,
572        window: &mut Window,
573        cx: &mut Context<Self>,
574    ) {
575        let replacement = action.replacement.clone();
576        let Some(((pane, workspace), editor)) = self
577            .pane(window, cx)
578            .zip(self.workspace(window, cx))
579            .zip(self.editor())
580        else {
581            return;
582        };
583        if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
584            let range = action.range.buffer_range(vim, editor, window, cx)?;
585            let snapshot = editor.snapshot(window, cx);
586            let snapshot = snapshot.buffer_snapshot();
587            let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
588            let range = snapshot.anchor_before(Point::new(range.start.0, 0))
589                ..snapshot.anchor_after(end_point);
590            editor.set_search_within_ranges(&[range], cx);
591            anyhow::Ok(())
592        }) {
593            workspace.update(cx, |workspace, cx| {
594                result.notify_err(workspace, cx);
595            })
596        }
597        let Some(search_bar) = pane.update(cx, |pane, cx| {
598            pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
599        }) else {
600            return;
601        };
602        let mut options = SearchOptions::REGEX;
603        let search = search_bar.update(cx, |search_bar, cx| {
604            if !search_bar.show(window, cx) {
605                return None;
606            }
607
608            let search = if replacement.search.is_empty() {
609                search_bar.query(cx)
610            } else {
611                replacement.search
612            };
613
614            if let Some(case) = replacement.case_sensitive {
615                options.set(SearchOptions::CASE_SENSITIVE, case)
616            } else if search_bar.should_use_smartcase_search(cx) {
617                options.set(
618                    SearchOptions::CASE_SENSITIVE,
619                    search_bar.is_contains_uppercase(&search),
620                );
621            } else {
622                // Fallback: no explicit i/I flags and smartcase disabled;
623                // use global editor.search.case_sensitive.
624                options.set(
625                    SearchOptions::CASE_SENSITIVE,
626                    EditorSettings::get_global(cx).search.case_sensitive,
627                )
628            }
629
630            // gdefault inverts the behavior of the 'g' flag.
631            let replace_all = VimSettings::get_global(cx).gdefault != replacement.flag_g;
632            if !replace_all {
633                options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
634            }
635
636            search_bar.set_replacement(Some(&replacement.replacement), cx);
637            if replacement.flag_c {
638                search_bar.focus_replace(window, cx);
639            }
640            Some(search_bar.search(&search, Some(options), true, window, cx))
641        });
642        if replacement.flag_n {
643            self.move_cursor(
644                Motion::StartOfLine {
645                    display_lines: false,
646                },
647                None,
648                window,
649                cx,
650            );
651            return;
652        }
653        let Some(search) = search else { return };
654        let search_bar = search_bar.downgrade();
655        cx.spawn_in(window, async move |vim, cx| {
656            search.await?;
657            search_bar.update_in(cx, |search_bar, window, cx| {
658                if replacement.flag_c {
659                    search_bar.select_first_match(window, cx);
660                    return;
661                }
662                search_bar.select_last_match(window, cx);
663                search_bar.replace_all(&Default::default(), window, cx);
664                editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
665                let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
666                vim.update(cx, |vim, cx| {
667                    vim.move_cursor(
668                        Motion::StartOfLine {
669                            display_lines: false,
670                        },
671                        None,
672                        window,
673                        cx,
674                    )
675                })
676                .ok();
677
678                // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
679                // this is not properly supported outside of vim mode, and
680                // not disabling it makes the "Replace All Matches" button
681                // actually replace only the first match on each line.
682                options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
683                search_bar.set_search_options(options, cx);
684            })
685        })
686        .detach_and_log_err(cx);
687    }
688}
689
690impl Replacement {
691    // convert a vim query into something more usable by zed.
692    // we don't attempt to fully convert between the two regex syntaxes,
693    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
694    // convert \0..\9 to $0..$9 in the replacement so that common idioms work,
695    // and escape literal `$` to `$$` in the replacement so vim's literal `$`
696    // is not interpreted as a Rust regex capture-group reference.
697    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
698        let delimiter = chars
699            .next()
700            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
701
702        let mut search = String::new();
703        let mut replacement = String::new();
704        let mut flags = String::new();
705
706        let mut buffer = &mut search;
707
708        let mut escaped = false;
709        // 0 - parsing search
710        // 1 - parsing replacement
711        // 2 - parsing flags
712        let mut phase = 0;
713
714        for c in chars {
715            if escaped {
716                escaped = false;
717                if phase == 1 && c.is_ascii_digit() {
718                    buffer.push('$')
719                } else if phase == 1 && c == '$' {
720                    // Second '$' escapes by fallthrough
721                    buffer.push('$')
722                // unescape escaped parens
723                } else if phase == 0 && (c == '(' || c == ')') {
724                } else if c != delimiter {
725                    buffer.push('\\')
726                }
727                buffer.push(c)
728            } else if c == '\\' {
729                escaped = true;
730            } else if c == delimiter {
731                if phase == 0 {
732                    buffer = &mut replacement;
733                    phase = 1;
734                } else if phase == 1 {
735                    buffer = &mut flags;
736                    phase = 2;
737                } else {
738                    break;
739                }
740            } else {
741                // escape unescaped parens
742                if phase == 0 && (c == '(' || c == ')') {
743                    buffer.push('\\')
744                } else if phase == 1 && c == '$' {
745                    // '$' is not special in the replacement clause,
746                    // so we also escape here.
747                    buffer.push('$')
748                }
749                buffer.push(c)
750            }
751        }
752
753        let mut replacement = Replacement {
754            search,
755            replacement,
756            case_sensitive: None,
757            flag_g: false,
758            flag_n: false,
759            flag_c: false,
760        };
761
762        for c in flags.chars() {
763            match c {
764                'g' => replacement.flag_g = !replacement.flag_g,
765                'n' => replacement.flag_n = true,
766                'c' => replacement.flag_c = true,
767                'i' => replacement.case_sensitive = Some(false),
768                'I' => replacement.case_sensitive = Some(true),
769                _ => {}
770            }
771        }
772
773        Some(replacement)
774    }
775}
776
777#[cfg(test)]
778mod test {
779    use std::time::Duration;
780
781    use crate::{
782        state::Mode,
783        test::{NeovimBackedTestContext, VimTestContext},
784    };
785    use editor::{DisplayPoint, display_map::DisplayRow};
786
787    use indoc::indoc;
788    use search::BufferSearchBar;
789    use settings::SettingsStore;
790
791    #[test]
792    fn test_replacement_parse_escaped_dollar() {
793        let parsed = super::Replacement::parse(r"/\$test/\$rest/g".chars().peekable())
794            .expect("parse should succeed");
795
796        assert_eq!(parsed.search, r"\$test");
797        assert_eq!(parsed.replacement, "$$rest");
798        assert!(parsed.flag_g);
799    }
800
801    #[gpui::test]
802    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
803        let mut cx = VimTestContext::new(cx, true).await;
804        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
805
806        cx.simulate_keystrokes("*");
807        cx.run_until_parked();
808        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
809
810        cx.simulate_keystrokes("*");
811        cx.run_until_parked();
812        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
813
814        cx.simulate_keystrokes("#");
815        cx.run_until_parked();
816        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
817
818        cx.simulate_keystrokes("#");
819        cx.run_until_parked();
820        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
821
822        cx.simulate_keystrokes("2 *");
823        cx.run_until_parked();
824        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
825
826        cx.simulate_keystrokes("g *");
827        cx.run_until_parked();
828        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
829
830        cx.simulate_keystrokes("n");
831        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
832
833        cx.simulate_keystrokes("g #");
834        cx.run_until_parked();
835        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
836    }
837
838    #[gpui::test]
839    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
840        let mut cx = VimTestContext::new(cx, true).await;
841
842        cx.update_global(|store: &mut SettingsStore, cx| {
843            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
844        });
845
846        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
847
848        cx.simulate_keystrokes("*");
849        cx.run_until_parked();
850        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
851
852        cx.simulate_keystrokes("*");
853        cx.run_until_parked();
854        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
855
856        cx.simulate_keystrokes("#");
857        cx.run_until_parked();
858        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
859
860        cx.simulate_keystrokes("3 *");
861        cx.run_until_parked();
862        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
863
864        cx.simulate_keystrokes("g *");
865        cx.run_until_parked();
866        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
867
868        cx.simulate_keystrokes("n");
869        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
870
871        cx.simulate_keystrokes("g #");
872        cx.run_until_parked();
873        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
874    }
875
876    #[gpui::test]
877    async fn test_search(cx: &mut gpui::TestAppContext) {
878        let mut cx = VimTestContext::new(cx, true).await;
879
880        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
881        cx.simulate_keystrokes("/ c c");
882
883        let search_bar = cx.workspace(|workspace, _, cx| {
884            workspace
885                .active_pane()
886                .read(cx)
887                .toolbar()
888                .read(cx)
889                .item_of_type::<BufferSearchBar>()
890                .expect("Buffer search bar should be deployed")
891        });
892
893        cx.update_entity(search_bar, |bar, _window, cx| {
894            assert_eq!(bar.query(cx), "cc");
895        });
896
897        cx.run_until_parked();
898
899        cx.update_editor(|editor, window, cx| {
900            let highlights = editor.all_text_background_highlights(window, cx);
901            assert_eq!(3, highlights.len());
902            assert_eq!(
903                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
904                highlights[0].0
905            )
906        });
907
908        cx.simulate_keystrokes("enter");
909        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
910
911        // n to go to next/N to go to previous
912        cx.simulate_keystrokes("n");
913        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
914        cx.simulate_keystrokes("shift-n");
915        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
916
917        // ?<enter> to go to previous
918        cx.simulate_keystrokes("? enter");
919        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
920        cx.simulate_keystrokes("? enter");
921        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
922
923        // /<enter> to go to next
924        cx.simulate_keystrokes("/ enter");
925        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
926
927        // ?{search}<enter> to search backwards
928        cx.simulate_keystrokes("? b enter");
929        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
930
931        // works with counts
932        cx.simulate_keystrokes("4 / c");
933        cx.simulate_keystrokes("enter");
934        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
935
936        // check that searching resumes from cursor, not previous match
937        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
938        cx.simulate_keystrokes("/ d");
939        cx.simulate_keystrokes("enter");
940        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
941        cx.update_editor(|editor, window, cx| {
942            editor.move_to_beginning(&Default::default(), window, cx)
943        });
944        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
945        cx.simulate_keystrokes("/ b");
946        cx.simulate_keystrokes("enter");
947        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
948
949        // check that searching switches to normal mode if in visual mode
950        cx.set_state("ˇone two one", Mode::Normal);
951        cx.simulate_keystrokes("v l l");
952        cx.assert_editor_state("«oneˇ» two one");
953        cx.simulate_keystrokes("*");
954        cx.assert_state("one two ˇone", Mode::Normal);
955
956        // check that a backward search after last match works correctly
957        cx.set_state("aa\naa\nbbˇ", Mode::Normal);
958        cx.simulate_keystrokes("? a a");
959        cx.simulate_keystrokes("enter");
960        cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
961
962        // check that searching with unable search wrap
963        cx.update_global(|store: &mut SettingsStore, cx| {
964            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
965        });
966        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
967        cx.simulate_keystrokes("/ c c enter");
968
969        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
970
971        // n to go to next/N to go to previous
972        cx.simulate_keystrokes("n");
973        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
974        cx.simulate_keystrokes("shift-n");
975        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
976
977        // ?<enter> to go to previous
978        cx.simulate_keystrokes("? enter");
979        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
980        cx.simulate_keystrokes("? enter");
981        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
982    }
983
984    #[gpui::test]
985    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
986        let mut cx = VimTestContext::new(cx, false).await;
987        cx.cx.set_state("ˇone one one one");
988        cx.run_until_parked();
989        cx.simulate_keystrokes("cmd-f");
990        cx.run_until_parked();
991
992        cx.assert_editor_state("«oneˇ» one one one");
993        cx.simulate_keystrokes("enter");
994        cx.assert_editor_state("one «oneˇ» one one");
995        cx.simulate_keystrokes("shift-enter");
996        cx.assert_editor_state("«oneˇ» one one one");
997    }
998
999    #[gpui::test]
1000    async fn test_non_vim_search_in_vim_mode(cx: &mut gpui::TestAppContext) {
1001        let mut cx = VimTestContext::new(cx, true).await;
1002        cx.cx.set_state("ˇone one one one");
1003        cx.run_until_parked();
1004        cx.simulate_keystrokes("cmd-f");
1005        cx.run_until_parked();
1006
1007        cx.assert_state("«oneˇ» one one one", Mode::Visual);
1008        cx.simulate_keystrokes("enter");
1009        cx.run_until_parked();
1010        cx.assert_state("one «oneˇ» one one", Mode::Visual);
1011        cx.simulate_keystrokes("shift-enter");
1012        cx.run_until_parked();
1013        cx.assert_state("«oneˇ» one one one", Mode::Visual);
1014
1015        cx.simulate_keystrokes("escape");
1016        cx.run_until_parked();
1017        cx.assert_state("«oneˇ» one one one", Mode::Visual);
1018    }
1019
1020    #[gpui::test]
1021    async fn test_non_vim_search_in_vim_insert_mode(cx: &mut gpui::TestAppContext) {
1022        let mut cx = VimTestContext::new(cx, true).await;
1023        cx.set_state("ˇone one one one", Mode::Insert);
1024        cx.run_until_parked();
1025        cx.simulate_keystrokes("cmd-f");
1026        cx.run_until_parked();
1027
1028        cx.assert_state("«oneˇ» one one one", Mode::Insert);
1029        cx.simulate_keystrokes("enter");
1030        cx.run_until_parked();
1031        cx.assert_state("one «oneˇ» one one", Mode::Insert);
1032
1033        cx.simulate_keystrokes("escape");
1034        cx.run_until_parked();
1035        cx.assert_state("one «oneˇ» one one", Mode::Insert);
1036    }
1037
1038    #[gpui::test]
1039    async fn test_n_after_cmd_f_search(cx: &mut gpui::TestAppContext) {
1040        let mut cx = VimTestContext::new(cx, true).await;
1041        cx.set_state("ˇone two one two one", Mode::Normal);
1042        cx.run_until_parked();
1043
1044        // Use cmd+f to search (non-vim style)
1045        cx.simulate_keystrokes("cmd-f");
1046        cx.run_until_parked();
1047        cx.simulate_keystrokes("escape");
1048        cx.run_until_parked();
1049
1050        // Now use n to go to next match — should move cursor, not create selection
1051        cx.simulate_keystrokes("n");
1052        cx.run_until_parked();
1053        cx.assert_state("one two ˇone two one", Mode::Normal);
1054
1055        cx.simulate_keystrokes("n");
1056        cx.run_until_parked();
1057        cx.assert_state("one two one two ˇone", Mode::Normal);
1058    }
1059
1060    #[gpui::test]
1061    async fn test_star_after_cmd_f_search(cx: &mut gpui::TestAppContext) {
1062        let mut cx = VimTestContext::new(cx, true).await;
1063        cx.set_state("ˇone two one two one", Mode::Normal);
1064        cx.run_until_parked();
1065
1066        // Use cmd+f to search (non-vim style)
1067        cx.simulate_keystrokes("cmd-f");
1068        cx.run_until_parked();
1069        cx.simulate_keystrokes("escape");
1070        cx.run_until_parked();
1071
1072        // Now use * to search under cursor — should move cursor, not create selection
1073        cx.simulate_keystrokes("*");
1074        cx.run_until_parked();
1075        cx.assert_state("one two ˇone two one", Mode::Normal);
1076    }
1077
1078    #[gpui::test]
1079    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
1080        let mut cx = NeovimBackedTestContext::new(cx).await;
1081
1082        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1083        cx.simulate_shared_keystrokes("v 3 l *").await;
1084        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
1085    }
1086
1087    #[gpui::test]
1088    async fn test_d_search(cx: &mut gpui::TestAppContext) {
1089        let mut cx = NeovimBackedTestContext::new(cx).await;
1090
1091        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1092        cx.simulate_shared_keystrokes("d / c d").await;
1093        cx.simulate_shared_keystrokes("enter").await;
1094        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
1095    }
1096
1097    #[gpui::test]
1098    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
1099        let mut cx = NeovimBackedTestContext::new(cx).await;
1100
1101        cx.set_shared_state("ˇa b a b a b a").await;
1102        cx.simulate_shared_keystrokes("*").await;
1103        cx.simulate_shared_keystrokes("n").await;
1104        cx.shared_state().await.assert_eq("a b a b ˇa b a");
1105        cx.simulate_shared_keystrokes("#").await;
1106        cx.shared_state().await.assert_eq("a b ˇa b a b a");
1107        cx.simulate_shared_keystrokes("n").await;
1108        cx.shared_state().await.assert_eq("ˇa b a b a b a");
1109    }
1110
1111    #[gpui::test]
1112    async fn test_v_search(cx: &mut gpui::TestAppContext) {
1113        let mut cx = NeovimBackedTestContext::new(cx).await;
1114
1115        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1116        cx.simulate_shared_keystrokes("v / c d").await;
1117        cx.simulate_shared_keystrokes("enter").await;
1118        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
1119
1120        cx.set_shared_state("a a aˇ a a a").await;
1121        cx.simulate_shared_keystrokes("v / a").await;
1122        cx.simulate_shared_keystrokes("enter").await;
1123        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1124        cx.simulate_shared_keystrokes("/ enter").await;
1125        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1126        cx.simulate_shared_keystrokes("? enter").await;
1127        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1128        cx.simulate_shared_keystrokes("? enter").await;
1129        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
1130        cx.simulate_shared_keystrokes("/ enter").await;
1131        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1132        cx.simulate_shared_keystrokes("/ enter").await;
1133        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1134    }
1135
1136    #[gpui::test]
1137    async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
1138        let mut cx = NeovimBackedTestContext::new(cx).await;
1139
1140        cx.set_shared_state("ˇaa aa").await;
1141        cx.simulate_shared_keystrokes("v / a a").await;
1142        cx.simulate_shared_keystrokes("enter").await;
1143        cx.shared_state().await.assert_eq("«aa aˇ»a");
1144    }
1145
1146    #[gpui::test]
1147    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
1148        let mut cx = NeovimBackedTestContext::new(cx).await;
1149
1150        cx.set_shared_state(indoc! {
1151            "ˇone two
1152             three four
1153             five six
1154             "
1155        })
1156        .await;
1157        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
1158        cx.simulate_shared_keystrokes("enter").await;
1159        cx.shared_state().await.assert_eq(indoc! {
1160            "«one twoˇ»
1161             «three fˇ»our
1162             five six
1163             "
1164        });
1165    }
1166
1167    #[gpui::test]
1168    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
1169        let mut cx = NeovimBackedTestContext::new(cx).await;
1170
1171        cx.set_shared_state(indoc! {
1172            "ˇa
1173            a
1174            a
1175            a
1176            a
1177            a
1178            a
1179             "
1180        })
1181        .await;
1182        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
1183        cx.simulate_shared_keystrokes("enter").await;
1184        cx.shared_state().await.assert_eq(indoc! {
1185            "a
1186            ba
1187            ba
1188            ba
1189            ˇba
1190            a
1191            a
1192             "
1193        });
1194
1195        cx.simulate_shared_keystrokes("/ a").await;
1196        cx.simulate_shared_keystrokes("enter").await;
1197        cx.shared_state().await.assert_eq(indoc! {
1198            "a
1199                ba
1200                ba
1201                ba
1202                bˇa
1203                a
1204                a
1205                 "
1206        });
1207    }
1208
1209    #[gpui::test]
1210    async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
1211        let mut cx = NeovimBackedTestContext::new(cx).await;
1212        cx.set_shared_state(indoc! {
1213            "ˇaa aa aa"
1214        })
1215        .await;
1216
1217        cx.simulate_shared_keystrokes("/ a a").await;
1218        cx.simulate_shared_keystrokes("enter").await;
1219
1220        cx.shared_state().await.assert_eq(indoc! {
1221            "aa ˇaa aa"
1222        });
1223
1224        cx.simulate_shared_keystrokes("left / a a").await;
1225        cx.simulate_shared_keystrokes("enter").await;
1226
1227        cx.shared_state().await.assert_eq(indoc! {
1228            "aa ˇaa aa"
1229        });
1230    }
1231
1232    #[gpui::test]
1233    async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1234        let mut cx = NeovimBackedTestContext::new(cx).await;
1235        cx.set_shared_state(indoc! {
1236            "ˇaa
1237            bb
1238            aa"
1239        })
1240        .await;
1241
1242        cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1243        cx.simulate_shared_keystrokes("enter").await;
1244
1245        cx.shared_state().await.assert_eq(indoc! {
1246            "ˇaa
1247            bb
1248            aa"
1249        });
1250
1251        let search_bar = cx.update_workspace(|workspace, _, cx| {
1252            workspace.active_pane().update(cx, |pane, cx| {
1253                pane.toolbar()
1254                    .read(cx)
1255                    .item_of_type::<BufferSearchBar>()
1256                    .unwrap()
1257            })
1258        });
1259        cx.update_entity(search_bar, |search_bar, _, cx| {
1260            assert!(!search_bar.is_dismissed());
1261            assert_eq!(search_bar.query(cx), "bb".to_string());
1262            assert_eq!(search_bar.replacement(cx), "dd".to_string());
1263        })
1264    }
1265
1266    #[gpui::test]
1267    async fn test_replace_literal_dollar(cx: &mut gpui::TestAppContext) {
1268        let mut cx = NeovimBackedTestContext::new(cx).await;
1269        cx.set_shared_state(indoc! {
1270            "ˇBase=hello
1271            echo $Base"
1272        })
1273        .await;
1274
1275        cx.simulate_shared_keystrokes(
1276            ": % s / \\ $ shift-b a s e / \\ $ shift-b a s e shift-n e w / g",
1277        )
1278        .await;
1279        cx.simulate_shared_keystrokes("enter").await;
1280
1281        cx.shared_state().await.assert_eq(indoc! {
1282            "Base=hello
1283            ˇecho $BaseNew"
1284        });
1285    }
1286
1287    #[gpui::test]
1288    async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1289        let mut cx = NeovimBackedTestContext::new(cx).await;
1290        cx.set_shared_state(indoc! {
1291            "ˇaa aa aa aa
1292            aa
1293            aa"
1294        })
1295        .await;
1296
1297        cx.simulate_shared_keystrokes(": s / a a / b b").await;
1298        cx.simulate_shared_keystrokes("enter").await;
1299        cx.shared_state().await.assert_eq(indoc! {
1300            "ˇbb aa aa aa
1301            aa
1302            aa"
1303        });
1304        cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1305        cx.simulate_shared_keystrokes("enter").await;
1306        cx.shared_state().await.assert_eq(indoc! {
1307            "ˇbb bb bb bb
1308            aa
1309            aa"
1310        });
1311    }
1312
1313    #[gpui::test]
1314    async fn test_replace_gdefault(cx: &mut gpui::TestAppContext) {
1315        let mut cx = NeovimBackedTestContext::new(cx).await;
1316
1317        // Set the `gdefault` option in both Zed and Neovim.
1318        cx.simulate_shared_keystrokes(": s e t space g d e f a u l t")
1319            .await;
1320        cx.simulate_shared_keystrokes("enter").await;
1321
1322        cx.set_shared_state(indoc! {
1323            "ˇaa aa aa aa
1324                aa
1325                aa"
1326        })
1327        .await;
1328
1329        // With gdefault on, :s/// replaces all matches (like :s///g normally).
1330        cx.simulate_shared_keystrokes(": s / a a / b b").await;
1331        cx.simulate_shared_keystrokes("enter").await;
1332        cx.shared_state().await.assert_eq(indoc! {
1333            "ˇbb bb bb bb
1334                aa
1335                aa"
1336        });
1337
1338        // With gdefault on, :s///g replaces only the first match.
1339        cx.simulate_shared_keystrokes(": s / b b / c c / g").await;
1340        cx.simulate_shared_keystrokes("enter").await;
1341        cx.shared_state().await.assert_eq(indoc! {
1342            "ˇcc bb bb bb
1343                aa
1344                aa"
1345        });
1346
1347        // Each successive `/g` flag should invert the one before it.
1348        cx.simulate_shared_keystrokes(": s / b b / d d / g g").await;
1349        cx.simulate_shared_keystrokes("enter").await;
1350        cx.shared_state().await.assert_eq(indoc! {
1351            "ˇcc dd dd dd
1352                aa
1353                aa"
1354        });
1355
1356        cx.simulate_shared_keystrokes(": s / c c / e e / g g g")
1357            .await;
1358        cx.simulate_shared_keystrokes("enter").await;
1359        cx.shared_state().await.assert_eq(indoc! {
1360            "ˇee dd dd dd
1361                aa
1362                aa"
1363        });
1364    }
1365
1366    #[gpui::test]
1367    async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1368        let mut cx = VimTestContext::new(cx, true).await;
1369        cx.set_state(
1370            indoc! {
1371                "ˇaa
1372            aa
1373            aa"
1374            },
1375            Mode::Normal,
1376        );
1377
1378        cx.simulate_keystrokes("v j : s / a a / d d / c");
1379        cx.simulate_keystrokes("enter");
1380
1381        cx.assert_state(
1382            indoc! {
1383                "ˇaa
1384            aa
1385            aa"
1386            },
1387            Mode::Normal,
1388        );
1389
1390        cx.simulate_keystrokes("enter");
1391
1392        cx.assert_state(
1393            indoc! {
1394                "dd
1395            ˇaa
1396            aa"
1397            },
1398            Mode::Normal,
1399        );
1400
1401        cx.simulate_keystrokes("enter");
1402        cx.assert_state(
1403            indoc! {
1404                "dd
1405            ddˇ
1406            aa"
1407            },
1408            Mode::Normal,
1409        );
1410        cx.simulate_keystrokes("enter");
1411        cx.assert_state(
1412            indoc! {
1413                "dd
1414            ddˇ
1415            aa"
1416            },
1417            Mode::Normal,
1418        );
1419    }
1420
1421    #[gpui::test]
1422    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1423        let mut cx = NeovimBackedTestContext::new(cx).await;
1424
1425        cx.set_shared_state(indoc! {
1426            "ˇa
1427            a
1428            a
1429            a
1430            a
1431            a
1432            a
1433             "
1434        })
1435        .await;
1436        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1437        cx.simulate_shared_keystrokes("enter").await;
1438        cx.shared_state().await.assert_eq(indoc! {
1439            "a
1440            b
1441            b
1442            b
1443            ˇb
1444            a
1445            a
1446             "
1447        });
1448        cx.executor().advance_clock(Duration::from_millis(250));
1449        cx.run_until_parked();
1450
1451        cx.simulate_shared_keystrokes("/ a enter").await;
1452        cx.shared_state().await.assert_eq(indoc! {
1453            "a
1454                b
1455                b
1456                b
1457                b
1458                ˇa
1459                a
1460                 "
1461        });
1462    }
1463
1464    #[gpui::test]
1465    async fn test_search_dismiss_restores_cursor(cx: &mut gpui::TestAppContext) {
1466        let mut cx = VimTestContext::new(cx, true).await;
1467        cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1468
1469        // Move cursor to line 2
1470        cx.simulate_keystrokes("j");
1471        cx.run_until_parked();
1472        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1473
1474        // Open search
1475        cx.simulate_keystrokes("/");
1476        cx.run_until_parked();
1477
1478        // Dismiss search with Escape - cursor should return to line 2
1479        cx.simulate_keystrokes("escape");
1480        cx.run_until_parked();
1481        // Cursor should be restored to line 2 where it was when search was opened
1482        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1483    }
1484
1485    #[gpui::test]
1486    async fn test_search_dismiss_restores_cursor_no_matches(cx: &mut gpui::TestAppContext) {
1487        let mut cx = VimTestContext::new(cx, true).await;
1488        cx.set_state("ˇapple\nbanana\ncherry\n", Mode::Normal);
1489
1490        // Move cursor to line 2
1491        cx.simulate_keystrokes("j");
1492        cx.run_until_parked();
1493        cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1494
1495        // Open search and type query for something that doesn't exist
1496        cx.simulate_keystrokes("/ n o n e x i s t e n t");
1497        cx.run_until_parked();
1498
1499        // Dismiss search with Escape - cursor should still be at original position
1500        cx.simulate_keystrokes("escape");
1501        cx.run_until_parked();
1502        cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1503    }
1504
1505    #[gpui::test]
1506    async fn test_search_dismiss_after_editor_focus_does_not_restore(
1507        cx: &mut gpui::TestAppContext,
1508    ) {
1509        let mut cx = VimTestContext::new(cx, true).await;
1510        cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1511
1512        // Move cursor to line 2
1513        cx.simulate_keystrokes("j");
1514        cx.run_until_parked();
1515        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1516
1517        // Open search and type a query that matches line 3
1518        cx.simulate_keystrokes("/ a g a i n");
1519        cx.run_until_parked();
1520
1521        // Simulate the editor gaining focus while search is still open
1522        // This represents the user clicking in the editor
1523        cx.update_editor(|_, window, cx| cx.focus_self(window));
1524        cx.run_until_parked();
1525
1526        // Now dismiss the search bar directly
1527        cx.workspace(|workspace, window, cx| {
1528            let pane = workspace.active_pane().read(cx);
1529            if let Some(search_bar) = pane
1530                .toolbar()
1531                .read(cx)
1532                .item_of_type::<search::BufferSearchBar>()
1533            {
1534                search_bar.update(cx, |bar, cx| {
1535                    bar.dismiss(&search::buffer_search::Dismiss, window, cx)
1536                });
1537            }
1538        });
1539        cx.run_until_parked();
1540
1541        // Cursor should NOT be restored to line 2 (row 1) where search was opened.
1542        // Since the user "clicked" in the editor (by focusing it), prior_selections
1543        // was cleared, so dismiss should not restore the cursor.
1544        // The cursor should be at the match location on line 3 (row 2).
1545        cx.assert_state("hello world\nfoo bar\nhello ˇagain\n", Mode::Normal);
1546    }
1547
1548    #[gpui::test]
1549    async fn test_vim_search_respects_search_settings(cx: &mut gpui::TestAppContext) {
1550        let mut cx = VimTestContext::new(cx, true).await;
1551
1552        cx.update_global(|store: &mut SettingsStore, cx| {
1553            store.update_user_settings(cx, |settings| {
1554                settings.vim.get_or_insert_default().use_regex_search = Some(false);
1555            });
1556        });
1557
1558        cx.set_state("ˇcontent", Mode::Normal);
1559        cx.simulate_keystrokes("/");
1560        cx.run_until_parked();
1561
1562        // Verify search options are set from settings
1563        let search_bar = cx.workspace(|workspace, _, cx| {
1564            workspace
1565                .active_pane()
1566                .read(cx)
1567                .toolbar()
1568                .read(cx)
1569                .item_of_type::<BufferSearchBar>()
1570                .expect("Buffer search bar should be active")
1571        });
1572
1573        cx.update_entity(search_bar, |bar, _window, _cx| {
1574            assert!(
1575                !bar.has_search_option(search::SearchOptions::REGEX),
1576                "Vim search open without regex mode"
1577            );
1578        });
1579
1580        cx.simulate_keystrokes("escape");
1581        cx.run_until_parked();
1582
1583        cx.update_global(|store: &mut SettingsStore, cx| {
1584            store.update_user_settings(cx, |settings| {
1585                settings.vim.get_or_insert_default().use_regex_search = Some(true);
1586            });
1587        });
1588
1589        cx.simulate_keystrokes("/");
1590        cx.run_until_parked();
1591
1592        let search_bar = cx.workspace(|workspace, _, cx| {
1593            workspace
1594                .active_pane()
1595                .read(cx)
1596                .toolbar()
1597                .read(cx)
1598                .item_of_type::<BufferSearchBar>()
1599                .expect("Buffer search bar should be active")
1600        });
1601
1602        cx.update_entity(search_bar, |bar, _window, _cx| {
1603            assert!(
1604                bar.has_search_option(search::SearchOptions::REGEX),
1605                "Vim search opens with regex mode"
1606            );
1607        });
1608    }
1609}
1610
Served at tenant.openagents/omega Member data and write actions are omitted.