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