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