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