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