find.rs

  1use aho_corasick::AhoCorasickBuilder;
  2use editor::{char_kind, Editor, EditorSettings};
  3use gpui::{
  4    action, elements::*, keymap::Binding, Entity, MutableAppContext, RenderContext, View,
  5    ViewContext, ViewHandle,
  6};
  7use postage::watch;
  8use std::sync::Arc;
  9use workspace::{ItemViewHandle, Settings, Toolbar, Workspace};
 10
 11action!(Deploy);
 12action!(Cancel);
 13action!(ToggleMode, SearchMode);
 14
 15#[derive(Clone, Copy)]
 16pub enum SearchMode {
 17    WholeWord,
 18    CaseSensitive,
 19    Regex,
 20}
 21
 22pub fn init(cx: &mut MutableAppContext) {
 23    cx.add_bindings([
 24        Binding::new("cmd-f", Deploy, Some("Editor && mode == full")),
 25        Binding::new("escape", Cancel, Some("FindBar")),
 26    ]);
 27    cx.add_action(FindBar::deploy);
 28    cx.add_action(FindBar::cancel);
 29    cx.add_action(FindBar::toggle_mode);
 30}
 31
 32struct FindBar {
 33    settings: watch::Receiver<Settings>,
 34    query_editor: ViewHandle<Editor>,
 35    active_editor: Option<ViewHandle<Editor>>,
 36    case_sensitive_mode: bool,
 37    whole_word_mode: bool,
 38    regex_mode: bool,
 39}
 40
 41impl Entity for FindBar {
 42    type Event = ();
 43}
 44
 45impl View for FindBar {
 46    fn ui_name() -> &'static str {
 47        "FindBar"
 48    }
 49
 50    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 51        cx.focus(&self.query_editor);
 52    }
 53
 54    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 55        let theme = &self.settings.borrow().theme.find;
 56        Flex::row()
 57            .with_child(
 58                ChildView::new(&self.query_editor)
 59                    .contained()
 60                    .with_style(theme.editor.input.container)
 61                    .constrained()
 62                    .with_max_width(theme.editor.max_width)
 63                    .boxed(),
 64            )
 65            .with_child(
 66                Flex::row()
 67                    .with_child(self.render_mode_button("Aa", SearchMode::CaseSensitive, theme, cx))
 68                    .with_child(self.render_mode_button("|ab|", SearchMode::WholeWord, theme, cx))
 69                    .with_child(self.render_mode_button(".*", SearchMode::Regex, theme, cx))
 70                    .contained()
 71                    .with_style(theme.mode_button_group)
 72                    .boxed(),
 73            )
 74            .contained()
 75            .with_style(theme.container)
 76            .boxed()
 77    }
 78}
 79
 80impl Toolbar for FindBar {
 81    fn active_item_changed(
 82        &mut self,
 83        item: Option<Box<dyn ItemViewHandle>>,
 84        cx: &mut ViewContext<Self>,
 85    ) -> bool {
 86        self.active_editor = item.and_then(|item| item.act_as::<Editor>(cx));
 87        self.active_editor.is_some()
 88    }
 89}
 90
 91impl FindBar {
 92    fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
 93        let query_editor = cx.add_view(|cx| {
 94            Editor::single_line(
 95                {
 96                    let settings = settings.clone();
 97                    Arc::new(move |_| {
 98                        let settings = settings.borrow();
 99                        EditorSettings {
100                            style: settings.theme.find.editor.input.as_editor(),
101                            tab_size: settings.tab_size,
102                            soft_wrap: editor::SoftWrap::None,
103                        }
104                    })
105                },
106                cx,
107            )
108        });
109        cx.subscribe(&query_editor, Self::on_query_editor_event)
110            .detach();
111
112        Self {
113            query_editor,
114            active_editor: None,
115            case_sensitive_mode: false,
116            whole_word_mode: false,
117            regex_mode: false,
118            settings,
119        }
120    }
121
122    fn render_mode_button(
123        &self,
124        icon: &str,
125        mode: SearchMode,
126        theme: &theme::Find,
127        cx: &mut RenderContext<Self>,
128    ) -> ElementBox {
129        let is_active = self.is_mode_enabled(mode);
130        MouseEventHandler::new::<Self, _, _, _>(mode as usize, cx, |state, _| {
131            let style = match (is_active, state.hovered) {
132                (false, false) => &theme.mode_button,
133                (false, true) => &theme.hovered_mode_button,
134                (true, false) => &theme.active_mode_button,
135                (true, true) => &theme.active_hovered_mode_button,
136            };
137            Label::new(icon.to_string(), style.text.clone())
138                .contained()
139                .with_style(style.container)
140                .boxed()
141        })
142        .on_click(move |cx| cx.dispatch_action(ToggleMode(mode)))
143        .boxed()
144    }
145
146    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
147        let settings = workspace.settings();
148        workspace.active_pane().update(cx, |pane, cx| {
149            pane.show_toolbar(cx, |cx| FindBar::new(settings, cx));
150            if let Some(toolbar) = pane.active_toolbar() {
151                cx.focus(toolbar);
152            }
153        });
154    }
155
156    fn cancel(workspace: &mut Workspace, _: &Cancel, cx: &mut ViewContext<Workspace>) {
157        workspace
158            .active_pane()
159            .update(cx, |pane, cx| pane.hide_toolbar(cx));
160    }
161
162    fn is_mode_enabled(&self, mode: SearchMode) -> bool {
163        match mode {
164            SearchMode::WholeWord => self.whole_word_mode,
165            SearchMode::CaseSensitive => self.case_sensitive_mode,
166            SearchMode::Regex => self.regex_mode,
167        }
168    }
169
170    fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext<Self>) {
171        eprintln!("TOGGLE MODE");
172        let value = match mode {
173            SearchMode::WholeWord => &mut self.whole_word_mode,
174            SearchMode::CaseSensitive => &mut self.case_sensitive_mode,
175            SearchMode::Regex => &mut self.regex_mode,
176        };
177        *value = !*value;
178        cx.notify();
179    }
180
181    fn on_query_editor_event(
182        &mut self,
183        _: ViewHandle<Editor>,
184        _: &editor::Event,
185        cx: &mut ViewContext<Self>,
186    ) {
187        if let Some(editor) = &self.active_editor {
188            let search = self.query_editor.read(cx).text(cx);
189            let theme = &self.settings.borrow().theme.find;
190            editor.update(cx, |editor, cx| {
191                if search.is_empty() {
192                    editor.clear_highlighted_ranges::<Self>(cx);
193                    return;
194                }
195
196                let search = AhoCorasickBuilder::new()
197                    .auto_configure(&[&search])
198                    .ascii_case_insensitive(!self.case_sensitive_mode)
199                    .build(&[&search]);
200                let buffer = editor.buffer().read(cx).snapshot(cx);
201                let ranges = search
202                    .stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
203                    .filter_map(|mat| {
204                        let mat = mat.unwrap();
205
206                        if self.whole_word_mode {
207                            let prev_kind =
208                                buffer.reversed_chars_at(mat.start()).next().map(char_kind);
209                            let start_kind =
210                                char_kind(buffer.chars_at(mat.start()).next().unwrap());
211                            let end_kind =
212                                char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
213                            let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
214                            if Some(start_kind) != prev_kind && Some(end_kind) != next_kind {
215                                Some(
216                                    buffer.anchor_after(mat.start())
217                                        ..buffer.anchor_before(mat.end()),
218                                )
219                            } else {
220                                None
221                            }
222                        } else {
223                            Some(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()))
224                        }
225                    })
226                    .collect();
227                editor.highlight_ranges::<Self>(ranges, theme.match_background, cx);
228            });
229        }
230    }
231}