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