1use editor::{
2 combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt,
3 Autoscroll, DisplayPoint, Editor, ToPoint,
4};
5use fuzzy::StringMatch;
6use gpui::{
7 action,
8 elements::*,
9 geometry::vector::Vector2F,
10 keymap::{self, Binding},
11 AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
12 WeakViewHandle,
13};
14use language::Outline;
15use ordered_float::OrderedFloat;
16use postage::watch;
17use std::cmp::{self, Reverse};
18use workspace::{
19 menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
20 Settings, Workspace,
21};
22
23action!(Toggle);
24
25pub fn init(cx: &mut MutableAppContext) {
26 cx.add_bindings([
27 Binding::new("cmd-shift-O", Toggle, Some("Editor")),
28 Binding::new("escape", Toggle, Some("OutlineView")),
29 ]);
30 cx.add_action(OutlineView::toggle);
31 cx.add_action(OutlineView::confirm);
32 cx.add_action(OutlineView::select_prev);
33 cx.add_action(OutlineView::select_next);
34 cx.add_action(OutlineView::select_first);
35 cx.add_action(OutlineView::select_last);
36}
37
38struct OutlineView {
39 handle: WeakViewHandle<Self>,
40 active_editor: ViewHandle<Editor>,
41 outline: Outline<Anchor>,
42 selected_match_index: usize,
43 prev_scroll_position: Option<Vector2F>,
44 matches: Vec<StringMatch>,
45 query_editor: ViewHandle<Editor>,
46 list_state: UniformListState,
47 settings: watch::Receiver<Settings>,
48}
49
50pub enum Event {
51 Dismissed,
52}
53
54impl Entity for OutlineView {
55 type Event = Event;
56
57 fn release(&mut self, cx: &mut MutableAppContext) {
58 self.restore_active_editor(cx);
59 }
60}
61
62impl View for OutlineView {
63 fn ui_name() -> &'static str {
64 "OutlineView"
65 }
66
67 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
68 let mut cx = Self::default_keymap_context();
69 cx.set.insert("menu".into());
70 cx
71 }
72
73 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
74 let settings = self.settings.borrow();
75
76 Flex::new(Axis::Vertical)
77 .with_child(
78 Container::new(ChildView::new(&self.query_editor).boxed())
79 .with_style(settings.theme.selector.input_editor.container)
80 .boxed(),
81 )
82 .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
83 .contained()
84 .with_style(settings.theme.selector.container)
85 .constrained()
86 .with_max_width(800.0)
87 .with_max_height(1200.0)
88 .aligned()
89 .top()
90 .named("outline view")
91 }
92
93 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
94 cx.focus(&self.query_editor);
95 }
96}
97
98impl OutlineView {
99 fn new(
100 outline: Outline<Anchor>,
101 editor: ViewHandle<Editor>,
102 settings: watch::Receiver<Settings>,
103 cx: &mut ViewContext<Self>,
104 ) -> Self {
105 let query_editor = cx.add_view(|cx| {
106 Editor::single_line(
107 settings.clone(),
108 Some(|theme| theme.selector.input_editor.clone()),
109 cx,
110 )
111 });
112 cx.subscribe(&query_editor, Self::on_query_editor_event)
113 .detach();
114
115 let mut this = Self {
116 handle: cx.weak_handle(),
117 matches: Default::default(),
118 selected_match_index: 0,
119 prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
120 active_editor: editor,
121 outline,
122 query_editor,
123 list_state: Default::default(),
124 settings,
125 };
126 this.update_matches(cx);
127 this
128 }
129
130 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
131 if let Some(editor) = workspace
132 .active_item(cx)
133 .and_then(|item| item.downcast::<Editor>())
134 {
135 let settings = workspace.settings();
136 let buffer = editor
137 .read(cx)
138 .buffer()
139 .read(cx)
140 .read(cx)
141 .outline(Some(settings.borrow().theme.editor.syntax.as_ref()));
142 if let Some(outline) = buffer {
143 workspace.toggle_modal(cx, |cx, _| {
144 let view = cx.add_view(|cx| OutlineView::new(outline, editor, settings, cx));
145 cx.subscribe(&view, Self::on_event).detach();
146 view
147 })
148 }
149 }
150 }
151
152 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
153 if self.selected_match_index > 0 {
154 self.select(self.selected_match_index - 1, true, false, cx);
155 }
156 }
157
158 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
159 if self.selected_match_index + 1 < self.matches.len() {
160 self.select(self.selected_match_index + 1, true, false, cx);
161 }
162 }
163
164 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
165 self.select(0, true, false, cx);
166 }
167
168 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
169 self.select(self.matches.len().saturating_sub(1), true, false, cx);
170 }
171
172 fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
173 self.selected_match_index = index;
174 self.list_state.scroll_to(if center {
175 ScrollTarget::Center(index)
176 } else {
177 ScrollTarget::Show(index)
178 });
179 if navigate {
180 let selected_match = &self.matches[self.selected_match_index];
181 let outline_item = &self.outline.items[selected_match.candidate_id];
182 self.active_editor.update(cx, |active_editor, cx| {
183 let snapshot = active_editor.snapshot(cx).display_snapshot;
184 let buffer_snapshot = &snapshot.buffer_snapshot;
185 let start = outline_item.range.start.to_point(&buffer_snapshot);
186 let end = outline_item.range.end.to_point(&buffer_snapshot);
187 let display_rows = start.to_display_point(&snapshot).row()
188 ..end.to_display_point(&snapshot).row() + 1;
189 active_editor.set_highlighted_rows(Some(display_rows));
190 active_editor.request_autoscroll(Autoscroll::Center, cx);
191 });
192 }
193 cx.notify();
194 }
195
196 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
197 self.prev_scroll_position.take();
198 self.active_editor.update(cx, |active_editor, cx| {
199 if let Some(rows) = active_editor.highlighted_rows() {
200 let snapshot = active_editor.snapshot(cx).display_snapshot;
201 let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
202 active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
203 }
204 });
205 cx.emit(Event::Dismissed);
206 }
207
208 fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
209 self.active_editor.update(cx, |editor, cx| {
210 editor.set_highlighted_rows(None);
211 if let Some(scroll_position) = self.prev_scroll_position {
212 editor.set_scroll_position(scroll_position, cx);
213 }
214 })
215 }
216
217 fn on_event(
218 workspace: &mut Workspace,
219 _: ViewHandle<Self>,
220 event: &Event,
221 cx: &mut ViewContext<Workspace>,
222 ) {
223 match event {
224 Event::Dismissed => workspace.dismiss_modal(cx),
225 }
226 }
227
228 fn on_query_editor_event(
229 &mut self,
230 _: ViewHandle<Editor>,
231 event: &editor::Event,
232 cx: &mut ViewContext<Self>,
233 ) {
234 match event {
235 editor::Event::Blurred => cx.emit(Event::Dismissed),
236 editor::Event::Edited => self.update_matches(cx),
237 _ => {}
238 }
239 }
240
241 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
242 let selected_index;
243 let navigate_to_selected_index;
244 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
245 if query.is_empty() {
246 self.restore_active_editor(cx);
247 self.matches = self
248 .outline
249 .items
250 .iter()
251 .enumerate()
252 .map(|(index, _)| StringMatch {
253 candidate_id: index,
254 score: Default::default(),
255 positions: Default::default(),
256 string: Default::default(),
257 })
258 .collect();
259
260 let editor = self.active_editor.read(cx);
261 let buffer = editor.buffer().read(cx).read(cx);
262 let cursor_offset = editor
263 .newest_selection_with_snapshot::<usize>(&buffer)
264 .head();
265 selected_index = self
266 .outline
267 .items
268 .iter()
269 .enumerate()
270 .map(|(ix, item)| {
271 let range = item.range.to_offset(&buffer);
272 let distance_to_closest_endpoint = cmp::min(
273 (range.start as isize - cursor_offset as isize).abs() as usize,
274 (range.end as isize - cursor_offset as isize).abs() as usize,
275 );
276 let depth = if range.contains(&cursor_offset) {
277 Some(item.depth)
278 } else {
279 None
280 };
281 (ix, depth, distance_to_closest_endpoint)
282 })
283 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
284 .unwrap()
285 .0;
286 navigate_to_selected_index = false;
287 } else {
288 self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
289 selected_index = self
290 .matches
291 .iter()
292 .enumerate()
293 .max_by_key(|(_, m)| OrderedFloat(m.score))
294 .map(|(ix, _)| ix)
295 .unwrap_or(0);
296 navigate_to_selected_index = !self.matches.is_empty();
297 }
298 self.select(selected_index, navigate_to_selected_index, true, cx);
299 }
300
301 fn render_matches(&self) -> ElementBox {
302 if self.matches.is_empty() {
303 let settings = self.settings.borrow();
304 return Container::new(
305 Label::new(
306 "No matches".into(),
307 settings.theme.selector.empty.label.clone(),
308 )
309 .boxed(),
310 )
311 .with_style(settings.theme.selector.empty.container)
312 .named("empty matches");
313 }
314
315 let handle = self.handle.clone();
316 let list = UniformList::new(
317 self.list_state.clone(),
318 self.matches.len(),
319 move |mut range, items, cx| {
320 let cx = cx.as_ref();
321 let view = handle.upgrade(cx).unwrap();
322 let view = view.read(cx);
323 let start = range.start;
324 range.end = cmp::min(range.end, view.matches.len());
325 items.extend(
326 view.matches[range]
327 .iter()
328 .enumerate()
329 .map(move |(ix, m)| view.render_match(m, start + ix)),
330 );
331 },
332 );
333
334 Container::new(list.boxed())
335 .with_margin_top(6.0)
336 .named("matches")
337 }
338
339 fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
340 let settings = self.settings.borrow();
341 let style = if index == self.selected_match_index {
342 &settings.theme.selector.active_item
343 } else {
344 &settings.theme.selector.item
345 };
346 let outline_item = &self.outline.items[string_match.candidate_id];
347
348 Text::new(outline_item.text.clone(), style.label.text.clone())
349 .with_soft_wrap(false)
350 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
351 &outline_item.text,
352 style.label.text.clone().into(),
353 outline_item.highlight_ranges.iter().cloned(),
354 &string_match.positions,
355 ))
356 .contained()
357 .with_padding_left(20. * outline_item.depth as f32)
358 .contained()
359 .with_style(style.container)
360 .boxed()
361 }
362}