1use gpui::{actions, Action, AppContext, ViewContext};
2use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
3use serde_derive::Deserialize;
4use workspace::{searchable::Direction, Pane, Workspace};
5
6use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
7
8#[derive(Action, Clone, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub(crate) struct MoveToNext {
11 #[serde(default)]
12 partial_word: bool,
13}
14
15#[derive(Action, Clone, Deserialize, PartialEq)]
16#[serde(rename_all = "camelCase")]
17pub(crate) struct MoveToPrev {
18 #[serde(default)]
19 partial_word: bool,
20}
21
22#[derive(Action, Clone, Deserialize, PartialEq)]
23pub(crate) struct Search {
24 #[serde(default)]
25 backwards: bool,
26}
27
28#[derive(Action, Debug, Clone, PartialEq, Deserialize)]
29pub struct FindCommand {
30 pub query: String,
31 pub backwards: bool,
32}
33
34#[derive(Action, Debug, Clone, PartialEq, Deserialize)]
35pub struct ReplaceCommand {
36 pub query: String,
37}
38
39#[derive(Debug, Default)]
40struct Replacement {
41 search: String,
42 replacement: String,
43 should_replace_all: bool,
44 is_case_sensitive: bool,
45}
46
47actions!(SearchSubmit);
48
49pub(crate) fn init(cx: &mut AppContext) {
50 // todo!()
51 // cx.add_action(move_to_next);
52 // cx.add_action(move_to_prev);
53 // cx.add_action(search);
54 // cx.add_action(search_submit);
55 // cx.add_action(search_deploy);
56
57 // cx.add_action(find_command);
58 // cx.add_action(replace_command);
59}
60
61fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
62 move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
63}
64
65fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
66 move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
67}
68
69fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
70 let pane = workspace.active_pane().clone();
71 let direction = if action.backwards {
72 Direction::Prev
73 } else {
74 Direction::Next
75 };
76 Vim::update(cx, |vim, cx| {
77 let count = vim.take_count(cx).unwrap_or(1);
78 pane.update(cx, |pane, cx| {
79 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
80 search_bar.update(cx, |search_bar, cx| {
81 if !search_bar.show(cx) {
82 return;
83 }
84 let query = search_bar.query(cx);
85
86 search_bar.select_query(cx);
87 cx.focus_self();
88
89 if query.is_empty() {
90 search_bar.set_replacement(None, cx);
91 search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
92 search_bar.activate_search_mode(SearchMode::Regex, cx);
93 }
94 vim.workspace_state.search = SearchState {
95 direction,
96 count,
97 initial_query: query.clone(),
98 };
99 });
100 }
101 })
102 })
103}
104
105// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
106fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
107 Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
108 cx.propagate();
109}
110
111fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
112 Vim::update(cx, |vim, cx| {
113 let pane = workspace.active_pane().clone();
114 pane.update(cx, |pane, cx| {
115 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
116 search_bar.update(cx, |search_bar, cx| {
117 let state = &mut vim.workspace_state.search;
118 let mut count = state.count;
119 let direction = state.direction;
120
121 // in the case that the query has changed, the search bar
122 // will have selected the next match already.
123 if (search_bar.query(cx) != state.initial_query)
124 && state.direction == Direction::Next
125 {
126 count = count.saturating_sub(1)
127 }
128 state.count = 1;
129 search_bar.select_match(direction, count, cx);
130 search_bar.focus_editor(&Default::default(), cx);
131 });
132 }
133 });
134 })
135}
136
137pub fn move_to_internal(
138 workspace: &mut Workspace,
139 direction: Direction,
140 whole_word: bool,
141 cx: &mut ViewContext<Workspace>,
142) {
143 Vim::update(cx, |vim, cx| {
144 let pane = workspace.active_pane().clone();
145 let count = vim.take_count(cx).unwrap_or(1);
146 pane.update(cx, |pane, cx| {
147 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
148 let search = search_bar.update(cx, |search_bar, cx| {
149 let mut options = SearchOptions::CASE_SENSITIVE;
150 options.set(SearchOptions::WHOLE_WORD, whole_word);
151 if search_bar.show(cx) {
152 search_bar
153 .query_suggestion(cx)
154 .map(|query| search_bar.search(&query, Some(options), cx))
155 } else {
156 None
157 }
158 });
159
160 if let Some(search) = search {
161 let search_bar = search_bar.downgrade();
162 cx.spawn(|_, mut cx| async move {
163 search.await?;
164 search_bar.update(&mut cx, |search_bar, cx| {
165 search_bar.select_match(direction, count, cx)
166 })?;
167 anyhow::Ok(())
168 })
169 .detach_and_log_err(cx);
170 }
171 }
172 });
173 vim.clear_operator(cx);
174 });
175}
176
177fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
178 let pane = workspace.active_pane().clone();
179 pane.update(cx, |pane, cx| {
180 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
181 let search = search_bar.update(cx, |search_bar, cx| {
182 if !search_bar.show(cx) {
183 return None;
184 }
185 let mut query = action.query.clone();
186 if query == "" {
187 query = search_bar.query(cx);
188 };
189
190 search_bar.activate_search_mode(SearchMode::Regex, cx);
191 Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
192 });
193 let Some(search) = search else { return };
194 let search_bar = search_bar.downgrade();
195 let direction = if action.backwards {
196 Direction::Prev
197 } else {
198 Direction::Next
199 };
200 cx.spawn(|_, mut cx| async move {
201 search.await?;
202 search_bar.update(&mut cx, |search_bar, cx| {
203 search_bar.select_match(direction, 1, cx)
204 })?;
205 anyhow::Ok(())
206 })
207 .detach_and_log_err(cx);
208 }
209 })
210}
211
212fn replace_command(
213 workspace: &mut Workspace,
214 action: &ReplaceCommand,
215 cx: &mut ViewContext<Workspace>,
216) {
217 let replacement = parse_replace_all(&action.query);
218 let pane = workspace.active_pane().clone();
219 pane.update(cx, |pane, cx| {
220 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
221 return;
222 };
223 let search = search_bar.update(cx, |search_bar, cx| {
224 if !search_bar.show(cx) {
225 return None;
226 }
227
228 let mut options = SearchOptions::default();
229 if replacement.is_case_sensitive {
230 options.set(SearchOptions::CASE_SENSITIVE, true)
231 }
232 let search = if replacement.search == "" {
233 search_bar.query(cx)
234 } else {
235 replacement.search
236 };
237
238 search_bar.set_replacement(Some(&replacement.replacement), cx);
239 search_bar.activate_search_mode(SearchMode::Regex, cx);
240 Some(search_bar.search(&search, Some(options), cx))
241 });
242 let Some(search) = search else { return };
243 let search_bar = search_bar.downgrade();
244 cx.spawn(|_, mut cx| async move {
245 search.await?;
246 search_bar.update(&mut cx, |search_bar, cx| {
247 if replacement.should_replace_all {
248 search_bar.select_last_match(cx);
249 search_bar.replace_all(&Default::default(), cx);
250 Vim::update(cx, |vim, cx| {
251 move_cursor(
252 vim,
253 Motion::StartOfLine {
254 display_lines: false,
255 },
256 None,
257 cx,
258 )
259 })
260 }
261 })?;
262 anyhow::Ok(())
263 })
264 .detach_and_log_err(cx);
265 })
266}
267
268// convert a vim query into something more usable by zed.
269// we don't attempt to fully convert between the two regex syntaxes,
270// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
271// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
272fn parse_replace_all(query: &str) -> Replacement {
273 let mut chars = query.chars();
274 if Some('%') != chars.next() || Some('s') != chars.next() {
275 return Replacement::default();
276 }
277
278 let Some(delimeter) = chars.next() else {
279 return Replacement::default();
280 };
281
282 let mut search = String::new();
283 let mut replacement = String::new();
284 let mut flags = String::new();
285
286 let mut buffer = &mut search;
287
288 let mut escaped = false;
289 // 0 - parsing search
290 // 1 - parsing replacement
291 // 2 - parsing flags
292 let mut phase = 0;
293
294 for c in chars {
295 if escaped {
296 escaped = false;
297 if phase == 1 && c.is_digit(10) {
298 buffer.push('$')
299 // unescape escaped parens
300 } else if phase == 0 && c == '(' || c == ')' {
301 } else if c != delimeter {
302 buffer.push('\\')
303 }
304 buffer.push(c)
305 } else if c == '\\' {
306 escaped = true;
307 } else if c == delimeter {
308 if phase == 0 {
309 buffer = &mut replacement;
310 phase = 1;
311 } else if phase == 1 {
312 buffer = &mut flags;
313 phase = 2;
314 } else {
315 break;
316 }
317 } else {
318 // escape unescaped parens
319 if phase == 0 && c == '(' || c == ')' {
320 buffer.push('\\')
321 }
322 buffer.push(c)
323 }
324 }
325
326 let mut replacement = Replacement {
327 search,
328 replacement,
329 should_replace_all: true,
330 is_case_sensitive: true,
331 };
332
333 for c in flags.chars() {
334 match c {
335 'g' | 'I' => {}
336 'c' | 'n' => replacement.should_replace_all = false,
337 'i' => replacement.is_case_sensitive = false,
338 _ => {}
339 }
340 }
341
342 replacement
343}
344
345// #[cfg(test)]
346// mod test {
347// use std::sync::Arc;
348
349// use editor::DisplayPoint;
350// use search::BufferSearchBar;
351
352// use crate::{state::Mode, test::VimTestContext};
353
354// #[gpui::test]
355// async fn test_move_to_next(
356// cx: &mut gpui::TestAppContext,
357// deterministic: Arc<gpui::executor::Deterministic>,
358// ) {
359// let mut cx = VimTestContext::new(cx, true).await;
360// cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
361
362// cx.simulate_keystrokes(["*"]);
363// deterministic.run_until_parked();
364// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
365
366// cx.simulate_keystrokes(["*"]);
367// deterministic.run_until_parked();
368// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
369
370// cx.simulate_keystrokes(["#"]);
371// deterministic.run_until_parked();
372// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
373
374// cx.simulate_keystrokes(["#"]);
375// deterministic.run_until_parked();
376// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
377
378// cx.simulate_keystrokes(["2", "*"]);
379// deterministic.run_until_parked();
380// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
381
382// cx.simulate_keystrokes(["g", "*"]);
383// deterministic.run_until_parked();
384// cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
385
386// cx.simulate_keystrokes(["n"]);
387// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
388
389// cx.simulate_keystrokes(["g", "#"]);
390// deterministic.run_until_parked();
391// cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
392// }
393
394// #[gpui::test]
395// async fn test_search(
396// cx: &mut gpui::TestAppContext,
397// deterministic: Arc<gpui::executor::Deterministic>,
398// ) {
399// let mut cx = VimTestContext::new(cx, true).await;
400
401// cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
402// cx.simulate_keystrokes(["/", "c", "c"]);
403
404// let search_bar = cx.workspace(|workspace, cx| {
405// workspace
406// .active_pane()
407// .read(cx)
408// .toolbar()
409// .read(cx)
410// .item_of_type::<BufferSearchBar>()
411// .expect("Buffer search bar should be deployed")
412// });
413
414// search_bar.read_with(cx.cx, |bar, cx| {
415// assert_eq!(bar.query(cx), "cc");
416// });
417
418// deterministic.run_until_parked();
419
420// cx.update_editor(|editor, cx| {
421// let highlights = editor.all_text_background_highlights(cx);
422// assert_eq!(3, highlights.len());
423// assert_eq!(
424// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
425// highlights[0].0
426// )
427// });
428
429// cx.simulate_keystrokes(["enter"]);
430// cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
431
432// // n to go to next/N to go to previous
433// cx.simulate_keystrokes(["n"]);
434// cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
435// cx.simulate_keystrokes(["shift-n"]);
436// cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
437
438// // ?<enter> to go to previous
439// cx.simulate_keystrokes(["?", "enter"]);
440// deterministic.run_until_parked();
441// cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
442// cx.simulate_keystrokes(["?", "enter"]);
443// deterministic.run_until_parked();
444// cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
445
446// // /<enter> to go to next
447// cx.simulate_keystrokes(["/", "enter"]);
448// deterministic.run_until_parked();
449// cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
450
451// // ?{search}<enter> to search backwards
452// cx.simulate_keystrokes(["?", "b", "enter"]);
453// deterministic.run_until_parked();
454// cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
455
456// // works with counts
457// cx.simulate_keystrokes(["4", "/", "c"]);
458// deterministic.run_until_parked();
459// cx.simulate_keystrokes(["enter"]);
460// cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
461
462// // check that searching resumes from cursor, not previous match
463// cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
464// cx.simulate_keystrokes(["/", "d"]);
465// deterministic.run_until_parked();
466// cx.simulate_keystrokes(["enter"]);
467// cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
468// cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
469// cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
470// cx.simulate_keystrokes(["/", "b"]);
471// deterministic.run_until_parked();
472// cx.simulate_keystrokes(["enter"]);
473// cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
474// }
475
476// #[gpui::test]
477// async fn test_non_vim_search(
478// cx: &mut gpui::TestAppContext,
479// deterministic: Arc<gpui::executor::Deterministic>,
480// ) {
481// let mut cx = VimTestContext::new(cx, false).await;
482// cx.set_state("ˇone one one one", Mode::Normal);
483// cx.simulate_keystrokes(["cmd-f"]);
484// deterministic.run_until_parked();
485
486// cx.assert_editor_state("«oneˇ» one one one");
487// cx.simulate_keystrokes(["enter"]);
488// cx.assert_editor_state("one «oneˇ» one one");
489// cx.simulate_keystrokes(["shift-enter"]);
490// cx.assert_editor_state("«oneˇ» one one one");
491// }
492// }