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 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}