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