outline.rs

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