outline.rs

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