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