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.workspace_state.search = SearchState {
74 direction,
75 count,
76 initial_query: query.clone(),
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.workspace_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 state = &mut vim.workspace_state.search;
97 let mut count = state.count;
98 let direction = state.direction;
99
100 // in the case that the query has changed, the search bar
101 // will have selected the next match already.
102 if (search_bar.query(cx) != state.initial_query)
103 && state.direction == Direction::Next
104 {
105 count = count.saturating_sub(1)
106 }
107 state.count = 1;
108 search_bar.select_match(direction, count, cx);
109 search_bar.focus_editor(&Default::default(), cx);
110 });
111 }
112 });
113 })
114}
115
116pub fn move_to_internal(
117 workspace: &mut Workspace,
118 direction: Direction,
119 whole_word: bool,
120 cx: &mut ViewContext<Workspace>,
121) {
122 Vim::update(cx, |vim, cx| {
123 let pane = workspace.active_pane().clone();
124 let count = vim.pop_number_operator(cx).unwrap_or(1);
125 pane.update(cx, |pane, cx| {
126 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
127 let search = search_bar.update(cx, |search_bar, cx| {
128 let mut options = SearchOptions::CASE_SENSITIVE;
129 options.set(SearchOptions::WHOLE_WORD, whole_word);
130 if search_bar.show(cx) {
131 search_bar
132 .query_suggestion(cx)
133 .map(|query| search_bar.search(&query, Some(options), cx))
134 } else {
135 None
136 }
137 });
138
139 if let Some(search) = search {
140 let search_bar = search_bar.downgrade();
141 cx.spawn(|_, mut cx| async move {
142 search.await?;
143 search_bar.update(&mut cx, |search_bar, cx| {
144 search_bar.select_match(direction, count, cx)
145 })?;
146 anyhow::Ok(())
147 })
148 .detach_and_log_err(cx);
149 }
150 }
151 });
152 vim.clear_operator(cx);
153 });
154}
155
156#[cfg(test)]
157mod test {
158 use std::sync::Arc;
159
160 use editor::DisplayPoint;
161 use search::BufferSearchBar;
162
163 use crate::{state::Mode, test::VimTestContext};
164
165 #[gpui::test]
166 async fn test_move_to_next(
167 cx: &mut gpui::TestAppContext,
168 deterministic: Arc<gpui::executor::Deterministic>,
169 ) {
170 let mut cx = VimTestContext::new(cx, true).await;
171 cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
172
173 cx.simulate_keystrokes(["*"]);
174 deterministic.run_until_parked();
175 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
176
177 cx.simulate_keystrokes(["*"]);
178 deterministic.run_until_parked();
179 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
180
181 cx.simulate_keystrokes(["#"]);
182 deterministic.run_until_parked();
183 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
184
185 cx.simulate_keystrokes(["#"]);
186 deterministic.run_until_parked();
187 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
188
189 cx.simulate_keystrokes(["2", "*"]);
190 deterministic.run_until_parked();
191 cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
192
193 cx.simulate_keystrokes(["g", "*"]);
194 deterministic.run_until_parked();
195 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
196
197 cx.simulate_keystrokes(["n"]);
198 cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
199
200 cx.simulate_keystrokes(["g", "#"]);
201 deterministic.run_until_parked();
202 cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
203 }
204
205 #[gpui::test]
206 async fn test_search(
207 cx: &mut gpui::TestAppContext,
208 deterministic: Arc<gpui::executor::Deterministic>,
209 ) {
210 let mut cx = VimTestContext::new(cx, true).await;
211
212 cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
213 cx.simulate_keystrokes(["/", "c", "c"]);
214
215 let search_bar = cx.workspace(|workspace, cx| {
216 workspace
217 .active_pane()
218 .read(cx)
219 .toolbar()
220 .read(cx)
221 .item_of_type::<BufferSearchBar>()
222 .expect("Buffer search bar should be deployed")
223 });
224
225 search_bar.read_with(cx.cx, |bar, cx| {
226 assert_eq!(bar.query(cx), "cc");
227 });
228
229 deterministic.run_until_parked();
230
231 cx.update_editor(|editor, cx| {
232 let highlights = editor.all_background_highlights(cx);
233 assert_eq!(3, highlights.len());
234 assert_eq!(
235 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
236 highlights[0].0
237 )
238 });
239
240 cx.simulate_keystrokes(["enter"]);
241 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
242
243 // n to go to next/N to go to previous
244 cx.simulate_keystrokes(["n"]);
245 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
246 cx.simulate_keystrokes(["shift-n"]);
247 cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
248
249 // ?<enter> to go to previous
250 cx.simulate_keystrokes(["?", "enter"]);
251 deterministic.run_until_parked();
252 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
253 cx.simulate_keystrokes(["?", "enter"]);
254 deterministic.run_until_parked();
255 cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
256
257 // /<enter> to go to next
258 cx.simulate_keystrokes(["/", "enter"]);
259 deterministic.run_until_parked();
260 cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
261
262 // ?{search}<enter> to search backwards
263 cx.simulate_keystrokes(["?", "b", "enter"]);
264 deterministic.run_until_parked();
265 cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
266
267 // works with counts
268 cx.simulate_keystrokes(["4", "/", "c"]);
269 deterministic.run_until_parked();
270 cx.simulate_keystrokes(["enter"]);
271 cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
272
273 // check that searching resumes from cursor, not previous match
274 cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
275 cx.simulate_keystrokes(["/", "d"]);
276 deterministic.run_until_parked();
277 cx.simulate_keystrokes(["enter"]);
278 cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
279 cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
280 cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
281 cx.simulate_keystrokes(["/", "b"]);
282 deterministic.run_until_parked();
283 cx.simulate_keystrokes(["enter"]);
284 cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
285 }
286
287 #[gpui::test]
288 async fn test_non_vim_search(
289 cx: &mut gpui::TestAppContext,
290 deterministic: Arc<gpui::executor::Deterministic>,
291 ) {
292 let mut cx = VimTestContext::new(cx, false).await;
293 cx.set_state("ˇone one one one", Mode::Normal);
294 cx.simulate_keystrokes(["cmd-f"]);
295 deterministic.run_until_parked();
296
297 cx.assert_editor_state("«oneˇ» one one one");
298 cx.simulate_keystrokes(["enter"]);
299 cx.assert_editor_state("one «oneˇ» one one");
300 cx.simulate_keystrokes(["shift-enter"]);
301 cx.assert_editor_state("«oneˇ» one one one");
302 }
303}