search.rs

  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// }