1use gpui::{impl_actions, AppContext, ViewContext};
2use search::{BufferSearchBar, SearchOptions};
3use serde_derive::Deserialize;
4use workspace::{searchable::Direction, Workspace};
5
6use crate::Vim;
7
8#[derive(Clone, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub(crate) struct MoveToNext {
11 #[serde(default)]
12 partial_word: bool,
13}
14
15#[derive(Clone, Deserialize, PartialEq)]
16#[serde(rename_all = "camelCase")]
17pub(crate) struct MoveToPrev {
18 #[serde(default)]
19 partial_word: bool,
20}
21
22#[derive(Clone, Deserialize, PartialEq)]
23pub(crate) struct Search {
24 #[serde(default)]
25 backwards: bool,
26}
27
28impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
29
30pub(crate) fn init(cx: &mut AppContext) {
31 cx.add_action(move_to_next);
32 cx.add_action(move_to_prev);
33 cx.add_action(search);
34}
35
36fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
37 move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
38}
39
40fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
41 move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
42}
43
44fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
45 let pane = workspace.active_pane().clone();
46 pane.update(cx, |pane, cx| {
47 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
48 search_bar.update(cx, |search_bar, cx| {
49 let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
50 let direction = if action.backwards {
51 Direction::Prev
52 } else {
53 Direction::Next
54 };
55 search_bar.select_match(direction, cx);
56 search_bar.show_with_options(true, false, options, cx);
57 })
58 }
59 })
60}
61
62pub fn move_to_internal(
63 workspace: &mut Workspace,
64 direction: Direction,
65 whole_word: bool,
66 cx: &mut ViewContext<Workspace>,
67) {
68 Vim::update(cx, |vim, cx| {
69 let pane = workspace.active_pane().clone();
70 pane.update(cx, |pane, cx| {
71 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
72 search_bar.update(cx, |search_bar, cx| {
73 let mut options = SearchOptions::CASE_SENSITIVE;
74 options.set(SearchOptions::WHOLE_WORD, whole_word);
75 search_bar.select_word_under_cursor(direction, options, cx);
76 });
77 }
78 });
79 vim.clear_operator(cx);
80 });
81}
82
83#[cfg(test)]
84mod test {
85 use std::sync::Arc;
86
87 use editor::DisplayPoint;
88 use search::BufferSearchBar;
89
90 use crate::{state::Mode, test::VimTestContext};
91
92 #[gpui::test]
93 async fn test_move_to_next(
94 cx: &mut gpui::TestAppContext,
95 deterministic: Arc<gpui::executor::Deterministic>,
96 ) {
97 let mut cx = VimTestContext::new(cx, true).await;
98 let search_bar = cx.workspace(|workspace, cx| {
99 workspace
100 .active_pane()
101 .read(cx)
102 .toolbar()
103 .read(cx)
104 .item_of_type::<BufferSearchBar>()
105 .expect("Buffer search bar should be deployed")
106 });
107 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
108
109 cx.simulate_keystrokes(["*"]);
110 deterministic.run_until_parked();
111 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
112
113 cx.simulate_keystrokes(["*"]);
114 deterministic.run_until_parked();
115 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
116
117 cx.simulate_keystrokes(["#"]);
118 deterministic.run_until_parked();
119 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
120
121 cx.simulate_keystrokes(["#"]);
122 deterministic.run_until_parked();
123 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
124
125 cx.simulate_keystrokes(["g", "*"]);
126 deterministic.run_until_parked();
127 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
128
129 cx.simulate_keystrokes(["n"]);
130 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
131
132 cx.simulate_keystrokes(["g", "#"]);
133 deterministic.run_until_parked();
134 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
135 }
136
137 #[gpui::test]
138 async fn test_search(cx: &mut gpui::TestAppContext) {
139 let mut cx = VimTestContext::new(cx, true).await;
140
141 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
142 cx.simulate_keystrokes(["/", "c", "c"]);
143
144 let search_bar = cx.workspace(|workspace, cx| {
145 workspace
146 .active_pane()
147 .read(cx)
148 .toolbar()
149 .read(cx)
150 .item_of_type::<BufferSearchBar>()
151 .expect("Buffer search bar should be deployed")
152 });
153
154 search_bar.read_with(cx.cx, |bar, cx| {
155 assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
156 });
157
158 // wait for the query editor change event to fire.
159 search_bar.next_notification(&cx).await;
160
161 cx.update_editor(|editor, cx| {
162 let highlights = editor.all_background_highlights(cx);
163 assert_eq!(3, highlights.len());
164 assert_eq!(
165 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
166 highlights[0].0
167 )
168 });
169
170 cx.simulate_keystrokes(["enter"]);
171
172 // n to go to next/N to go to previous
173 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
174 cx.simulate_keystrokes(["n"]);
175 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
176 cx.simulate_keystrokes(["shift-n"]);
177
178 // ?<enter> to go to previous
179 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
180 cx.simulate_keystrokes(["?", "enter"]);
181 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
182 cx.simulate_keystrokes(["?", "enter"]);
183
184 // /<enter> to go to next
185 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
186 cx.simulate_keystrokes(["/", "enter"]);
187 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
188
189 // ?{search}<enter> to search backwards
190 cx.simulate_keystrokes(["?", "b", "enter"]);
191
192 // wait for the query editor change event to fire.
193 search_bar.next_notification(&cx).await;
194
195 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
196 }
197}