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