1use gpui::{actions, impl_actions, AppContext, ViewContext};
2use search::{buffer_search, BufferSearchBar, SearchOptions};
3use serde_derive::Deserialize;
4use workspace::{searchable::Direction, Pane, Workspace};
5
6use crate::{state::SearchState, 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]);
29actions!(vim, [SearchSubmit]);
30
31pub(crate) fn init(cx: &mut AppContext) {
32 cx.add_action(move_to_next);
33 cx.add_action(move_to_prev);
34 cx.add_action(search);
35 cx.add_action(search_submit);
36 cx.add_action(search_deploy);
37}
38
39fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
40 move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
41}
42
43fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
44 move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
45}
46
47fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
48 let pane = workspace.active_pane().clone();
49 let direction = if action.backwards {
50 Direction::Prev
51 } else {
52 Direction::Next
53 };
54 Vim::update(cx, |vim, cx| {
55 let count = vim.pop_number_operator(cx).unwrap_or(1);
56 pane.update(cx, |pane, cx| {
57 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
58 search_bar.update(cx, |search_bar, cx| {
59 if !search_bar.show(cx) {
60 return;
61 }
62 let query = search_bar.query(cx);
63
64 search_bar.select_query(cx);
65 cx.focus_self();
66
67 if query.is_empty() {
68 search_bar.set_search_options(
69 SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
70 cx,
71 );
72 }
73 vim.state.search = SearchState {
74 direction,
75 count,
76 initial_query: query,
77 };
78 });
79 }
80 })
81 })
82}
83
84// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
85fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
86 Vim::update(cx, |vim, _| vim.state.search = Default::default());
87 cx.propagate_action();
88}
89
90fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
91 Vim::update(cx, |vim, cx| {
92 let pane = workspace.active_pane().clone();
93 pane.update(cx, |pane, cx| {
94 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
95 search_bar.update(cx, |search_bar, cx| {
96 let mut state = &mut vim.state.search;
97 let mut count = state.count;
98
99 // in the case that the query has changed, the search bar
100 // will have selected the next match already.
101 if (search_bar.query(cx) != state.initial_query)
102 && state.direction == Direction::Next
103 {
104 count = count.saturating_sub(1)
105 }
106 search_bar.select_match(state.direction, count, cx);
107 state.count = 1;
108 search_bar.focus_editor(&Default::default(), cx);
109 });
110 }
111 });
112 })
113}
114
115pub fn move_to_internal(
116 workspace: &mut Workspace,
117 direction: Direction,
118 whole_word: bool,
119 cx: &mut ViewContext<Workspace>,
120) {
121 Vim::update(cx, |vim, cx| {
122 let pane = workspace.active_pane().clone();
123 let count = vim.pop_number_operator(cx).unwrap_or(1);
124 pane.update(cx, |pane, cx| {
125 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
126 let search = search_bar.update(cx, |search_bar, cx| {
127 let mut options = SearchOptions::CASE_SENSITIVE;
128 options.set(SearchOptions::WHOLE_WORD, whole_word);
129 if search_bar.show(cx) {
130 search_bar
131 .query_suggestion(cx)
132 .map(|query| search_bar.search(&query, Some(options), cx))
133 } else {
134 None
135 }
136 });
137
138 if let Some(search) = search {
139 let search_bar = search_bar.downgrade();
140 cx.spawn(|_, mut cx| async move {
141 search.await?;
142 search_bar.update(&mut cx, |search_bar, cx| {
143 search_bar.select_match(direction, count, cx)
144 })?;
145 anyhow::Ok(())
146 })
147 .detach_and_log_err(cx);
148 }
149 }
150 });
151 vim.clear_operator(cx);
152 });
153}
154
155#[cfg(test)]
156mod test {
157 use std::sync::Arc;
158
159 use editor::DisplayPoint;
160 use search::BufferSearchBar;
161
162 use crate::{state::Mode, test::VimTestContext};
163
164 #[gpui::test]
165 async fn test_move_to_next(
166 cx: &mut gpui::TestAppContext,
167 deterministic: Arc<gpui::executor::Deterministic>,
168 ) {
169 let mut cx = VimTestContext::new(cx, true).await;
170 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
171
172 cx.simulate_keystrokes(["*"]);
173 deterministic.run_until_parked();
174 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
175
176 cx.simulate_keystrokes(["*"]);
177 deterministic.run_until_parked();
178 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
179
180 cx.simulate_keystrokes(["#"]);
181 deterministic.run_until_parked();
182 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
183
184 cx.simulate_keystrokes(["#"]);
185 deterministic.run_until_parked();
186 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
187
188 cx.simulate_keystrokes(["2", "*"]);
189 deterministic.run_until_parked();
190 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
191
192 cx.simulate_keystrokes(["g", "*"]);
193 deterministic.run_until_parked();
194 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
195
196 cx.simulate_keystrokes(["n"]);
197 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
198
199 cx.simulate_keystrokes(["g", "#"]);
200 deterministic.run_until_parked();
201 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
202 }
203
204 #[gpui::test]
205 async fn test_search(
206 cx: &mut gpui::TestAppContext,
207 deterministic: Arc<gpui::executor::Deterministic>,
208 ) {
209 let mut cx = VimTestContext::new(cx, true).await;
210
211 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
212 cx.simulate_keystrokes(["/", "c", "c"]);
213
214 let search_bar = cx.workspace(|workspace, cx| {
215 workspace
216 .active_pane()
217 .read(cx)
218 .toolbar()
219 .read(cx)
220 .item_of_type::<BufferSearchBar>()
221 .expect("Buffer search bar should be deployed")
222 });
223
224 search_bar.read_with(cx.cx, |bar, cx| {
225 assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
226 });
227
228 deterministic.run_until_parked();
229
230 cx.update_editor(|editor, cx| {
231 let highlights = editor.all_background_highlights(cx);
232 assert_eq!(3, highlights.len());
233 assert_eq!(
234 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
235 highlights[0].0
236 )
237 });
238
239 cx.simulate_keystrokes(["enter"]);
240 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
241
242 // n to go to next/N to go to previous
243 cx.simulate_keystrokes(["n"]);
244 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
245 cx.simulate_keystrokes(["shift-n"]);
246 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
247
248 // ?<enter> to go to previous
249 cx.simulate_keystrokes(["?", "enter"]);
250 deterministic.run_until_parked();
251 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
252 cx.simulate_keystrokes(["?", "enter"]);
253 deterministic.run_until_parked();
254 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
255
256 // /<enter> to go to next
257 cx.simulate_keystrokes(["/", "enter"]);
258 deterministic.run_until_parked();
259 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
260
261 // ?{search}<enter> to search backwards
262 cx.simulate_keystrokes(["?", "b", "enter"]);
263 deterministic.run_until_parked();
264 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
265
266 // works with counts
267 cx.simulate_keystrokes(["4", "/", "c"]);
268 deterministic.run_until_parked();
269 cx.simulate_keystrokes(["enter"]);
270 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
271
272 // check that searching resumes from cursor, not previous match
273 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
274 cx.simulate_keystrokes(["/", "d"]);
275 deterministic.run_until_parked();
276 cx.simulate_keystrokes(["enter"]);
277 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
278 cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
279 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
280 cx.simulate_keystrokes(["/", "b"]);
281 deterministic.run_until_parked();
282 cx.simulate_keystrokes(["enter"]);
283 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
284 }
285
286 #[gpui::test]
287 async fn test_non_vim_search(
288 cx: &mut gpui::TestAppContext,
289 deterministic: Arc<gpui::executor::Deterministic>,
290 ) {
291 let mut cx = VimTestContext::new(cx, false).await;
292 cx.set_state("ˇone one one one", Mode::Normal);
293 cx.simulate_keystrokes(["cmd-f"]);
294 deterministic.run_until_parked();
295
296 cx.assert_editor_state("«oneˇ» one one one");
297 cx.simulate_keystrokes(["enter"]);
298 cx.assert_editor_state("one «oneˇ» one one");
299 cx.simulate_keystrokes(["shift-enter"]);
300 cx.assert_editor_state("«oneˇ» one one one");
301 }
302}