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