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