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