1use editor::{
2 combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint,
3 scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, ToPoint,
4};
5use fuzzy::StringMatch;
6use gpui::{
7 actions, elements::*, geometry::vector::Vector2F, AppContext, MouseState, Task, ViewContext,
8 ViewHandle, WindowContext,
9};
10use language::Outline;
11use ordered_float::OrderedFloat;
12use picker::{Picker, PickerDelegate, PickerEvent};
13use settings::Settings;
14use std::{
15 cmp::{self, Reverse},
16 sync::Arc,
17};
18use workspace::Workspace;
19
20actions!(outline, [Toggle]);
21
22pub fn init(cx: &mut AppContext) {
23 cx.add_action(toggle);
24 OutlineView::init(cx);
25}
26
27pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
28 if let Some(editor) = workspace
29 .active_item(cx)
30 .and_then(|item| item.downcast::<Editor>())
31 {
32 let outline = editor
33 .read(cx)
34 .buffer()
35 .read(cx)
36 .snapshot(cx)
37 .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
38 if let Some(outline) = outline {
39 workspace.toggle_modal(cx, |_, cx| {
40 cx.add_view(|cx| {
41 OutlineView::new(OutlineViewDelegate::new(outline, editor, cx), cx)
42 .with_max_size(800., 1200.)
43 })
44 });
45 }
46 }
47}
48
49type OutlineView = Picker<OutlineViewDelegate>;
50
51struct OutlineViewDelegate {
52 active_editor: ViewHandle<Editor>,
53 outline: Outline<Anchor>,
54 selected_match_index: usize,
55 prev_scroll_position: Option<Vector2F>,
56 matches: Vec<StringMatch>,
57 last_query: String,
58}
59
60impl OutlineViewDelegate {
61 fn new(
62 outline: Outline<Anchor>,
63 editor: ViewHandle<Editor>,
64 cx: &mut ViewContext<OutlineView>,
65 ) -> Self {
66 Self {
67 last_query: Default::default(),
68 matches: Default::default(),
69 selected_match_index: 0,
70 prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
71 active_editor: editor,
72 outline,
73 }
74 }
75
76 fn restore_active_editor(&mut self, cx: &mut WindowContext) {
77 self.active_editor.update(cx, |editor, cx| {
78 editor.highlight_rows(None);
79 if let Some(scroll_position) = self.prev_scroll_position {
80 editor.set_scroll_position(scroll_position, cx);
81 }
82 })
83 }
84
85 fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<OutlineView>) {
86 self.selected_match_index = ix;
87 if navigate && !self.matches.is_empty() {
88 let selected_match = &self.matches[self.selected_match_index];
89 let outline_item = &self.outline.items[selected_match.candidate_id];
90 self.active_editor.update(cx, |active_editor, cx| {
91 let snapshot = active_editor.snapshot(cx).display_snapshot;
92 let buffer_snapshot = &snapshot.buffer_snapshot;
93 let start = outline_item.range.start.to_point(buffer_snapshot);
94 let end = outline_item.range.end.to_point(buffer_snapshot);
95 let display_rows = start.to_display_point(&snapshot).row()
96 ..end.to_display_point(&snapshot).row() + 1;
97 active_editor.highlight_rows(Some(display_rows));
98 active_editor.request_autoscroll(Autoscroll::center(), cx);
99 });
100 }
101 }
102}
103
104impl PickerDelegate for OutlineViewDelegate {
105 fn placeholder_text(&self) -> Arc<str> {
106 "Search buffer symbols...".into()
107 }
108
109 fn match_count(&self) -> usize {
110 self.matches.len()
111 }
112
113 fn selected_index(&self) -> usize {
114 self.selected_match_index
115 }
116
117 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<OutlineView>) {
118 self.set_selected_index(ix, true, cx);
119 }
120
121 fn center_selection_after_match_updates(&self) -> bool {
122 true
123 }
124
125 fn update_matches(&mut self, query: String, cx: &mut ViewContext<OutlineView>) -> Task<()> {
126 let selected_index;
127 if query.is_empty() {
128 self.restore_active_editor(cx);
129 self.matches = self
130 .outline
131 .items
132 .iter()
133 .enumerate()
134 .map(|(index, _)| StringMatch {
135 candidate_id: index,
136 score: Default::default(),
137 positions: Default::default(),
138 string: Default::default(),
139 })
140 .collect();
141
142 let editor = self.active_editor.read(cx);
143 let cursor_offset = editor.selections.newest::<usize>(cx).head();
144 let buffer = editor.buffer().read(cx).snapshot(cx);
145 selected_index = self
146 .outline
147 .items
148 .iter()
149 .enumerate()
150 .map(|(ix, item)| {
151 let range = item.range.to_offset(&buffer);
152 let distance_to_closest_endpoint = cmp::min(
153 (range.start as isize - cursor_offset as isize).abs(),
154 (range.end as isize - cursor_offset as isize).abs(),
155 );
156 let depth = if range.contains(&cursor_offset) {
157 Some(item.depth)
158 } else {
159 None
160 };
161 (ix, depth, distance_to_closest_endpoint)
162 })
163 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
164 .map(|(ix, _, _)| ix)
165 .unwrap_or(0);
166 } else {
167 self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
168 selected_index = self
169 .matches
170 .iter()
171 .enumerate()
172 .max_by_key(|(_, m)| OrderedFloat(m.score))
173 .map(|(ix, _)| ix)
174 .unwrap_or(0);
175 }
176 self.last_query = query;
177 self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
178 Task::ready(())
179 }
180
181 fn confirm(&mut self, cx: &mut ViewContext<OutlineView>) {
182 self.prev_scroll_position.take();
183 self.active_editor.update(cx, |active_editor, cx| {
184 if let Some(rows) = active_editor.highlighted_rows() {
185 let snapshot = active_editor.snapshot(cx).display_snapshot;
186 let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
187 active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
188 s.select_ranges([position..position])
189 });
190 active_editor.highlight_rows(None);
191 }
192 });
193 cx.emit(PickerEvent::Dismiss);
194 }
195
196 fn dismissed(&mut self, cx: &mut ViewContext<OutlineView>) {
197 self.restore_active_editor(cx);
198 }
199
200 fn render_match(
201 &self,
202 ix: usize,
203 mouse_state: &mut MouseState,
204 selected: bool,
205 cx: &AppContext,
206 ) -> AnyElement<Picker<Self>> {
207 let settings = cx.global::<Settings>();
208 let string_match = &self.matches[ix];
209 let style = settings.theme.picker.item.style_for(mouse_state, selected);
210 let outline_item = &self.outline.items[string_match.candidate_id];
211
212 Text::new(outline_item.text.clone(), style.label.text.clone())
213 .with_soft_wrap(false)
214 .with_highlights(combine_syntax_and_fuzzy_match_highlights(
215 &outline_item.text,
216 style.label.text.clone().into(),
217 outline_item.highlight_ranges.iter().cloned(),
218 &string_match.positions,
219 ))
220 .contained()
221 .with_padding_left(20. * outline_item.depth as f32)
222 .contained()
223 .with_style(style.container)
224 .into_any()
225 }
226}