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,
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/// Initiates a search operation with the specified parameters.
46#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
47#[action(namespace = vim)]
48#[serde(deny_unknown_fields)]
49pub(crate) struct Search {
50 #[serde(default)]
51 backwards: bool,
52 #[serde(default = "default_true")]
53 regex: bool,
54}
55
56/// Executes a find command to search for patterns in the buffer.
57#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
58#[action(namespace = vim)]
59#[serde(deny_unknown_fields)]
60pub struct FindCommand {
61 pub query: String,
62 pub backwards: bool,
63}
64
65/// Executes a search and replace command within the specified range.
66#[derive(Clone, Debug, PartialEq, Action)]
67#[action(namespace = vim, no_json, no_register)]
68pub struct ReplaceCommand {
69 pub(crate) range: CommandRange,
70 pub(crate) replacement: Replacement,
71}
72
73#[derive(Clone, Debug, PartialEq)]
74pub struct Replacement {
75 search: String,
76 replacement: String,
77 case_sensitive: Option<bool>,
78 flag_n: bool,
79 flag_g: bool,
80 flag_c: bool,
81}
82
83actions!(
84 vim,
85 [
86 /// Submits the current search query.
87 SearchSubmit,
88 /// Moves to the next search match.
89 MoveToNextMatch,
90 /// Moves to the previous search match.
91 MoveToPreviousMatch
92 ]
93);
94
95pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
96 Vim::action(editor, cx, Vim::move_to_next);
97 Vim::action(editor, cx, Vim::move_to_previous);
98 Vim::action(editor, cx, Vim::move_to_next_match);
99 Vim::action(editor, cx, Vim::move_to_previous_match);
100 Vim::action(editor, cx, Vim::search);
101 Vim::action(editor, cx, Vim::search_deploy);
102 Vim::action(editor, cx, Vim::find_command);
103 Vim::action(editor, cx, Vim::replace_command);
104}
105
106impl Vim {
107 fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context<Self>) {
108 self.move_to_internal(
109 Direction::Next,
110 action.case_sensitive,
111 !action.partial_word,
112 action.regex,
113 window,
114 cx,
115 )
116 }
117
118 fn move_to_previous(
119 &mut self,
120 action: &MoveToPrevious,
121 window: &mut Window,
122 cx: &mut Context<Self>,
123 ) {
124 self.move_to_internal(
125 Direction::Prev,
126 action.case_sensitive,
127 !action.partial_word,
128 action.regex,
129 window,
130 cx,
131 )
132 }
133
134 fn move_to_next_match(
135 &mut self,
136 _: &MoveToNextMatch,
137 window: &mut Window,
138 cx: &mut Context<Self>,
139 ) {
140 self.move_to_match_internal(self.search.direction, window, cx)
141 }
142
143 fn move_to_previous_match(
144 &mut self,
145 _: &MoveToPreviousMatch,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) {
149 self.move_to_match_internal(self.search.direction.opposite(), window, cx)
150 }
151
152 fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context<Self>) {
153 let Some(pane) = self.pane(window, cx) else {
154 return;
155 };
156 let direction = if action.backwards {
157 Direction::Prev
158 } else {
159 Direction::Next
160 };
161 let count = Vim::take_count(cx).unwrap_or(1);
162 Vim::take_forced_motion(cx);
163 let prior_selections = self.editor_selections(window, cx);
164 pane.update(cx, |pane, cx| {
165 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
166 search_bar.update(cx, |search_bar, cx| {
167 if !search_bar.show(window, cx) {
168 return;
169 }
170
171 search_bar.select_query(window, cx);
172 cx.focus_self(window);
173
174 search_bar.set_replacement(None, cx);
175 let mut options = SearchOptions::NONE;
176 if action.regex {
177 options |= SearchOptions::REGEX;
178 }
179 if action.backwards {
180 options |= SearchOptions::BACKWARDS;
181 }
182 if EditorSettings::get_global(cx).search.case_sensitive {
183 options |= SearchOptions::CASE_SENSITIVE;
184 }
185 search_bar.set_search_options(options, cx);
186 let prior_mode = if self.temp_mode {
187 Mode::Insert
188 } else {
189 self.mode
190 };
191
192 self.search = SearchState {
193 direction,
194 count,
195 prior_selections,
196 prior_operator: self.operator_stack.last().cloned(),
197 prior_mode,
198 helix_select: false,
199 }
200 });
201 }
202 })
203 }
204
205 // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
206 fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
207 // Preserve the current mode when resetting search state
208 let current_mode = self.mode;
209 self.search = Default::default();
210 self.search.prior_mode = current_mode;
211 cx.propagate();
212 }
213
214 pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
215 self.store_visual_marks(window, cx);
216 let Some(pane) = self.pane(window, cx) else {
217 return;
218 };
219 let new_selections = self.editor_selections(window, cx);
220 let result = pane.update(cx, |pane, cx| {
221 let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
222 if self.search.helix_select {
223 search_bar.update(cx, |search_bar, cx| {
224 search_bar.select_all_matches(&Default::default(), window, cx)
225 });
226 return None;
227 }
228 search_bar.update(cx, |search_bar, cx| {
229 let mut count = self.search.count;
230 let direction = self.search.direction;
231 search_bar.has_active_match();
232 let new_head = new_selections.last()?.start;
233 let is_different_head = self
234 .search
235 .prior_selections
236 .last()
237 .is_none_or(|range| range.start != new_head);
238
239 if is_different_head {
240 count = count.saturating_sub(1)
241 }
242 self.search.count = 1;
243 search_bar.select_match(direction, count, window, cx);
244 search_bar.focus_editor(&Default::default(), window, cx);
245
246 let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
247 let prior_mode = self.search.prior_mode;
248 let prior_operator = self.search.prior_operator.take();
249
250 let query = search_bar.query(cx).into();
251 Vim::globals(cx).registers.insert('/', query);
252 Some((prior_selections, prior_mode, prior_operator))
253 })
254 });
255
256 let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
257 return;
258 };
259
260 let new_selections = self.editor_selections(window, cx);
261
262 // If the active editor has changed during a search, don't panic.
263 if prior_selections.iter().any(|s| {
264 self.update_editor(cx, |_, editor, cx| {
265 !s.start
266 .is_valid(&editor.snapshot(window, cx).buffer_snapshot())
267 })
268 .unwrap_or(true)
269 }) {
270 prior_selections.clear();
271 }
272
273 if prior_mode != self.mode {
274 self.switch_mode(prior_mode, true, window, cx);
275 }
276 if let Some(operator) = prior_operator {
277 self.push_operator(operator, window, cx);
278 };
279 self.search_motion(
280 Motion::ZedSearchResult {
281 prior_selections,
282 new_selections,
283 },
284 window,
285 cx,
286 );
287 }
288
289 pub fn move_to_match_internal(
290 &mut self,
291 direction: Direction,
292 window: &mut Window,
293 cx: &mut Context<Self>,
294 ) {
295 let Some(pane) = self.pane(window, cx) else {
296 return;
297 };
298 let count = Vim::take_count(cx).unwrap_or(1);
299 Vim::take_forced_motion(cx);
300 let prior_selections = self.editor_selections(window, cx);
301
302 let success = pane.update(cx, |pane, cx| {
303 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
304 return false;
305 };
306 search_bar.update(cx, |search_bar, cx| {
307 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
308 return false;
309 }
310 search_bar.select_match(direction, count, window, cx);
311 true
312 })
313 });
314 if !success {
315 return;
316 }
317
318 let new_selections = self.editor_selections(window, cx);
319 self.search_motion(
320 Motion::ZedSearchResult {
321 prior_selections,
322 new_selections,
323 },
324 window,
325 cx,
326 );
327 }
328
329 pub fn move_to_internal(
330 &mut self,
331 direction: Direction,
332 case_sensitive: bool,
333 whole_word: bool,
334 regex: bool,
335 window: &mut Window,
336 cx: &mut Context<Self>,
337 ) {
338 let Some(pane) = self.pane(window, cx) else {
339 return;
340 };
341 let count = Vim::take_count(cx).unwrap_or(1);
342 Vim::take_forced_motion(cx);
343 let prior_selections = self.editor_selections(window, cx);
344 let cursor_word = self.editor_cursor_word(window, cx);
345 let vim = cx.entity();
346
347 let searched = pane.update(cx, |pane, cx| {
348 self.search.direction = direction;
349 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
350 return false;
351 };
352 let search = search_bar.update(cx, |search_bar, cx| {
353 let mut options = SearchOptions::NONE;
354 if case_sensitive {
355 options |= SearchOptions::CASE_SENSITIVE;
356 }
357 if regex {
358 options |= SearchOptions::REGEX;
359 }
360 if whole_word {
361 options |= SearchOptions::WHOLE_WORD;
362 }
363 if !search_bar.show(window, cx) {
364 return None;
365 }
366 let Some(query) = search_bar
367 .query_suggestion(window, cx)
368 .or_else(|| cursor_word)
369 else {
370 drop(search_bar.search("", None, false, window, cx));
371 return None;
372 };
373
374 let query = regex::escape(&query);
375 Some(search_bar.search(&query, Some(options), true, window, cx))
376 });
377
378 let Some(search) = search else { return false };
379
380 let search_bar = search_bar.downgrade();
381 cx.spawn_in(window, async move |_, cx| {
382 search.await?;
383 search_bar.update_in(cx, |search_bar, window, cx| {
384 search_bar.select_match(direction, count, window, cx);
385
386 vim.update(cx, |vim, cx| {
387 let new_selections = vim.editor_selections(window, cx);
388 vim.search_motion(
389 Motion::ZedSearchResult {
390 prior_selections,
391 new_selections,
392 },
393 window,
394 cx,
395 )
396 });
397 })?;
398 anyhow::Ok(())
399 })
400 .detach_and_log_err(cx);
401 true
402 });
403 if !searched {
404 self.clear_operator(window, cx)
405 }
406
407 if self.mode.is_visual() {
408 self.switch_mode(Mode::Normal, false, window, cx)
409 }
410 }
411
412 fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
413 let Some(pane) = self.pane(window, cx) else {
414 return;
415 };
416 pane.update(cx, |pane, cx| {
417 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
418 let search = search_bar.update(cx, |search_bar, cx| {
419 if !search_bar.show(window, cx) {
420 return None;
421 }
422 let mut query = action.query.clone();
423 if query.is_empty() {
424 query = search_bar.query(cx);
425 };
426
427 let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
428 if search_bar.should_use_smartcase_search(cx) {
429 options.set(
430 SearchOptions::CASE_SENSITIVE,
431 search_bar.is_contains_uppercase(&query),
432 );
433 }
434
435 Some(search_bar.search(&query, Some(options), true, window, cx))
436 });
437 let Some(search) = search else { return };
438 let search_bar = search_bar.downgrade();
439 let direction = if action.backwards {
440 Direction::Prev
441 } else {
442 Direction::Next
443 };
444 cx.spawn_in(window, async move |_, cx| {
445 search.await?;
446 search_bar.update_in(cx, |search_bar, window, cx| {
447 search_bar.select_match(direction, 1, window, cx)
448 })?;
449 anyhow::Ok(())
450 })
451 .detach_and_log_err(cx);
452 }
453 })
454 }
455
456 fn replace_command(
457 &mut self,
458 action: &ReplaceCommand,
459 window: &mut Window,
460 cx: &mut Context<Self>,
461 ) {
462 let replacement = action.replacement.clone();
463 let Some(((pane, workspace), editor)) = self
464 .pane(window, cx)
465 .zip(self.workspace(window))
466 .zip(self.editor())
467 else {
468 return;
469 };
470 if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
471 let range = action.range.buffer_range(vim, editor, window, cx)?;
472 let snapshot = editor.snapshot(window, cx);
473 let snapshot = snapshot.buffer_snapshot();
474 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
475 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
476 ..snapshot.anchor_after(end_point);
477 editor.set_search_within_ranges(&[range], cx);
478 anyhow::Ok(())
479 }) {
480 workspace.update(cx, |workspace, cx| {
481 result.notify_err(workspace, cx);
482 })
483 }
484 let Some(search_bar) = pane.update(cx, |pane, cx| {
485 pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
486 }) else {
487 return;
488 };
489 let mut options = SearchOptions::REGEX;
490 let search = search_bar.update(cx, |search_bar, cx| {
491 if !search_bar.show(window, cx) {
492 return None;
493 }
494
495 let search = if replacement.search.is_empty() {
496 search_bar.query(cx)
497 } else {
498 replacement.search
499 };
500
501 if let Some(case) = replacement.case_sensitive {
502 options.set(SearchOptions::CASE_SENSITIVE, case)
503 } else if search_bar.should_use_smartcase_search(cx) {
504 options.set(
505 SearchOptions::CASE_SENSITIVE,
506 search_bar.is_contains_uppercase(&search),
507 );
508 } else {
509 options.set(SearchOptions::CASE_SENSITIVE, false)
510 }
511
512 if !replacement.flag_g {
513 options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
514 }
515
516 search_bar.set_replacement(Some(&replacement.replacement), cx);
517 if replacement.flag_c {
518 search_bar.focus_replace(window, cx);
519 }
520 Some(search_bar.search(&search, Some(options), true, window, cx))
521 });
522 if replacement.flag_n {
523 self.move_cursor(
524 Motion::StartOfLine {
525 display_lines: false,
526 },
527 None,
528 window,
529 cx,
530 );
531 return;
532 }
533 let Some(search) = search else { return };
534 let search_bar = search_bar.downgrade();
535 cx.spawn_in(window, async move |vim, cx| {
536 search.await?;
537 search_bar.update_in(cx, |search_bar, window, cx| {
538 if replacement.flag_c {
539 search_bar.select_first_match(window, cx);
540 return;
541 }
542 search_bar.select_last_match(window, cx);
543 search_bar.replace_all(&Default::default(), window, cx);
544 editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
545 let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
546 vim.update(cx, |vim, cx| {
547 vim.move_cursor(
548 Motion::StartOfLine {
549 display_lines: false,
550 },
551 None,
552 window,
553 cx,
554 )
555 })
556 .ok();
557
558 // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
559 // this is not properly supported outside of vim mode, and
560 // not disabling it makes the "Replace All Matches" button
561 // actually replace only the first match on each line.
562 options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
563 search_bar.set_search_options(options, cx);
564 })
565 })
566 .detach_and_log_err(cx);
567 }
568}
569
570impl Replacement {
571 // convert a vim query into something more usable by zed.
572 // we don't attempt to fully convert between the two regex syntaxes,
573 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
574 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
575 pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
576 let delimiter = chars
577 .next()
578 .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
579
580 let mut search = String::new();
581 let mut replacement = String::new();
582 let mut flags = String::new();
583
584 let mut buffer = &mut search;
585
586 let mut escaped = false;
587 // 0 - parsing search
588 // 1 - parsing replacement
589 // 2 - parsing flags
590 let mut phase = 0;
591
592 for c in chars {
593 if escaped {
594 escaped = false;
595 if phase == 1 && c.is_ascii_digit() {
596 buffer.push('$')
597 // unescape escaped parens
598 } else if phase == 0 && (c == '(' || c == ')') {
599 } else if c != delimiter {
600 buffer.push('\\')
601 }
602 buffer.push(c)
603 } else if c == '\\' {
604 escaped = true;
605 } else if c == delimiter {
606 if phase == 0 {
607 buffer = &mut replacement;
608 phase = 1;
609 } else if phase == 1 {
610 buffer = &mut flags;
611 phase = 2;
612 } else {
613 break;
614 }
615 } else {
616 // escape unescaped parens
617 if phase == 0 && (c == '(' || c == ')') {
618 buffer.push('\\')
619 }
620 buffer.push(c)
621 }
622 }
623
624 let mut replacement = Replacement {
625 search,
626 replacement,
627 case_sensitive: None,
628 flag_g: false,
629 flag_n: false,
630 flag_c: false,
631 };
632
633 for c in flags.chars() {
634 match c {
635 'g' => replacement.flag_g = true,
636 'n' => replacement.flag_n = true,
637 'c' => replacement.flag_c = true,
638 'i' => replacement.case_sensitive = Some(false),
639 'I' => replacement.case_sensitive = Some(true),
640 _ => {}
641 }
642 }
643
644 Some(replacement)
645 }
646}
647
648#[cfg(test)]
649mod test {
650 use std::time::Duration;
651
652 use crate::{
653 state::Mode,
654 test::{NeovimBackedTestContext, VimTestContext},
655 };
656 use editor::{DisplayPoint, display_map::DisplayRow};
657
658 use indoc::indoc;
659 use search::BufferSearchBar;
660 use settings::SettingsStore;
661
662 #[gpui::test]
663 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
664 let mut cx = VimTestContext::new(cx, true).await;
665 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
666
667 cx.simulate_keystrokes("*");
668 cx.run_until_parked();
669 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
670
671 cx.simulate_keystrokes("*");
672 cx.run_until_parked();
673 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
674
675 cx.simulate_keystrokes("#");
676 cx.run_until_parked();
677 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
678
679 cx.simulate_keystrokes("#");
680 cx.run_until_parked();
681 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
682
683 cx.simulate_keystrokes("2 *");
684 cx.run_until_parked();
685 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
686
687 cx.simulate_keystrokes("g *");
688 cx.run_until_parked();
689 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
690
691 cx.simulate_keystrokes("n");
692 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
693
694 cx.simulate_keystrokes("g #");
695 cx.run_until_parked();
696 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
697 }
698
699 #[gpui::test]
700 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
701 let mut cx = VimTestContext::new(cx, true).await;
702
703 cx.update_global(|store: &mut SettingsStore, cx| {
704 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
705 });
706
707 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
708
709 cx.simulate_keystrokes("*");
710 cx.run_until_parked();
711 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
712
713 cx.simulate_keystrokes("*");
714 cx.run_until_parked();
715 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
716
717 cx.simulate_keystrokes("#");
718 cx.run_until_parked();
719 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
720
721 cx.simulate_keystrokes("3 *");
722 cx.run_until_parked();
723 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
724
725 cx.simulate_keystrokes("g *");
726 cx.run_until_parked();
727 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
728
729 cx.simulate_keystrokes("n");
730 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
731
732 cx.simulate_keystrokes("g #");
733 cx.run_until_parked();
734 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
735 }
736
737 #[gpui::test]
738 async fn test_search(cx: &mut gpui::TestAppContext) {
739 let mut cx = VimTestContext::new(cx, true).await;
740
741 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
742 cx.simulate_keystrokes("/ c c");
743
744 let search_bar = cx.workspace(|workspace, _, cx| {
745 workspace
746 .active_pane()
747 .read(cx)
748 .toolbar()
749 .read(cx)
750 .item_of_type::<BufferSearchBar>()
751 .expect("Buffer search bar should be deployed")
752 });
753
754 cx.update_entity(search_bar, |bar, _window, cx| {
755 assert_eq!(bar.query(cx), "cc");
756 });
757
758 cx.run_until_parked();
759
760 cx.update_editor(|editor, window, cx| {
761 let highlights = editor.all_text_background_highlights(window, cx);
762 assert_eq!(3, highlights.len());
763 assert_eq!(
764 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
765 highlights[0].0
766 )
767 });
768
769 cx.simulate_keystrokes("enter");
770 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
771
772 // n to go to next/N to go to previous
773 cx.simulate_keystrokes("n");
774 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
775 cx.simulate_keystrokes("shift-n");
776 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
777
778 // ?<enter> to go to previous
779 cx.simulate_keystrokes("? enter");
780 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
781 cx.simulate_keystrokes("? enter");
782 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
783
784 // /<enter> to go to next
785 cx.simulate_keystrokes("/ enter");
786 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
787
788 // ?{search}<enter> to search backwards
789 cx.simulate_keystrokes("? b enter");
790 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
791
792 // works with counts
793 cx.simulate_keystrokes("4 / c");
794 cx.simulate_keystrokes("enter");
795 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
796
797 // check that searching resumes from cursor, not previous match
798 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
799 cx.simulate_keystrokes("/ d");
800 cx.simulate_keystrokes("enter");
801 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
802 cx.update_editor(|editor, window, cx| {
803 editor.move_to_beginning(&Default::default(), window, cx)
804 });
805 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
806 cx.simulate_keystrokes("/ b");
807 cx.simulate_keystrokes("enter");
808 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
809
810 // check that searching switches to normal mode if in visual mode
811 cx.set_state("ˇone two one", Mode::Normal);
812 cx.simulate_keystrokes("v l l");
813 cx.assert_editor_state("«oneˇ» two one");
814 cx.simulate_keystrokes("*");
815 cx.assert_state("one two ˇone", Mode::Normal);
816
817 // check that a backward search after last match works correctly
818 cx.set_state("aa\naa\nbbˇ", Mode::Normal);
819 cx.simulate_keystrokes("? a a");
820 cx.simulate_keystrokes("enter");
821 cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
822
823 // check that searching with unable search wrap
824 cx.update_global(|store: &mut SettingsStore, cx| {
825 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
826 });
827 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
828 cx.simulate_keystrokes("/ c c enter");
829
830 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
831
832 // n to go to next/N to go to previous
833 cx.simulate_keystrokes("n");
834 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
835 cx.simulate_keystrokes("shift-n");
836 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
837
838 // ?<enter> to go to previous
839 cx.simulate_keystrokes("? enter");
840 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
841 cx.simulate_keystrokes("? enter");
842 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
843 }
844
845 #[gpui::test]
846 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
847 let mut cx = VimTestContext::new(cx, false).await;
848 cx.cx.set_state("ˇone one one one");
849 cx.run_until_parked();
850 cx.simulate_keystrokes("cmd-f");
851 cx.run_until_parked();
852
853 cx.assert_editor_state("«oneˇ» one one one");
854 cx.simulate_keystrokes("enter");
855 cx.assert_editor_state("one «oneˇ» one one");
856 cx.simulate_keystrokes("shift-enter");
857 cx.assert_editor_state("«oneˇ» one one one");
858 }
859
860 #[gpui::test]
861 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
862 let mut cx = NeovimBackedTestContext::new(cx).await;
863
864 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
865 cx.simulate_shared_keystrokes("v 3 l *").await;
866 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
867 }
868
869 #[gpui::test]
870 async fn test_d_search(cx: &mut gpui::TestAppContext) {
871 let mut cx = NeovimBackedTestContext::new(cx).await;
872
873 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
874 cx.simulate_shared_keystrokes("d / c d").await;
875 cx.simulate_shared_keystrokes("enter").await;
876 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
877 }
878
879 #[gpui::test]
880 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
881 let mut cx = NeovimBackedTestContext::new(cx).await;
882
883 cx.set_shared_state("ˇa b a b a b a").await;
884 cx.simulate_shared_keystrokes("*").await;
885 cx.simulate_shared_keystrokes("n").await;
886 cx.shared_state().await.assert_eq("a b a b ˇa b a");
887 cx.simulate_shared_keystrokes("#").await;
888 cx.shared_state().await.assert_eq("a b ˇa b a b a");
889 cx.simulate_shared_keystrokes("n").await;
890 cx.shared_state().await.assert_eq("ˇa b a b a b a");
891 }
892
893 #[gpui::test]
894 async fn test_v_search(cx: &mut gpui::TestAppContext) {
895 let mut cx = NeovimBackedTestContext::new(cx).await;
896
897 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
898 cx.simulate_shared_keystrokes("v / c d").await;
899 cx.simulate_shared_keystrokes("enter").await;
900 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
901
902 cx.set_shared_state("a a aˇ a a a").await;
903 cx.simulate_shared_keystrokes("v / a").await;
904 cx.simulate_shared_keystrokes("enter").await;
905 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
906 cx.simulate_shared_keystrokes("/ enter").await;
907 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
908 cx.simulate_shared_keystrokes("? enter").await;
909 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
910 cx.simulate_shared_keystrokes("? enter").await;
911 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
912 cx.simulate_shared_keystrokes("/ enter").await;
913 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
914 cx.simulate_shared_keystrokes("/ enter").await;
915 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
916 }
917
918 #[gpui::test]
919 async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
920 let mut cx = NeovimBackedTestContext::new(cx).await;
921
922 cx.set_shared_state("ˇaa aa").await;
923 cx.simulate_shared_keystrokes("v / a a").await;
924 cx.simulate_shared_keystrokes("enter").await;
925 cx.shared_state().await.assert_eq("«aa aˇ»a");
926 }
927
928 #[gpui::test]
929 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
930 let mut cx = NeovimBackedTestContext::new(cx).await;
931
932 cx.set_shared_state(indoc! {
933 "ˇone two
934 three four
935 five six
936 "
937 })
938 .await;
939 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
940 cx.simulate_shared_keystrokes("enter").await;
941 cx.shared_state().await.assert_eq(indoc! {
942 "«one twoˇ»
943 «three fˇ»our
944 five six
945 "
946 });
947 }
948
949 #[gpui::test]
950 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
951 let mut cx = NeovimBackedTestContext::new(cx).await;
952
953 cx.set_shared_state(indoc! {
954 "ˇa
955 a
956 a
957 a
958 a
959 a
960 a
961 "
962 })
963 .await;
964 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
965 cx.simulate_shared_keystrokes("enter").await;
966 cx.shared_state().await.assert_eq(indoc! {
967 "a
968 ba
969 ba
970 ba
971 ˇba
972 a
973 a
974 "
975 });
976
977 cx.simulate_shared_keystrokes("/ a").await;
978 cx.simulate_shared_keystrokes("enter").await;
979 cx.shared_state().await.assert_eq(indoc! {
980 "a
981 ba
982 ba
983 ba
984 bˇa
985 a
986 a
987 "
988 });
989 }
990
991 #[gpui::test]
992 async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
993 let mut cx = NeovimBackedTestContext::new(cx).await;
994 cx.set_shared_state(indoc! {
995 "ˇaa aa aa"
996 })
997 .await;
998
999 cx.simulate_shared_keystrokes("/ a a").await;
1000 cx.simulate_shared_keystrokes("enter").await;
1001
1002 cx.shared_state().await.assert_eq(indoc! {
1003 "aa ˇaa aa"
1004 });
1005
1006 cx.simulate_shared_keystrokes("left / a a").await;
1007 cx.simulate_shared_keystrokes("enter").await;
1008
1009 cx.shared_state().await.assert_eq(indoc! {
1010 "aa ˇaa aa"
1011 });
1012 }
1013
1014 #[gpui::test]
1015 async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1016 let mut cx = NeovimBackedTestContext::new(cx).await;
1017 cx.set_shared_state(indoc! {
1018 "ˇaa
1019 bb
1020 aa"
1021 })
1022 .await;
1023
1024 cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1025 cx.simulate_shared_keystrokes("enter").await;
1026
1027 cx.shared_state().await.assert_eq(indoc! {
1028 "ˇaa
1029 bb
1030 aa"
1031 });
1032
1033 let search_bar = cx.update_workspace(|workspace, _, cx| {
1034 workspace.active_pane().update(cx, |pane, cx| {
1035 pane.toolbar()
1036 .read(cx)
1037 .item_of_type::<BufferSearchBar>()
1038 .unwrap()
1039 })
1040 });
1041 cx.update_entity(search_bar, |search_bar, _, cx| {
1042 assert!(!search_bar.is_dismissed());
1043 assert_eq!(search_bar.query(cx), "bb".to_string());
1044 assert_eq!(search_bar.replacement(cx), "dd".to_string());
1045 })
1046 }
1047
1048 #[gpui::test]
1049 async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1050 let mut cx = NeovimBackedTestContext::new(cx).await;
1051 cx.set_shared_state(indoc! {
1052 "ˇaa aa aa aa
1053 aa
1054 aa"
1055 })
1056 .await;
1057
1058 cx.simulate_shared_keystrokes(": s / a a / b b").await;
1059 cx.simulate_shared_keystrokes("enter").await;
1060 cx.shared_state().await.assert_eq(indoc! {
1061 "ˇbb aa aa aa
1062 aa
1063 aa"
1064 });
1065 cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1066 cx.simulate_shared_keystrokes("enter").await;
1067 cx.shared_state().await.assert_eq(indoc! {
1068 "ˇbb bb bb bb
1069 aa
1070 aa"
1071 });
1072 }
1073
1074 #[gpui::test]
1075 async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1076 let mut cx = VimTestContext::new(cx, true).await;
1077 cx.set_state(
1078 indoc! {
1079 "ˇaa
1080 aa
1081 aa"
1082 },
1083 Mode::Normal,
1084 );
1085
1086 cx.simulate_keystrokes("v j : s / a a / d d / c");
1087 cx.simulate_keystrokes("enter");
1088
1089 cx.assert_state(
1090 indoc! {
1091 "ˇaa
1092 aa
1093 aa"
1094 },
1095 Mode::Normal,
1096 );
1097
1098 cx.simulate_keystrokes("enter");
1099
1100 cx.assert_state(
1101 indoc! {
1102 "dd
1103 ˇaa
1104 aa"
1105 },
1106 Mode::Normal,
1107 );
1108
1109 cx.simulate_keystrokes("enter");
1110 cx.assert_state(
1111 indoc! {
1112 "dd
1113 ddˇ
1114 aa"
1115 },
1116 Mode::Normal,
1117 );
1118 cx.simulate_keystrokes("enter");
1119 cx.assert_state(
1120 indoc! {
1121 "dd
1122 ddˇ
1123 aa"
1124 },
1125 Mode::Normal,
1126 );
1127 }
1128
1129 #[gpui::test]
1130 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1131 let mut cx = NeovimBackedTestContext::new(cx).await;
1132
1133 cx.set_shared_state(indoc! {
1134 "ˇa
1135 a
1136 a
1137 a
1138 a
1139 a
1140 a
1141 "
1142 })
1143 .await;
1144 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1145 cx.simulate_shared_keystrokes("enter").await;
1146 cx.shared_state().await.assert_eq(indoc! {
1147 "a
1148 b
1149 b
1150 b
1151 ˇb
1152 a
1153 a
1154 "
1155 });
1156 cx.executor().advance_clock(Duration::from_millis(250));
1157 cx.run_until_parked();
1158
1159 cx.simulate_shared_keystrokes("/ a enter").await;
1160 cx.shared_state().await.assert_eq(indoc! {
1161 "a
1162 b
1163 b
1164 b
1165 b
1166 ˇa
1167 a
1168 "
1169 });
1170 }
1171}