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