1use editor::{
2 display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
3 DisplayPoint, Editor, ToPoint,
4};
5use fuzzy::StringMatch;
6use gpui::{
7 actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
8 FontStyle, FontWeight, HighlightStyle, ParentElement, Point, Render, Styled, StyledText, Task,
9 TextStyle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
10};
11use language::Outline;
12use ordered_float::OrderedFloat;
13use picker::{Picker, PickerDelegate};
14use settings::Settings;
15use std::{
16 cmp::{self, Reverse},
17 sync::Arc,
18};
19
20use theme::{color_alpha, ActiveTheme, ThemeSettings};
21use ui::{prelude::*, ListItem};
22use util::ResultExt;
23use workspace::Workspace;
24
25actions!(Toggle);
26
27pub fn init(cx: &mut AppContext) {
28 cx.observe_new_views(OutlineView::register).detach();
29}
30
31pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
32 if let Some(editor) = workspace
33 .active_item(cx)
34 .and_then(|item| item.downcast::<Editor>())
35 {
36 let outline = editor
37 .read(cx)
38 .buffer()
39 .read(cx)
40 .snapshot(cx)
41 .outline(Some(&cx.theme().syntax()));
42
43 if let Some(outline) = outline {
44 workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
45 }
46 }
47}
48
49pub struct OutlineView {
50 picker: View<Picker<OutlineViewDelegate>>,
51}
52
53impl FocusableView for OutlineView {
54 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
55 self.picker.focus_handle(cx)
56 }
57}
58
59impl EventEmitter<DismissEvent> for OutlineView {}
60
61impl Render for OutlineView {
62 type Element = Div;
63
64 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
65 v_stack().w(rems(34.)).child(self.picker.clone())
66 }
67}
68
69impl OutlineView {
70 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
71 workspace.register_action(toggle);
72 }
73
74 fn new(
75 outline: Outline<Anchor>,
76 editor: View<Editor>,
77 cx: &mut ViewContext<Self>,
78 ) -> OutlineView {
79 let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx);
80 let picker = cx.build_view(|cx| Picker::new(delegate, cx));
81 OutlineView { picker }
82 }
83}
84
85struct OutlineViewDelegate {
86 outline_view: WeakView<OutlineView>,
87 active_editor: View<Editor>,
88 outline: Outline<Anchor>,
89 selected_match_index: usize,
90 prev_scroll_position: Option<Point<f32>>,
91 matches: Vec<StringMatch>,
92 last_query: String,
93}
94
95impl OutlineViewDelegate {
96 fn new(
97 outline_view: WeakView<OutlineView>,
98 outline: Outline<Anchor>,
99 editor: View<Editor>,
100 cx: &mut ViewContext<OutlineView>,
101 ) -> Self {
102 Self {
103 outline_view,
104 last_query: Default::default(),
105 matches: Default::default(),
106 selected_match_index: 0,
107 prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
108 active_editor: editor,
109 outline,
110 }
111 }
112
113 fn restore_active_editor(&mut self, cx: &mut WindowContext) {
114 self.active_editor.update(cx, |editor, cx| {
115 editor.highlight_rows(None);
116 if let Some(scroll_position) = self.prev_scroll_position {
117 editor.set_scroll_position(scroll_position, cx);
118 }
119 })
120 }
121
122 fn set_selected_index(
123 &mut self,
124 ix: usize,
125 navigate: bool,
126 cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
127 ) {
128 self.selected_match_index = ix;
129
130 if navigate && !self.matches.is_empty() {
131 let selected_match = &self.matches[self.selected_match_index];
132 let outline_item = &self.outline.items[selected_match.candidate_id];
133
134 self.active_editor.update(cx, |active_editor, cx| {
135 let snapshot = active_editor.snapshot(cx).display_snapshot;
136 let buffer_snapshot = &snapshot.buffer_snapshot;
137 let start = outline_item.range.start.to_point(buffer_snapshot);
138 let end = outline_item.range.end.to_point(buffer_snapshot);
139 let display_rows = start.to_display_point(&snapshot).row()
140 ..end.to_display_point(&snapshot).row() + 1;
141 active_editor.highlight_rows(Some(display_rows));
142 active_editor.request_autoscroll(Autoscroll::center(), cx);
143 });
144 }
145 }
146}
147
148impl PickerDelegate for OutlineViewDelegate {
149 type ListItem = ListItem;
150
151 fn placeholder_text(&self) -> Arc<str> {
152 "Search buffer symbols...".into()
153 }
154
155 fn match_count(&self) -> usize {
156 self.matches.len()
157 }
158
159 fn selected_index(&self) -> usize {
160 self.selected_match_index
161 }
162
163 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
164 self.set_selected_index(ix, true, cx);
165 }
166
167 fn update_matches(
168 &mut self,
169 query: String,
170 cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
171 ) -> Task<()> {
172 let selected_index;
173 if query.is_empty() {
174 self.restore_active_editor(cx);
175 self.matches = self
176 .outline
177 .items
178 .iter()
179 .enumerate()
180 .map(|(index, _)| StringMatch {
181 candidate_id: index,
182 score: Default::default(),
183 positions: Default::default(),
184 string: Default::default(),
185 })
186 .collect();
187
188 let editor = self.active_editor.read(cx);
189 let cursor_offset = editor.selections.newest::<usize>(cx).head();
190 let buffer = editor.buffer().read(cx).snapshot(cx);
191 selected_index = self
192 .outline
193 .items
194 .iter()
195 .enumerate()
196 .map(|(ix, item)| {
197 let range = item.range.to_offset(&buffer);
198 let distance_to_closest_endpoint = cmp::min(
199 (range.start as isize - cursor_offset as isize).abs(),
200 (range.end as isize - cursor_offset as isize).abs(),
201 );
202 let depth = if range.contains(&cursor_offset) {
203 Some(item.depth)
204 } else {
205 None
206 };
207 (ix, depth, distance_to_closest_endpoint)
208 })
209 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
210 .map(|(ix, _, _)| ix)
211 .unwrap_or(0);
212 } else {
213 self.matches = smol::block_on(
214 self.outline
215 .search(&query, cx.background_executor().clone()),
216 );
217 selected_index = self
218 .matches
219 .iter()
220 .enumerate()
221 .max_by_key(|(_, m)| OrderedFloat(m.score))
222 .map(|(ix, _)| ix)
223 .unwrap_or(0);
224 }
225 self.last_query = query;
226 self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
227 Task::ready(())
228 }
229
230 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
231 self.prev_scroll_position.take();
232
233 self.active_editor.update(cx, |active_editor, cx| {
234 if let Some(rows) = active_editor.highlighted_rows() {
235 let snapshot = active_editor.snapshot(cx).display_snapshot;
236 let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
237 active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
238 s.select_ranges([position..position])
239 });
240 active_editor.highlight_rows(None);
241 }
242 });
243
244 self.dismissed(cx);
245 }
246
247 fn dismissed(&mut self, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
248 self.outline_view
249 .update(cx, |_, cx| cx.emit(DismissEvent))
250 .log_err();
251 self.restore_active_editor(cx);
252 }
253
254 fn render_match(
255 &self,
256 ix: usize,
257 selected: bool,
258 cx: &mut ViewContext<Picker<Self>>,
259 ) -> Option<Self::ListItem> {
260 let settings = ThemeSettings::get_global(cx);
261
262 // TODO: We probably shouldn't need to build a whole new text style here
263 // but I'm not sure how to get the current one and modify it.
264 // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
265 let text_style = TextStyle {
266 color: cx.theme().colors().text,
267 font_family: settings.buffer_font.family.clone(),
268 font_features: settings.buffer_font.features,
269 font_size: settings.buffer_font_size(cx).into(),
270 font_weight: FontWeight::NORMAL,
271 font_style: FontStyle::Normal,
272 line_height: relative(1.).into(),
273 background_color: None,
274 underline: None,
275 white_space: WhiteSpace::Normal,
276 };
277
278 let mut highlight_style = HighlightStyle::default();
279 highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
280
281 let mat = &self.matches[ix];
282 let outline_item = &self.outline.items[mat.candidate_id];
283
284 let highlights = gpui::combine_highlights(
285 mat.ranges().map(|range| (range, highlight_style)),
286 outline_item.highlight_ranges.iter().cloned(),
287 );
288
289 let styled_text =
290 StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights);
291
292 Some(
293 ListItem::new(ix).inset(true).selected(selected).child(
294 div()
295 .text_ui()
296 .pl(rems(outline_item.depth as f32))
297 .child(styled_text),
298 ),
299 )
300 }
301}