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).buffer_snapshot;
473 let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
474 let range = snapshot.anchor_before(Point::new(range.start.0, 0))
475 ..snapshot.anchor_after(end_point);
476 editor.set_search_within_ranges(&[range], cx);
477 anyhow::Ok(())
478 }) {
479 workspace.update(cx, |workspace, cx| {
480 result.notify_err(workspace, cx);
481 })
482 }
483 let Some(search_bar) = pane.update(cx, |pane, cx| {
484 pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
485 }) else {
486 return;
487 };
488 let mut options = SearchOptions::REGEX;
489 let search = search_bar.update(cx, |search_bar, cx| {
490 if !search_bar.show(window, cx) {
491 return None;
492 }
493
494 let search = if replacement.search.is_empty() {
495 search_bar.query(cx)
496 } else {
497 replacement.search
498 };
499
500 if let Some(case) = replacement.case_sensitive {
501 options.set(SearchOptions::CASE_SENSITIVE, case)
502 } else if search_bar.should_use_smartcase_search(cx) {
503 options.set(
504 SearchOptions::CASE_SENSITIVE,
505 search_bar.is_contains_uppercase(&search),
506 );
507 } else {
508 options.set(SearchOptions::CASE_SENSITIVE, false)
509 }
510
511 if !replacement.flag_g {
512 options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
513 }
514
515 search_bar.set_replacement(Some(&replacement.replacement), cx);
516 if replacement.flag_c {
517 search_bar.focus_replace(window, cx);
518 }
519 Some(search_bar.search(&search, Some(options), true, window, cx))
520 });
521 if replacement.flag_n {
522 self.move_cursor(
523 Motion::StartOfLine {
524 display_lines: false,
525 },
526 None,
527 window,
528 cx,
529 );
530 return;
531 }
532 let Some(search) = search else { return };
533 let search_bar = search_bar.downgrade();
534 cx.spawn_in(window, async move |vim, cx| {
535 search.await?;
536 search_bar.update_in(cx, |search_bar, window, cx| {
537 if replacement.flag_c {
538 search_bar.select_first_match(window, cx);
539 return;
540 }
541 search_bar.select_last_match(window, cx);
542 search_bar.replace_all(&Default::default(), window, cx);
543 editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
544 let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
545 vim.update(cx, |vim, cx| {
546 vim.move_cursor(
547 Motion::StartOfLine {
548 display_lines: false,
549 },
550 None,
551 window,
552 cx,
553 )
554 })
555 .ok();
556
557 // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
558 // this is not properly supported outside of vim mode, and
559 // not disabling it makes the "Replace All Matches" button
560 // actually replace only the first match on each line.
561 options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
562 search_bar.set_search_options(options, cx);
563 })
564 })
565 .detach_and_log_err(cx);
566 }
567}
568
569impl Replacement {
570 // convert a vim query into something more usable by zed.
571 // we don't attempt to fully convert between the two regex syntaxes,
572 // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
573 // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
574 pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
575 let delimiter = chars
576 .next()
577 .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
578
579 let mut search = String::new();
580 let mut replacement = String::new();
581 let mut flags = String::new();
582
583 let mut buffer = &mut search;
584
585 let mut escaped = false;
586 // 0 - parsing search
587 // 1 - parsing replacement
588 // 2 - parsing flags
589 let mut phase = 0;
590
591 for c in chars {
592 if escaped {
593 escaped = false;
594 if phase == 1 && c.is_ascii_digit() {
595 buffer.push('$')
596 // unescape escaped parens
597 } else if phase == 0 && (c == '(' || c == ')') {
598 } else if c != delimiter {
599 buffer.push('\\')
600 }
601 buffer.push(c)
602 } else if c == '\\' {
603 escaped = true;
604 } else if c == delimiter {
605 if phase == 0 {
606 buffer = &mut replacement;
607 phase = 1;
608 } else if phase == 1 {
609 buffer = &mut flags;
610 phase = 2;
611 } else {
612 break;
613 }
614 } else {
615 // escape unescaped parens
616 if phase == 0 && (c == '(' || c == ')') {
617 buffer.push('\\')
618 }
619 buffer.push(c)
620 }
621 }
622
623 let mut replacement = Replacement {
624 search,
625 replacement,
626 case_sensitive: None,
627 flag_g: false,
628 flag_n: false,
629 flag_c: false,
630 };
631
632 for c in flags.chars() {
633 match c {
634 'g' => replacement.flag_g = true,
635 'n' => replacement.flag_n = true,
636 'c' => replacement.flag_c = true,
637 'i' => replacement.case_sensitive = Some(false),
638 'I' => replacement.case_sensitive = Some(true),
639 _ => {}
640 }
641 }
642
643 Some(replacement)
644 }
645}
646
647#[cfg(test)]
648mod test {
649 use std::time::Duration;
650
651 use crate::{
652 state::Mode,
653 test::{NeovimBackedTestContext, VimTestContext},
654 };
655 use editor::{DisplayPoint, display_map::DisplayRow};
656
657 use indoc::indoc;
658 use search::BufferSearchBar;
659 use settings::SettingsStore;
660
661 #[gpui::test]
662 async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
663 let mut cx = VimTestContext::new(cx, true).await;
664 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
665
666 cx.simulate_keystrokes("*");
667 cx.run_until_parked();
668 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
669
670 cx.simulate_keystrokes("*");
671 cx.run_until_parked();
672 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
673
674 cx.simulate_keystrokes("#");
675 cx.run_until_parked();
676 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
677
678 cx.simulate_keystrokes("#");
679 cx.run_until_parked();
680 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
681
682 cx.simulate_keystrokes("2 *");
683 cx.run_until_parked();
684 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
685
686 cx.simulate_keystrokes("g *");
687 cx.run_until_parked();
688 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
689
690 cx.simulate_keystrokes("n");
691 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
692
693 cx.simulate_keystrokes("g #");
694 cx.run_until_parked();
695 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
696 }
697
698 #[gpui::test]
699 async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
700 let mut cx = VimTestContext::new(cx, true).await;
701
702 cx.update_global(|store: &mut SettingsStore, cx| {
703 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
704 });
705
706 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
707
708 cx.simulate_keystrokes("*");
709 cx.run_until_parked();
710 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
711
712 cx.simulate_keystrokes("*");
713 cx.run_until_parked();
714 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
715
716 cx.simulate_keystrokes("#");
717 cx.run_until_parked();
718 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
719
720 cx.simulate_keystrokes("3 *");
721 cx.run_until_parked();
722 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
723
724 cx.simulate_keystrokes("g *");
725 cx.run_until_parked();
726 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
727
728 cx.simulate_keystrokes("n");
729 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
730
731 cx.simulate_keystrokes("g #");
732 cx.run_until_parked();
733 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
734 }
735
736 #[gpui::test]
737 async fn test_search(cx: &mut gpui::TestAppContext) {
738 let mut cx = VimTestContext::new(cx, true).await;
739
740 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
741 cx.simulate_keystrokes("/ c c");
742
743 let search_bar = cx.workspace(|workspace, _, cx| {
744 workspace
745 .active_pane()
746 .read(cx)
747 .toolbar()
748 .read(cx)
749 .item_of_type::<BufferSearchBar>()
750 .expect("Buffer search bar should be deployed")
751 });
752
753 cx.update_entity(search_bar, |bar, _window, cx| {
754 assert_eq!(bar.query(cx), "cc");
755 });
756
757 cx.run_until_parked();
758
759 cx.update_editor(|editor, window, cx| {
760 let highlights = editor.all_text_background_highlights(window, cx);
761 assert_eq!(3, highlights.len());
762 assert_eq!(
763 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
764 highlights[0].0
765 )
766 });
767
768 cx.simulate_keystrokes("enter");
769 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
770
771 // n to go to next/N to go to previous
772 cx.simulate_keystrokes("n");
773 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
774 cx.simulate_keystrokes("shift-n");
775 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
776
777 // ?<enter> to go to previous
778 cx.simulate_keystrokes("? enter");
779 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
780 cx.simulate_keystrokes("? enter");
781 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
782
783 // /<enter> to go to next
784 cx.simulate_keystrokes("/ enter");
785 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
786
787 // ?{search}<enter> to search backwards
788 cx.simulate_keystrokes("? b enter");
789 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
790
791 // works with counts
792 cx.simulate_keystrokes("4 / c");
793 cx.simulate_keystrokes("enter");
794 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
795
796 // check that searching resumes from cursor, not previous match
797 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
798 cx.simulate_keystrokes("/ d");
799 cx.simulate_keystrokes("enter");
800 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
801 cx.update_editor(|editor, window, cx| {
802 editor.move_to_beginning(&Default::default(), window, cx)
803 });
804 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
805 cx.simulate_keystrokes("/ b");
806 cx.simulate_keystrokes("enter");
807 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
808
809 // check that searching switches to normal mode if in visual mode
810 cx.set_state("ˇone two one", Mode::Normal);
811 cx.simulate_keystrokes("v l l");
812 cx.assert_editor_state("«oneˇ» two one");
813 cx.simulate_keystrokes("*");
814 cx.assert_state("one two ˇone", Mode::Normal);
815
816 // check that a backward search after last match works correctly
817 cx.set_state("aa\naa\nbbˇ", Mode::Normal);
818 cx.simulate_keystrokes("? a a");
819 cx.simulate_keystrokes("enter");
820 cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
821
822 // check that searching with unable search wrap
823 cx.update_global(|store: &mut SettingsStore, cx| {
824 store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
825 });
826 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
827 cx.simulate_keystrokes("/ c c enter");
828
829 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
830
831 // n to go to next/N to go to previous
832 cx.simulate_keystrokes("n");
833 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
834 cx.simulate_keystrokes("shift-n");
835 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
836
837 // ?<enter> to go to previous
838 cx.simulate_keystrokes("? enter");
839 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
840 cx.simulate_keystrokes("? enter");
841 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
842 }
843
844 #[gpui::test]
845 async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
846 let mut cx = VimTestContext::new(cx, false).await;
847 cx.cx.set_state("ˇone one one one");
848 cx.run_until_parked();
849 cx.simulate_keystrokes("cmd-f");
850 cx.run_until_parked();
851
852 cx.assert_editor_state("«oneˇ» one one one");
853 cx.simulate_keystrokes("enter");
854 cx.assert_editor_state("one «oneˇ» one one");
855 cx.simulate_keystrokes("shift-enter");
856 cx.assert_editor_state("«oneˇ» one one one");
857 }
858
859 #[gpui::test]
860 async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
861 let mut cx = NeovimBackedTestContext::new(cx).await;
862
863 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
864 cx.simulate_shared_keystrokes("v 3 l *").await;
865 cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
866 }
867
868 #[gpui::test]
869 async fn test_d_search(cx: &mut gpui::TestAppContext) {
870 let mut cx = NeovimBackedTestContext::new(cx).await;
871
872 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
873 cx.simulate_shared_keystrokes("d / c d").await;
874 cx.simulate_shared_keystrokes("enter").await;
875 cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
876 }
877
878 #[gpui::test]
879 async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
880 let mut cx = NeovimBackedTestContext::new(cx).await;
881
882 cx.set_shared_state("ˇa b a b a b a").await;
883 cx.simulate_shared_keystrokes("*").await;
884 cx.simulate_shared_keystrokes("n").await;
885 cx.shared_state().await.assert_eq("a b a b ˇa b a");
886 cx.simulate_shared_keystrokes("#").await;
887 cx.shared_state().await.assert_eq("a b ˇa b a b a");
888 cx.simulate_shared_keystrokes("n").await;
889 cx.shared_state().await.assert_eq("ˇa b a b a b a");
890 }
891
892 #[gpui::test]
893 async fn test_v_search(cx: &mut gpui::TestAppContext) {
894 let mut cx = NeovimBackedTestContext::new(cx).await;
895
896 cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
897 cx.simulate_shared_keystrokes("v / c d").await;
898 cx.simulate_shared_keystrokes("enter").await;
899 cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
900
901 cx.set_shared_state("a a aˇ a a a").await;
902 cx.simulate_shared_keystrokes("v / a").await;
903 cx.simulate_shared_keystrokes("enter").await;
904 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
905 cx.simulate_shared_keystrokes("/ enter").await;
906 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
907 cx.simulate_shared_keystrokes("? enter").await;
908 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
909 cx.simulate_shared_keystrokes("? enter").await;
910 cx.shared_state().await.assert_eq("a a «ˇa »a a a");
911 cx.simulate_shared_keystrokes("/ enter").await;
912 cx.shared_state().await.assert_eq("a a a« aˇ» a a");
913 cx.simulate_shared_keystrokes("/ enter").await;
914 cx.shared_state().await.assert_eq("a a a« a aˇ» a");
915 }
916
917 #[gpui::test]
918 async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
919 let mut cx = NeovimBackedTestContext::new(cx).await;
920
921 cx.set_shared_state("ˇaa aa").await;
922 cx.simulate_shared_keystrokes("v / a a").await;
923 cx.simulate_shared_keystrokes("enter").await;
924 cx.shared_state().await.assert_eq("«aa aˇ»a");
925 }
926
927 #[gpui::test]
928 async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
929 let mut cx = NeovimBackedTestContext::new(cx).await;
930
931 cx.set_shared_state(indoc! {
932 "ˇone two
933 three four
934 five six
935 "
936 })
937 .await;
938 cx.simulate_shared_keystrokes("ctrl-v j / f").await;
939 cx.simulate_shared_keystrokes("enter").await;
940 cx.shared_state().await.assert_eq(indoc! {
941 "«one twoˇ»
942 «three fˇ»our
943 five six
944 "
945 });
946 }
947
948 #[gpui::test]
949 async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
950 let mut cx = NeovimBackedTestContext::new(cx).await;
951
952 cx.set_shared_state(indoc! {
953 "ˇa
954 a
955 a
956 a
957 a
958 a
959 a
960 "
961 })
962 .await;
963 cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
964 cx.simulate_shared_keystrokes("enter").await;
965 cx.shared_state().await.assert_eq(indoc! {
966 "a
967 ba
968 ba
969 ba
970 ˇba
971 a
972 a
973 "
974 });
975
976 cx.simulate_shared_keystrokes("/ a").await;
977 cx.simulate_shared_keystrokes("enter").await;
978 cx.shared_state().await.assert_eq(indoc! {
979 "a
980 ba
981 ba
982 ba
983 bˇa
984 a
985 a
986 "
987 });
988 }
989
990 #[gpui::test]
991 async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
992 let mut cx = NeovimBackedTestContext::new(cx).await;
993 cx.set_shared_state(indoc! {
994 "ˇaa aa aa"
995 })
996 .await;
997
998 cx.simulate_shared_keystrokes("/ a a").await;
999 cx.simulate_shared_keystrokes("enter").await;
1000
1001 cx.shared_state().await.assert_eq(indoc! {
1002 "aa ˇaa aa"
1003 });
1004
1005 cx.simulate_shared_keystrokes("left / a a").await;
1006 cx.simulate_shared_keystrokes("enter").await;
1007
1008 cx.shared_state().await.assert_eq(indoc! {
1009 "aa ˇaa aa"
1010 });
1011 }
1012
1013 #[gpui::test]
1014 async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1015 let mut cx = NeovimBackedTestContext::new(cx).await;
1016 cx.set_shared_state(indoc! {
1017 "ˇaa
1018 bb
1019 aa"
1020 })
1021 .await;
1022
1023 cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1024 cx.simulate_shared_keystrokes("enter").await;
1025
1026 cx.shared_state().await.assert_eq(indoc! {
1027 "ˇaa
1028 bb
1029 aa"
1030 });
1031
1032 let search_bar = cx.update_workspace(|workspace, _, cx| {
1033 workspace.active_pane().update(cx, |pane, cx| {
1034 pane.toolbar()
1035 .read(cx)
1036 .item_of_type::<BufferSearchBar>()
1037 .unwrap()
1038 })
1039 });
1040 cx.update_entity(search_bar, |search_bar, _, cx| {
1041 assert!(!search_bar.is_dismissed());
1042 assert_eq!(search_bar.query(cx), "bb".to_string());
1043 assert_eq!(search_bar.replacement(cx), "dd".to_string());
1044 })
1045 }
1046
1047 #[gpui::test]
1048 async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1049 let mut cx = NeovimBackedTestContext::new(cx).await;
1050 cx.set_shared_state(indoc! {
1051 "ˇaa aa aa aa
1052 aa
1053 aa"
1054 })
1055 .await;
1056
1057 cx.simulate_shared_keystrokes(": s / a a / b b").await;
1058 cx.simulate_shared_keystrokes("enter").await;
1059 cx.shared_state().await.assert_eq(indoc! {
1060 "ˇbb aa aa aa
1061 aa
1062 aa"
1063 });
1064 cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1065 cx.simulate_shared_keystrokes("enter").await;
1066 cx.shared_state().await.assert_eq(indoc! {
1067 "ˇbb bb bb bb
1068 aa
1069 aa"
1070 });
1071 }
1072
1073 #[gpui::test]
1074 async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1075 let mut cx = VimTestContext::new(cx, true).await;
1076 cx.set_state(
1077 indoc! {
1078 "ˇaa
1079 aa
1080 aa"
1081 },
1082 Mode::Normal,
1083 );
1084
1085 cx.simulate_keystrokes("v j : s / a a / d d / c");
1086 cx.simulate_keystrokes("enter");
1087
1088 cx.assert_state(
1089 indoc! {
1090 "ˇaa
1091 aa
1092 aa"
1093 },
1094 Mode::Normal,
1095 );
1096
1097 cx.simulate_keystrokes("enter");
1098
1099 cx.assert_state(
1100 indoc! {
1101 "dd
1102 ˇaa
1103 aa"
1104 },
1105 Mode::Normal,
1106 );
1107
1108 cx.simulate_keystrokes("enter");
1109 cx.assert_state(
1110 indoc! {
1111 "dd
1112 ddˇ
1113 aa"
1114 },
1115 Mode::Normal,
1116 );
1117 cx.simulate_keystrokes("enter");
1118 cx.assert_state(
1119 indoc! {
1120 "dd
1121 ddˇ
1122 aa"
1123 },
1124 Mode::Normal,
1125 );
1126 }
1127
1128 #[gpui::test]
1129 async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1130 let mut cx = NeovimBackedTestContext::new(cx).await;
1131
1132 cx.set_shared_state(indoc! {
1133 "ˇa
1134 a
1135 a
1136 a
1137 a
1138 a
1139 a
1140 "
1141 })
1142 .await;
1143 cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1144 cx.simulate_shared_keystrokes("enter").await;
1145 cx.shared_state().await.assert_eq(indoc! {
1146 "a
1147 b
1148 b
1149 b
1150 ˇb
1151 a
1152 a
1153 "
1154 });
1155 cx.executor().advance_clock(Duration::from_millis(250));
1156 cx.run_until_parked();
1157
1158 cx.simulate_shared_keystrokes("/ a enter").await;
1159 cx.shared_state().await.assert_eq(indoc! {
1160 "a
1161 b
1162 b
1163 b
1164 b
1165 ˇa
1166 a
1167 "
1168 });
1169 }
1170}