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