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