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