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}