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