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