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