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