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