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