1use editor::{Editor, EditorSettings};
2use gpui::{Action, Context, 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 let prior_selections = self.editor_selections(window, cx);
393
394 let success = pane.update(cx, |pane, cx| {
395 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
396 return false;
397 };
398 search_bar.update(cx, |search_bar, cx| {
399 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
400 return false;
401 }
402 search_bar.select_match(direction, count, window, cx);
403 true
404 })
405 });
406 if !success {
407 return;
408 }
409
410 let new_selections = self.editor_selections(window, cx);
411 self.search_motion(
412 Motion::ZedSearchResult {
413 prior_selections,
414 new_selections,
415 },
416 window,
417 cx,
418 );
419 }
420
421 pub fn move_to_internal(
422 &mut self,
423 direction: Direction,
424 case_sensitive: bool,
425 whole_word: bool,
426 regex: bool,
427 move_cursor: bool,
428 window: &mut Window,
429 cx: &mut Context<Self>,
430 ) {
431 let Some(pane) = self.pane(window, cx) else {
432 return;
433 };
434 let count = Vim::take_count(cx).unwrap_or(1);
435 Vim::take_forced_motion(cx);
436 let prior_selections = self.editor_selections(window, cx);
437 let cursor_word = self.editor_cursor_word(window, cx);
438 let vim = cx.entity();
439
440 let searched = pane.update(cx, |pane, cx| {
441 self.search.direction = direction;
442 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
443 return false;
444 };
445 let search = search_bar.update(cx, |search_bar, cx| {
446 let mut options = SearchOptions::NONE;
447 if case_sensitive {
448 options |= SearchOptions::CASE_SENSITIVE;
449 }
450 if regex {
451 options |= SearchOptions::REGEX;
452 }
453 if whole_word {
454 options |= SearchOptions::WHOLE_WORD;
455 }
456 if !search_bar.show(window, cx) {
457 return None;
458 }
459 let Some(query) = search_bar
460 .query_suggestion(window, cx)
461 .or_else(|| cursor_word)
462 else {
463 drop(search_bar.search("", None, false, window, cx));
464 return None;
465 };
466
467 let query = regex::escape(&query);
468 Some(search_bar.search(&query, Some(options), true, window, cx))
469 });
470
471 let Some(search) = search else { return false };
472
473 if move_cursor {
474 let search_bar = search_bar.downgrade();
475 cx.spawn_in(window, async move |_, cx| {
476 search.await?;
477 search_bar.update_in(cx, |search_bar, window, cx| {
478 search_bar.select_match(direction, count, window, cx);
479
480 vim.update(cx, |vim, cx| {
481 let new_selections = vim.editor_selections(window, cx);
482 vim.search_motion(
483 Motion::ZedSearchResult {
484 prior_selections,
485 new_selections,
486 },
487 window,
488 cx,
489 )
490 });
491 })?;
492 anyhow::Ok(())
493 })
494 .detach_and_log_err(cx);
495 }
496 true
497 });
498 if !searched {
499 self.clear_operator(window, cx)
500 }
501
502 if self.mode.is_visual() {
503 self.switch_mode(Mode::Normal, false, window, cx)
504 }
505 }
506
507 fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
508 let Some(pane) = self.pane(window, cx) else {
509 return;
510 };
511 pane.update(cx, |pane, cx| {
512 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
513 let search = search_bar.update(cx, |search_bar, cx| {
514 if !search_bar.show(window, cx) {
515 return None;
516 }
517 let mut query = action.query.clone();
518 if query.is_empty() {
519 query = search_bar.query(cx);
520 };
521
522 let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
523 if search_bar.should_use_smartcase_search(cx) {
524 options.set(
525 SearchOptions::CASE_SENSITIVE,
526 search_bar.is_contains_uppercase(&query),
527 );
528 }
529
530 Some(search_bar.search(&query, Some(options), true, window, cx))
531 });
532 let Some(search) = search else { return };
533 let search_bar = search_bar.downgrade();
534 let direction = if action.backwards {
535 Direction::Prev
536 } else {
537 Direction::Next
538 };
539 cx.spawn_in(window, async move |_, cx| {
540 search.await?;
541 search_bar.update_in(cx, |search_bar, window, cx| {
542 search_bar.select_match(direction, 1, window, cx)
543 })?;
544 anyhow::Ok(())
545 })
546 .detach_and_log_err(cx);
547 }
548 })
549 }
550
551 fn replace_command(
552 &mut self,
553 action: &ReplaceCommand,
554 window: &mut Window,
555 cx: &mut Context<Self>,
556 ) {
557 let replacement = action.replacement.clone();
558 let Some(((pane, workspace), editor)) = self
559 .pane(window, cx)
560 .zip(self.workspace(window, cx))
561 .zip(self.editor())
562 else {
563 return;
564 };
565 if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
566 let range = action.range.buffer_range(vim, editor, window, cx)?;
567 let snapshot = editor.snapshot(window, cx);
568 let snapshot = snapshot.buffer_snapshot();
569 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
570 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
571 ..snapshot.anchor_after(end_point);
572 editor.set_search_within_ranges(&[range], cx);
573 anyhow::Ok(())
574 }) {
575 workspace.update(cx, |workspace, cx| {
576 result.notify_err(workspace, cx);
577 })
578 }
579 let Some(search_bar) = pane.update(cx, |pane, cx| {
580 pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
581 }) else {
582 return;
583 };
584 let mut options = SearchOptions::REGEX;
585 let search = search_bar.update(cx, |search_bar, cx| {
586 if !search_bar.show(window, cx) {
587 return None;
588 }
589
590 let search = if replacement.search.is_empty() {
591 search_bar.query(cx)
592 } else {
593 replacement.search
594 };
595
596 if let Some(case) = replacement.case_sensitive {
597 options.set(SearchOptions::CASE_SENSITIVE, case)
598 } else if search_bar.should_use_smartcase_search(cx) {
599 options.set(
600 SearchOptions::CASE_SENSITIVE,
601 search_bar.is_contains_uppercase(&search),
602 );
603 } else {
604 // Fallback: no explicit i/I flags and smartcase disabled;
605 // use global editor.search.case_sensitive.
606 options.set(
607 SearchOptions::CASE_SENSITIVE,
608 EditorSettings::get_global(cx).search.case_sensitive,
609 )
610 }
611
612 // gdefault inverts the behavior of the 'g' flag.
613 let replace_all = VimSettings::get_global(cx).gdefault != replacement.flag_g;
614 if !replace_all {
615 options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
616 }
617
618 search_bar.set_replacement(Some(&replacement.replacement), cx);
619 if replacement.flag_c {
620 search_bar.focus_replace(window, cx);
621 }
622 Some(search_bar.search(&search, Some(options), true, window, cx))
623 });
624 if replacement.flag_n {
625 self.move_cursor(
626 Motion::StartOfLine {
627 display_lines: false,
628 },
629 None,
630 window,
631 cx,
632 );
633 return;
634 }
635 let Some(search) = search else { return };
636 let search_bar = search_bar.downgrade();
637 cx.spawn_in(window, async move |vim, cx| {
638 search.await?;
639 search_bar.update_in(cx, |search_bar, window, cx| {
640 if replacement.flag_c {
641 search_bar.select_first_match(window, cx);
642 return;
643 }
644 search_bar.select_last_match(window, cx);
645 search_bar.replace_all(&Default::default(), window, cx);
646 editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
647 let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
648 vim.update(cx, |vim, cx| {
649 vim.move_cursor(
650 Motion::StartOfLine {
651 display_lines: false,
652 },
653 None,
654 window,
655 cx,
656 )
657 })
658 .ok();
659
660 // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
661 // this is not properly supported outside of vim mode, and
662 // not disabling it makes the "Replace All Matches" button
663 // actually replace only the first match on each line.
664 options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
665 search_bar.set_search_options(options, cx);
666 })
667 })
668 .detach_and_log_err(cx);
669 }
670}
671
672impl Replacement {
673 // convert a vim query into something more usable by zed.
674 // we don't attempt to fully convert between the two regex syntaxes,
675 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
676 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
677 pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
678 let delimiter = chars
679 .next()
680 .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
681
682 let mut search = String::new();
683 let mut replacement = String::new();
684 let mut flags = String::new();
685
686 let mut buffer = &mut search;
687
688 let mut escaped = false;
689 // 0 - parsing search
690 // 1 - parsing replacement
691 // 2 - parsing flags
692 let mut phase = 0;
693
694 for c in chars {
695 if escaped {
696 escaped = false;
697 if phase == 1 && c.is_ascii_digit() {
698 buffer.push('$')
699 // unescape escaped parens
700 } else if phase == 0 && (c == '(' || c == ')') {
701 } else if c != delimiter {
702 buffer.push('\\')
703 }
704 buffer.push(c)
705 } else if c == '\\' {
706 escaped = true;
707 } else if c == delimiter {
708 if phase == 0 {
709 buffer = &mut replacement;
710 phase = 1;
711 } else if phase == 1 {
712 buffer = &mut flags;
713 phase = 2;
714 } else {
715 break;
716 }
717 } else {
718 // escape unescaped parens
719 if phase == 0 && (c == '(' || c == ')') {
720 buffer.push('\\')
721 }
722 buffer.push(c)
723 }
724 }
725
726 let mut replacement = Replacement {
727 search,
728 replacement,
729 case_sensitive: None,
730 flag_g: false,
731 flag_n: false,
732 flag_c: false,
733 };
734
735 for c in flags.chars() {
736 match c {
737 'g' => replacement.flag_g = !replacement.flag_g,
738 'n' => replacement.flag_n = true,
739 'c' => replacement.flag_c = true,
740 'i' => replacement.case_sensitive = Some(false),
741 'I' => replacement.case_sensitive = Some(true),
742 _ => {}
743 }
744 }
745
746 Some(replacement)
747 }
748}
749
750#[cfg(test)]
751mod test {
752 use std::time::Duration;
753
754 use crate::{
755 state::Mode,
756 test::{NeovimBackedTestContext, VimTestContext},
757 };
758 use editor::{DisplayPoint, display_map::DisplayRow};
759
760 use indoc::indoc;
761 use search::BufferSearchBar;
762 use settings::SettingsStore;
763
764 #[gpui::test]
765 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
766 let mut cx = VimTestContext::new(cx, true).await;
767 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
768
769 cx.simulate_keystrokes("*");
770 cx.run_until_parked();
771 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
772
773 cx.simulate_keystrokes("*");
774 cx.run_until_parked();
775 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
776
777 cx.simulate_keystrokes("#");
778 cx.run_until_parked();
779 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
780
781 cx.simulate_keystrokes("#");
782 cx.run_until_parked();
783 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
784
785 cx.simulate_keystrokes("2 *");
786 cx.run_until_parked();
787 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
788
789 cx.simulate_keystrokes("g *");
790 cx.run_until_parked();
791 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
792
793 cx.simulate_keystrokes("n");
794 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
795
796 cx.simulate_keystrokes("g #");
797 cx.run_until_parked();
798 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
799 }
800
801 #[gpui::test]
802 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
803 let mut cx = VimTestContext::new(cx, true).await;
804
805 cx.update_global(|store: &mut SettingsStore, cx| {
806 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
807 });
808
809 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
810
811 cx.simulate_keystrokes("*");
812 cx.run_until_parked();
813 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
814
815 cx.simulate_keystrokes("*");
816 cx.run_until_parked();
817 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
818
819 cx.simulate_keystrokes("#");
820 cx.run_until_parked();
821 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
822
823 cx.simulate_keystrokes("3 *");
824 cx.run_until_parked();
825 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
826
827 cx.simulate_keystrokes("g *");
828 cx.run_until_parked();
829 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
830
831 cx.simulate_keystrokes("n");
832 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
833
834 cx.simulate_keystrokes("g #");
835 cx.run_until_parked();
836 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
837 }
838
839 #[gpui::test]
840 async fn test_search(cx: &mut gpui::TestAppContext) {
841 let mut cx = VimTestContext::new(cx, true).await;
842
843 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
844 cx.simulate_keystrokes("/ c c");
845
846 let search_bar = cx.workspace(|workspace, _, cx| {
847 workspace
848 .active_pane()
849 .read(cx)
850 .toolbar()
851 .read(cx)
852 .item_of_type::<BufferSearchBar>()
853 .expect("Buffer search bar should be deployed")
854 });
855
856 cx.update_entity(search_bar, |bar, _window, cx| {
857 assert_eq!(bar.query(cx), "cc");
858 });
859
860 cx.run_until_parked();
861
862 cx.update_editor(|editor, window, cx| {
863 let highlights = editor.all_text_background_highlights(window, cx);
864 assert_eq!(3, highlights.len());
865 assert_eq!(
866 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
867 highlights[0].0
868 )
869 });
870
871 cx.simulate_keystrokes("enter");
872 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
873
874 // n to go to next/N to go to previous
875 cx.simulate_keystrokes("n");
876 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
877 cx.simulate_keystrokes("shift-n");
878 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
879
880 // ?<enter> to go to previous
881 cx.simulate_keystrokes("? enter");
882 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
883 cx.simulate_keystrokes("? enter");
884 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
885
886 // /<enter> to go to next
887 cx.simulate_keystrokes("/ enter");
888 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
889
890 // ?{search}<enter> to search backwards
891 cx.simulate_keystrokes("? b enter");
892 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
893
894 // works with counts
895 cx.simulate_keystrokes("4 / c");
896 cx.simulate_keystrokes("enter");
897 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
898
899 // check that searching resumes from cursor, not previous match
900 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
901 cx.simulate_keystrokes("/ d");
902 cx.simulate_keystrokes("enter");
903 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
904 cx.update_editor(|editor, window, cx| {
905 editor.move_to_beginning(&Default::default(), window, cx)
906 });
907 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
908 cx.simulate_keystrokes("/ b");
909 cx.simulate_keystrokes("enter");
910 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
911
912 // check that searching switches to normal mode if in visual mode
913 cx.set_state("ˇone two one", Mode::Normal);
914 cx.simulate_keystrokes("v l l");
915 cx.assert_editor_state("«oneˇ» two one");
916 cx.simulate_keystrokes("*");
917 cx.assert_state("one two ˇone", Mode::Normal);
918
919 // check that a backward search after last match works correctly
920 cx.set_state("aa\naa\nbbˇ", Mode::Normal);
921 cx.simulate_keystrokes("? a a");
922 cx.simulate_keystrokes("enter");
923 cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
924
925 // check that searching with unable search wrap
926 cx.update_global(|store: &mut SettingsStore, cx| {
927 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
928 });
929 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
930 cx.simulate_keystrokes("/ c c enter");
931
932 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
933
934 // n to go to next/N to go to previous
935 cx.simulate_keystrokes("n");
936 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
937 cx.simulate_keystrokes("shift-n");
938 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
939
940 // ?<enter> to go to previous
941 cx.simulate_keystrokes("? enter");
942 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
943 cx.simulate_keystrokes("? enter");
944 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
945 }
946
947 #[gpui::test]
948 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
949 let mut cx = VimTestContext::new(cx, false).await;
950 cx.cx.set_state("ˇone one one one");
951 cx.run_until_parked();
952 cx.simulate_keystrokes("cmd-f");
953 cx.run_until_parked();
954
955 cx.assert_editor_state("«oneˇ» one one one");
956 cx.simulate_keystrokes("enter");
957 cx.assert_editor_state("one «oneˇ» one one");
958 cx.simulate_keystrokes("shift-enter");
959 cx.assert_editor_state("«oneˇ» one one one");
960 }
961
962 #[gpui::test]
963 async fn test_non_vim_search_in_vim_mode(cx: &mut gpui::TestAppContext) {
964 let mut cx = VimTestContext::new(cx, true).await;
965 cx.cx.set_state("ˇone one one one");
966 cx.run_until_parked();
967 cx.simulate_keystrokes("cmd-f");
968 cx.run_until_parked();
969
970 cx.assert_state("«oneˇ» one one one", Mode::Visual);
971 cx.simulate_keystrokes("enter");
972 cx.run_until_parked();
973 cx.assert_state("one «oneˇ» one one", Mode::Visual);
974 cx.simulate_keystrokes("shift-enter");
975 cx.run_until_parked();
976 cx.assert_state("«oneˇ» one one one", Mode::Visual);
977
978 cx.simulate_keystrokes("escape");
979 cx.run_until_parked();
980 cx.assert_state("«oneˇ» one one one", Mode::Visual);
981 }
982
983 #[gpui::test]
984 async fn test_non_vim_search_in_vim_insert_mode(cx: &mut gpui::TestAppContext) {
985 let mut cx = VimTestContext::new(cx, true).await;
986 cx.set_state("ˇone one one one", Mode::Insert);
987 cx.run_until_parked();
988 cx.simulate_keystrokes("cmd-f");
989 cx.run_until_parked();
990
991 cx.assert_state("«oneˇ» one one one", Mode::Insert);
992 cx.simulate_keystrokes("enter");
993 cx.run_until_parked();
994 cx.assert_state("one «oneˇ» one one", Mode::Insert);
995
996 cx.simulate_keystrokes("escape");
997 cx.run_until_parked();
998 cx.assert_state("one «oneˇ» one one", Mode::Insert);
999 }
1000
1001 #[gpui::test]
1002 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
1003 let mut cx = NeovimBackedTestContext::new(cx).await;
1004
1005 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1006 cx.simulate_shared_keystrokes("v 3 l *").await;
1007 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
1008 }
1009
1010 #[gpui::test]
1011 async fn test_d_search(cx: &mut gpui::TestAppContext) {
1012 let mut cx = NeovimBackedTestContext::new(cx).await;
1013
1014 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1015 cx.simulate_shared_keystrokes("d / c d").await;
1016 cx.simulate_shared_keystrokes("enter").await;
1017 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
1018 }
1019
1020 #[gpui::test]
1021 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
1022 let mut cx = NeovimBackedTestContext::new(cx).await;
1023
1024 cx.set_shared_state("ˇa b a b a b a").await;
1025 cx.simulate_shared_keystrokes("*").await;
1026 cx.simulate_shared_keystrokes("n").await;
1027 cx.shared_state().await.assert_eq("a b a b ˇa b a");
1028 cx.simulate_shared_keystrokes("#").await;
1029 cx.shared_state().await.assert_eq("a b ˇa b a b a");
1030 cx.simulate_shared_keystrokes("n").await;
1031 cx.shared_state().await.assert_eq("ˇa b a b a b a");
1032 }
1033
1034 #[gpui::test]
1035 async fn test_v_search(cx: &mut gpui::TestAppContext) {
1036 let mut cx = NeovimBackedTestContext::new(cx).await;
1037
1038 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1039 cx.simulate_shared_keystrokes("v / c d").await;
1040 cx.simulate_shared_keystrokes("enter").await;
1041 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
1042
1043 cx.set_shared_state("a a aˇ a a a").await;
1044 cx.simulate_shared_keystrokes("v / a").await;
1045 cx.simulate_shared_keystrokes("enter").await;
1046 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1047 cx.simulate_shared_keystrokes("/ enter").await;
1048 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1049 cx.simulate_shared_keystrokes("? enter").await;
1050 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1051 cx.simulate_shared_keystrokes("? enter").await;
1052 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
1053 cx.simulate_shared_keystrokes("/ enter").await;
1054 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1055 cx.simulate_shared_keystrokes("/ enter").await;
1056 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1057 }
1058
1059 #[gpui::test]
1060 async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
1061 let mut cx = NeovimBackedTestContext::new(cx).await;
1062
1063 cx.set_shared_state("ˇaa aa").await;
1064 cx.simulate_shared_keystrokes("v / a a").await;
1065 cx.simulate_shared_keystrokes("enter").await;
1066 cx.shared_state().await.assert_eq("«aa aˇ»a");
1067 }
1068
1069 #[gpui::test]
1070 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
1071 let mut cx = NeovimBackedTestContext::new(cx).await;
1072
1073 cx.set_shared_state(indoc! {
1074 "ˇone two
1075 three four
1076 five six
1077 "
1078 })
1079 .await;
1080 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
1081 cx.simulate_shared_keystrokes("enter").await;
1082 cx.shared_state().await.assert_eq(indoc! {
1083 "«one twoˇ»
1084 «three fˇ»our
1085 five six
1086 "
1087 });
1088 }
1089
1090 #[gpui::test]
1091 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
1092 let mut cx = NeovimBackedTestContext::new(cx).await;
1093
1094 cx.set_shared_state(indoc! {
1095 "ˇa
1096 a
1097 a
1098 a
1099 a
1100 a
1101 a
1102 "
1103 })
1104 .await;
1105 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
1106 cx.simulate_shared_keystrokes("enter").await;
1107 cx.shared_state().await.assert_eq(indoc! {
1108 "a
1109 ba
1110 ba
1111 ba
1112 ˇba
1113 a
1114 a
1115 "
1116 });
1117
1118 cx.simulate_shared_keystrokes("/ a").await;
1119 cx.simulate_shared_keystrokes("enter").await;
1120 cx.shared_state().await.assert_eq(indoc! {
1121 "a
1122 ba
1123 ba
1124 ba
1125 bˇa
1126 a
1127 a
1128 "
1129 });
1130 }
1131
1132 #[gpui::test]
1133 async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
1134 let mut cx = NeovimBackedTestContext::new(cx).await;
1135 cx.set_shared_state(indoc! {
1136 "ˇaa aa aa"
1137 })
1138 .await;
1139
1140 cx.simulate_shared_keystrokes("/ a a").await;
1141 cx.simulate_shared_keystrokes("enter").await;
1142
1143 cx.shared_state().await.assert_eq(indoc! {
1144 "aa ˇaa aa"
1145 });
1146
1147 cx.simulate_shared_keystrokes("left / a a").await;
1148 cx.simulate_shared_keystrokes("enter").await;
1149
1150 cx.shared_state().await.assert_eq(indoc! {
1151 "aa ˇaa aa"
1152 });
1153 }
1154
1155 #[gpui::test]
1156 async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1157 let mut cx = NeovimBackedTestContext::new(cx).await;
1158 cx.set_shared_state(indoc! {
1159 "ˇaa
1160 bb
1161 aa"
1162 })
1163 .await;
1164
1165 cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1166 cx.simulate_shared_keystrokes("enter").await;
1167
1168 cx.shared_state().await.assert_eq(indoc! {
1169 "ˇaa
1170 bb
1171 aa"
1172 });
1173
1174 let search_bar = cx.update_workspace(|workspace, _, cx| {
1175 workspace.active_pane().update(cx, |pane, cx| {
1176 pane.toolbar()
1177 .read(cx)
1178 .item_of_type::<BufferSearchBar>()
1179 .unwrap()
1180 })
1181 });
1182 cx.update_entity(search_bar, |search_bar, _, cx| {
1183 assert!(!search_bar.is_dismissed());
1184 assert_eq!(search_bar.query(cx), "bb".to_string());
1185 assert_eq!(search_bar.replacement(cx), "dd".to_string());
1186 })
1187 }
1188
1189 #[gpui::test]
1190 async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1191 let mut cx = NeovimBackedTestContext::new(cx).await;
1192 cx.set_shared_state(indoc! {
1193 "ˇaa aa aa aa
1194 aa
1195 aa"
1196 })
1197 .await;
1198
1199 cx.simulate_shared_keystrokes(": s / a a / b b").await;
1200 cx.simulate_shared_keystrokes("enter").await;
1201 cx.shared_state().await.assert_eq(indoc! {
1202 "ˇbb aa aa aa
1203 aa
1204 aa"
1205 });
1206 cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1207 cx.simulate_shared_keystrokes("enter").await;
1208 cx.shared_state().await.assert_eq(indoc! {
1209 "ˇbb bb bb bb
1210 aa
1211 aa"
1212 });
1213 }
1214
1215 #[gpui::test]
1216 async fn test_replace_gdefault(cx: &mut gpui::TestAppContext) {
1217 let mut cx = NeovimBackedTestContext::new(cx).await;
1218
1219 // Set the `gdefault` option in both Zed and Neovim.
1220 cx.simulate_shared_keystrokes(": s e t space g d e f a u l t")
1221 .await;
1222 cx.simulate_shared_keystrokes("enter").await;
1223
1224 cx.set_shared_state(indoc! {
1225 "ˇaa aa aa aa
1226 aa
1227 aa"
1228 })
1229 .await;
1230
1231 // With gdefault on, :s/// replaces all matches (like :s///g normally).
1232 cx.simulate_shared_keystrokes(": s / a a / b b").await;
1233 cx.simulate_shared_keystrokes("enter").await;
1234 cx.shared_state().await.assert_eq(indoc! {
1235 "ˇbb bb bb bb
1236 aa
1237 aa"
1238 });
1239
1240 // With gdefault on, :s///g replaces only the first match.
1241 cx.simulate_shared_keystrokes(": s / b b / c c / g").await;
1242 cx.simulate_shared_keystrokes("enter").await;
1243 cx.shared_state().await.assert_eq(indoc! {
1244 "ˇcc bb bb bb
1245 aa
1246 aa"
1247 });
1248
1249 // Each successive `/g` flag should invert the one before it.
1250 cx.simulate_shared_keystrokes(": s / b b / d d / g g").await;
1251 cx.simulate_shared_keystrokes("enter").await;
1252 cx.shared_state().await.assert_eq(indoc! {
1253 "ˇcc dd dd dd
1254 aa
1255 aa"
1256 });
1257
1258 cx.simulate_shared_keystrokes(": s / c c / e e / g g g")
1259 .await;
1260 cx.simulate_shared_keystrokes("enter").await;
1261 cx.shared_state().await.assert_eq(indoc! {
1262 "ˇee dd dd dd
1263 aa
1264 aa"
1265 });
1266 }
1267
1268 #[gpui::test]
1269 async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1270 let mut cx = VimTestContext::new(cx, true).await;
1271 cx.set_state(
1272 indoc! {
1273 "ˇaa
1274 aa
1275 aa"
1276 },
1277 Mode::Normal,
1278 );
1279
1280 cx.simulate_keystrokes("v j : s / a a / d d / c");
1281 cx.simulate_keystrokes("enter");
1282
1283 cx.assert_state(
1284 indoc! {
1285 "ˇaa
1286 aa
1287 aa"
1288 },
1289 Mode::Normal,
1290 );
1291
1292 cx.simulate_keystrokes("enter");
1293
1294 cx.assert_state(
1295 indoc! {
1296 "dd
1297 ˇaa
1298 aa"
1299 },
1300 Mode::Normal,
1301 );
1302
1303 cx.simulate_keystrokes("enter");
1304 cx.assert_state(
1305 indoc! {
1306 "dd
1307 ddˇ
1308 aa"
1309 },
1310 Mode::Normal,
1311 );
1312 cx.simulate_keystrokes("enter");
1313 cx.assert_state(
1314 indoc! {
1315 "dd
1316 ddˇ
1317 aa"
1318 },
1319 Mode::Normal,
1320 );
1321 }
1322
1323 #[gpui::test]
1324 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1325 let mut cx = NeovimBackedTestContext::new(cx).await;
1326
1327 cx.set_shared_state(indoc! {
1328 "ˇa
1329 a
1330 a
1331 a
1332 a
1333 a
1334 a
1335 "
1336 })
1337 .await;
1338 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1339 cx.simulate_shared_keystrokes("enter").await;
1340 cx.shared_state().await.assert_eq(indoc! {
1341 "a
1342 b
1343 b
1344 b
1345 ˇb
1346 a
1347 a
1348 "
1349 });
1350 cx.executor().advance_clock(Duration::from_millis(250));
1351 cx.run_until_parked();
1352
1353 cx.simulate_shared_keystrokes("/ a enter").await;
1354 cx.shared_state().await.assert_eq(indoc! {
1355 "a
1356 b
1357 b
1358 b
1359 b
1360 ˇa
1361 a
1362 "
1363 });
1364 }
1365
1366 #[gpui::test]
1367 async fn test_search_dismiss_restores_cursor(cx: &mut gpui::TestAppContext) {
1368 let mut cx = VimTestContext::new(cx, true).await;
1369 cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1370
1371 // Move cursor to line 2
1372 cx.simulate_keystrokes("j");
1373 cx.run_until_parked();
1374 cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1375
1376 // Open search
1377 cx.simulate_keystrokes("/");
1378 cx.run_until_parked();
1379
1380 // Dismiss search with Escape - cursor should return to line 2
1381 cx.simulate_keystrokes("escape");
1382 cx.run_until_parked();
1383 // Cursor should be restored to line 2 where it was when search was opened
1384 cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1385 }
1386
1387 #[gpui::test]
1388 async fn test_search_dismiss_restores_cursor_no_matches(cx: &mut gpui::TestAppContext) {
1389 let mut cx = VimTestContext::new(cx, true).await;
1390 cx.set_state("ˇapple\nbanana\ncherry\n", Mode::Normal);
1391
1392 // Move cursor to line 2
1393 cx.simulate_keystrokes("j");
1394 cx.run_until_parked();
1395 cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1396
1397 // Open search and type query for something that doesn't exist
1398 cx.simulate_keystrokes("/ n o n e x i s t e n t");
1399 cx.run_until_parked();
1400
1401 // Dismiss search with Escape - cursor should still be at original position
1402 cx.simulate_keystrokes("escape");
1403 cx.run_until_parked();
1404 cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1405 }
1406
1407 #[gpui::test]
1408 async fn test_search_dismiss_after_editor_focus_does_not_restore(
1409 cx: &mut gpui::TestAppContext,
1410 ) {
1411 let mut cx = VimTestContext::new(cx, true).await;
1412 cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1413
1414 // Move cursor to line 2
1415 cx.simulate_keystrokes("j");
1416 cx.run_until_parked();
1417 cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1418
1419 // Open search and type a query that matches line 3
1420 cx.simulate_keystrokes("/ a g a i n");
1421 cx.run_until_parked();
1422
1423 // Simulate the editor gaining focus while search is still open
1424 // This represents the user clicking in the editor
1425 cx.update_editor(|_, window, cx| cx.focus_self(window));
1426 cx.run_until_parked();
1427
1428 // Now dismiss the search bar directly
1429 cx.workspace(|workspace, window, cx| {
1430 let pane = workspace.active_pane().read(cx);
1431 if let Some(search_bar) = pane
1432 .toolbar()
1433 .read(cx)
1434 .item_of_type::<search::BufferSearchBar>()
1435 {
1436 search_bar.update(cx, |bar, cx| {
1437 bar.dismiss(&search::buffer_search::Dismiss, window, cx)
1438 });
1439 }
1440 });
1441 cx.run_until_parked();
1442
1443 // Cursor should NOT be restored to line 2 (row 1) where search was opened.
1444 // Since the user "clicked" in the editor (by focusing it), prior_selections
1445 // was cleared, so dismiss should not restore the cursor.
1446 // The cursor should be at the match location on line 3 (row 2).
1447 cx.assert_state("hello world\nfoo bar\nhello ˇagain\n", Mode::Normal);
1448 }
1449
1450 #[gpui::test]
1451 async fn test_vim_search_respects_search_settings(cx: &mut gpui::TestAppContext) {
1452 let mut cx = VimTestContext::new(cx, true).await;
1453
1454 cx.update_global(|store: &mut SettingsStore, cx| {
1455 store.update_user_settings(cx, |settings| {
1456 settings.vim.get_or_insert_default().use_regex_search = Some(false);
1457 });
1458 });
1459
1460 cx.set_state("ˇcontent", Mode::Normal);
1461 cx.simulate_keystrokes("/");
1462 cx.run_until_parked();
1463
1464 // Verify search options are set from settings
1465 let search_bar = cx.workspace(|workspace, _, cx| {
1466 workspace
1467 .active_pane()
1468 .read(cx)
1469 .toolbar()
1470 .read(cx)
1471 .item_of_type::<BufferSearchBar>()
1472 .expect("Buffer search bar should be active")
1473 });
1474
1475 cx.update_entity(search_bar, |bar, _window, _cx| {
1476 assert!(
1477 !bar.has_search_option(search::SearchOptions::REGEX),
1478 "Vim search open without regex mode"
1479 );
1480 });
1481
1482 cx.simulate_keystrokes("escape");
1483 cx.run_until_parked();
1484
1485 cx.update_global(|store: &mut SettingsStore, cx| {
1486 store.update_user_settings(cx, |settings| {
1487 settings.vim.get_or_insert_default().use_regex_search = Some(true);
1488 });
1489 });
1490
1491 cx.simulate_keystrokes("/");
1492 cx.run_until_parked();
1493
1494 let search_bar = cx.workspace(|workspace, _, cx| {
1495 workspace
1496 .active_pane()
1497 .read(cx)
1498 .toolbar()
1499 .read(cx)
1500 .item_of_type::<BufferSearchBar>()
1501 .expect("Buffer search bar should be active")
1502 });
1503
1504 cx.update_entity(search_bar, |bar, _window, _cx| {
1505 assert!(
1506 bar.has_search_option(search::SearchOptions::REGEX),
1507 "Vim search opens with regex mode"
1508 );
1509 });
1510 }
1511}