1use editor::{
2 display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings,
3 ToPoint,
4};
5use fuzzy::StringMatch;
6use gpui::{
7 action,
8 elements::*,
9 fonts::{self, HighlightStyle},
10 geometry::vector::Vector2F,
11 keymap::{self, Binding},
12 AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
13 WeakViewHandle,
14};
15use language::{Outline, Selection};
16use ordered_float::OrderedFloat;
17use postage::watch;
18use std::{
19 cmp::{self, Reverse},
20 ops::Range,
21 sync::Arc,
22};
23use workspace::{
24 menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
25 Settings, Workspace,
26};
27
28action!(Toggle);
29
30pub fn init(cx: &mut MutableAppContext) {
31 cx.add_bindings([
32 Binding::new("cmd-shift-O", Toggle, Some("Editor")),
33 Binding::new("escape", Toggle, Some("OutlineView")),
34 ]);
35 cx.add_action(OutlineView::toggle);
36 cx.add_action(OutlineView::confirm);
37 cx.add_action(OutlineView::select_prev);
38 cx.add_action(OutlineView::select_next);
39 cx.add_action(OutlineView::select_first);
40 cx.add_action(OutlineView::select_last);
41}
42
43struct OutlineView {
44 handle: WeakViewHandle<Self>,
45 active_editor: ViewHandle<Editor>,
46 outline: Outline<Anchor>,
47 selected_match_index: usize,
48 restore_state: Option<RestoreState>,
49 symbol_selection_id: Option<usize>,
50 matches: Vec<StringMatch>,
51 query_editor: ViewHandle<Editor>,
52 list_state: UniformListState,
53 settings: watch::Receiver<Settings>,
54}
55
56struct RestoreState {
57 scroll_position: Vector2F,
58 selections: Vec<Selection<usize>>,
59}
60
61pub enum Event {
62 Dismissed,
63}
64
65impl Entity for OutlineView {
66 type Event = Event;
67
68 fn release(&mut self, cx: &mut MutableAppContext) {
69 self.restore_active_editor(cx);
70 }
71}
72
73impl View for OutlineView {
74 fn ui_name() -> &'static str {
75 "OutlineView"
76 }
77
78 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
79 let mut cx = Self::default_keymap_context();
80 cx.set.insert("menu".into());
81 cx
82 }
83
84 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
85 let settings = self.settings.borrow();
86
87 Flex::new(Axis::Vertical)
88 .with_child(
89 Container::new(ChildView::new(self.query_editor.id()).boxed())
90 .with_style(settings.theme.selector.input_editor.container)
91 .boxed(),
92 )
93 .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
94 .contained()
95 .with_style(settings.theme.selector.container)
96 .constrained()
97 .with_max_width(800.0)
98 .with_max_height(1200.0)
99 .aligned()
100 .top()
101 .named("outline view")
102 }
103
104 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
105 cx.focus(&self.query_editor);
106 }
107}
108
109impl OutlineView {
110 fn new(
111 outline: Outline<Anchor>,
112 editor: ViewHandle<Editor>,
113 settings: watch::Receiver<Settings>,
114 cx: &mut ViewContext<Self>,
115 ) -> Self {
116 let query_editor = cx.add_view(|cx| {
117 Editor::single_line(
118 {
119 let settings = settings.clone();
120 Arc::new(move |_| {
121 let settings = settings.borrow();
122 EditorSettings {
123 style: settings.theme.selector.input_editor.as_editor(),
124 tab_size: settings.tab_size,
125 soft_wrap: editor::SoftWrap::None,
126 }
127 })
128 },
129 cx,
130 )
131 });
132 cx.subscribe(&query_editor, Self::on_query_editor_event)
133 .detach();
134
135 let restore_state = editor.update(cx, |editor, cx| {
136 Some(RestoreState {
137 scroll_position: editor.scroll_position(cx),
138 selections: editor.local_selections::<usize>(cx),
139 })
140 });
141
142 let mut this = Self {
143 handle: cx.weak_handle(),
144 active_editor: editor,
145 matches: Default::default(),
146 selected_match_index: 0,
147 restore_state,
148 symbol_selection_id: None,
149 outline,
150 query_editor,
151 list_state: Default::default(),
152 settings,
153 };
154 this.update_matches(cx);
155 this
156 }
157
158 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
159 if let Some(editor) = workspace
160 .active_item(cx)
161 .and_then(|item| item.to_any().downcast::<Editor>())
162 {
163 let settings = workspace.settings();
164 let buffer = editor
165 .read(cx)
166 .buffer()
167 .read(cx)
168 .read(cx)
169 .outline(Some(settings.borrow().theme.editor.syntax.as_ref()));
170 if let Some(outline) = buffer {
171 workspace.toggle_modal(cx, |cx, _| {
172 let view = cx.add_view(|cx| OutlineView::new(outline, editor, settings, cx));
173 cx.subscribe(&view, Self::on_event).detach();
174 view
175 })
176 }
177 }
178 }
179
180 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
181 if self.selected_match_index > 0 {
182 self.select(self.selected_match_index - 1, true, false, cx);
183 }
184 }
185
186 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
187 if self.selected_match_index + 1 < self.matches.len() {
188 self.select(self.selected_match_index + 1, true, false, cx);
189 }
190 }
191
192 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
193 self.select(0, true, false, cx);
194 }
195
196 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
197 self.select(self.matches.len().saturating_sub(1), true, false, cx);
198 }
199
200 fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
201 self.selected_match_index = index;
202 self.list_state.scroll_to(if center {
203 ScrollTarget::Center(index)
204 } else {
205 ScrollTarget::Show(index)
206 });
207 if navigate {
208 let selected_match = &self.matches[self.selected_match_index];
209 let outline_item = &self.outline.items[selected_match.candidate_id];
210 self.symbol_selection_id = self.active_editor.update(cx, |active_editor, cx| {
211 let snapshot = active_editor.snapshot(cx).display_snapshot;
212 let buffer_snapshot = &snapshot.buffer_snapshot;
213 let start = outline_item.range.start.to_point(&buffer_snapshot);
214 let end = outline_item.range.end.to_point(&buffer_snapshot);
215 let display_rows = start.to_display_point(&snapshot).row()
216 ..end.to_display_point(&snapshot).row() + 1;
217 active_editor.select_ranges([start..start], Some(Autoscroll::Center), cx);
218 active_editor.set_highlighted_rows(Some(display_rows));
219 Some(active_editor.newest_selection::<usize>(&buffer_snapshot).id)
220 });
221 cx.notify();
222 }
223 }
224
225 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
226 self.restore_state.take();
227 cx.emit(Event::Dismissed);
228 }
229
230 fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
231 let symbol_selection_id = self.symbol_selection_id.take();
232 self.active_editor.update(cx, |editor, cx| {
233 editor.set_highlighted_rows(None);
234 if let Some((symbol_selection_id, restore_state)) =
235 symbol_selection_id.zip(self.restore_state.as_ref())
236 {
237 let newest_selection =
238 editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
239 if symbol_selection_id == newest_selection.id {
240 editor.set_scroll_position(restore_state.scroll_position, cx);
241 editor.update_selections(restore_state.selections.clone(), None, cx);
242 }
243 }
244 })
245 }
246
247 fn on_event(
248 workspace: &mut Workspace,
249 _: ViewHandle<Self>,
250 event: &Event,
251 cx: &mut ViewContext<Workspace>,
252 ) {
253 match event {
254 Event::Dismissed => workspace.dismiss_modal(cx),
255 }
256 }
257
258 fn on_query_editor_event(
259 &mut self,
260 _: ViewHandle<Editor>,
261 event: &editor::Event,
262 cx: &mut ViewContext<Self>,
263 ) {
264 match event {
265 editor::Event::Blurred => cx.emit(Event::Dismissed),
266 editor::Event::Edited => self.update_matches(cx),
267 _ => {}
268 }
269 }
270
271 fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
272 let selected_index;
273 let navigate_to_selected_index;
274 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
275 if query.is_empty() {
276 self.restore_active_editor(cx);
277 self.matches = self
278 .outline
279 .items
280 .iter()
281 .enumerate()
282 .map(|(index, _)| StringMatch {
283 candidate_id: index,
284 score: Default::default(),
285 positions: Default::default(),
286 string: Default::default(),
287 })
288 .collect();
289
290 let editor = self.active_editor.read(cx);
291 let buffer = editor.buffer().read(cx).read(cx);
292 let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
293 selected_index = self
294 .outline
295 .items
296 .iter()
297 .enumerate()
298 .map(|(ix, item)| {
299 let range = item.range.to_offset(&buffer);
300 let distance_to_closest_endpoint = cmp::min(
301 (range.start as isize - cursor_offset as isize).abs() as usize,
302 (range.end as isize - cursor_offset as isize).abs() as usize,
303 );
304 let depth = if range.contains(&cursor_offset) {
305 Some(item.depth)
306 } else {
307 None
308 };
309 (ix, depth, distance_to_closest_endpoint)
310 })
311 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
312 .unwrap()
313 .0;
314 navigate_to_selected_index = false;
315 } else {
316 self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
317 selected_index = self
318 .matches
319 .iter()
320 .enumerate()
321 .max_by_key(|(_, m)| OrderedFloat(m.score))
322 .map(|(ix, _)| ix)
323 .unwrap_or(0);
324 navigate_to_selected_index = !self.matches.is_empty();
325 }
326 self.select(selected_index, navigate_to_selected_index, true, cx);
327 }
328
329 fn render_matches(&self) -> ElementBox {
330 if self.matches.is_empty() {
331 let settings = self.settings.borrow();
332 return Container::new(
333 Label::new(
334 "No matches".into(),
335 settings.theme.selector.empty.label.clone(),
336 )
337 .boxed(),
338 )
339 .with_style(settings.theme.selector.empty.container)
340 .named("empty matches");
341 }
342
343 let handle = self.handle.clone();
344 let list = UniformList::new(
345 self.list_state.clone(),
346 self.matches.len(),
347 move |mut range, items, cx| {
348 let cx = cx.as_ref();
349 let view = handle.upgrade(cx).unwrap();
350 let view = view.read(cx);
351 let start = range.start;
352 range.end = cmp::min(range.end, view.matches.len());
353 items.extend(
354 view.matches[range]
355 .iter()
356 .enumerate()
357 .map(move |(ix, m)| view.render_match(m, start + ix)),
358 );
359 },
360 );
361
362 Container::new(list.boxed())
363 .with_margin_top(6.0)
364 .named("matches")
365 }
366
367 fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
368 let settings = self.settings.borrow();
369 let style = if index == self.selected_match_index {
370 &settings.theme.selector.active_item
371 } else {
372 &settings.theme.selector.item
373 };
374 let outline_item = &self.outline.items[string_match.candidate_id];
375
376 Text::new(outline_item.text.clone(), style.label.text.clone())
377 .with_soft_wrap(false)
378 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
379 &outline_item.text,
380 style.label.text.clone().into(),
381 &outline_item.highlight_ranges,
382 &string_match.positions,
383 ))
384 .contained()
385 .with_padding_left(20. * outline_item.depth as f32)
386 .contained()
387 .with_style(style.container)
388 .boxed()
389 }
390}
391
392fn combine_syntax_and_fuzzy_match_highlights(
393 text: &str,
394 default_style: HighlightStyle,
395 syntax_ranges: &[(Range<usize>, HighlightStyle)],
396 match_indices: &[usize],
397) -> Vec<(Range<usize>, HighlightStyle)> {
398 let mut result = Vec::new();
399 let mut match_indices = match_indices.iter().copied().peekable();
400
401 for (range, mut syntax_highlight) in syntax_ranges
402 .iter()
403 .cloned()
404 .chain([(usize::MAX..0, Default::default())])
405 {
406 syntax_highlight.font_properties.weight(Default::default());
407
408 // Add highlights for any fuzzy match characters before the next
409 // syntax highlight range.
410 while let Some(&match_index) = match_indices.peek() {
411 if match_index >= range.start {
412 break;
413 }
414 match_indices.next();
415 let end_index = char_ix_after(match_index, text);
416 let mut match_style = default_style;
417 match_style.font_properties.weight(fonts::Weight::BOLD);
418 result.push((match_index..end_index, match_style));
419 }
420
421 if range.start == usize::MAX {
422 break;
423 }
424
425 // Add highlights for any fuzzy match characters within the
426 // syntax highlight range.
427 let mut offset = range.start;
428 while let Some(&match_index) = match_indices.peek() {
429 if match_index >= range.end {
430 break;
431 }
432
433 match_indices.next();
434 if match_index > offset {
435 result.push((offset..match_index, syntax_highlight));
436 }
437
438 let mut end_index = char_ix_after(match_index, text);
439 while let Some(&next_match_index) = match_indices.peek() {
440 if next_match_index == end_index && next_match_index < range.end {
441 end_index = char_ix_after(next_match_index, text);
442 match_indices.next();
443 } else {
444 break;
445 }
446 }
447
448 let mut match_style = syntax_highlight;
449 match_style.font_properties.weight(fonts::Weight::BOLD);
450 result.push((match_index..end_index, match_style));
451 offset = end_index;
452 }
453
454 if offset < range.end {
455 result.push((offset..range.end, syntax_highlight));
456 }
457 }
458
459 result
460}
461
462fn char_ix_after(ix: usize, text: &str) -> usize {
463 ix + text[ix..].chars().next().unwrap().len_utf8()
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use gpui::{color::Color, fonts::HighlightStyle};
470
471 #[test]
472 fn test_combine_syntax_and_fuzzy_match_highlights() {
473 let string = "abcdefghijklmnop";
474 let default = HighlightStyle::default();
475 let syntax_ranges = [
476 (
477 0..3,
478 HighlightStyle {
479 color: Color::red(),
480 ..default
481 },
482 ),
483 (
484 4..8,
485 HighlightStyle {
486 color: Color::green(),
487 ..default
488 },
489 ),
490 ];
491 let match_indices = [4, 6, 7, 8];
492 assert_eq!(
493 combine_syntax_and_fuzzy_match_highlights(
494 &string,
495 default,
496 &syntax_ranges,
497 &match_indices,
498 ),
499 &[
500 (
501 0..3,
502 HighlightStyle {
503 color: Color::red(),
504 ..default
505 },
506 ),
507 (
508 4..5,
509 HighlightStyle {
510 color: Color::green(),
511 font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
512 ..default
513 },
514 ),
515 (
516 5..6,
517 HighlightStyle {
518 color: Color::green(),
519 ..default
520 },
521 ),
522 (
523 6..8,
524 HighlightStyle {
525 color: Color::green(),
526 font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
527 ..default
528 },
529 ),
530 (
531 8..9,
532 HighlightStyle {
533 font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
534 ..default
535 },
536 ),
537 ]
538 );
539 }
540}