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