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