outline.rs

  1use editor::{
  2    combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint,
  3    scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, ToPoint,
  4};
  5use fuzzy::StringMatch;
  6use gpui::{
  7    actions, elements::*, geometry::vector::Vector2F, AppContext, MouseState, Task, ViewContext,
  8    ViewHandle, WindowContext,
  9};
 10use language::Outline;
 11use ordered_float::OrderedFloat;
 12use picker::{Picker, PickerDelegate, PickerEvent};
 13use settings::Settings;
 14use std::{
 15    cmp::{self, Reverse},
 16    sync::Arc,
 17};
 18use workspace::Workspace;
 19
 20actions!(outline, [Toggle]);
 21
 22pub fn init(cx: &mut AppContext) {
 23    cx.add_action(toggle);
 24    OutlineView::init(cx);
 25}
 26
 27pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 28    if let Some(editor) = workspace
 29        .active_item(cx)
 30        .and_then(|item| item.downcast::<Editor>())
 31    {
 32        let outline = editor
 33            .read(cx)
 34            .buffer()
 35            .read(cx)
 36            .snapshot(cx)
 37            .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
 38        if let Some(outline) = outline {
 39            workspace.toggle_modal(cx, |_, cx| {
 40                cx.add_view(|cx| {
 41                    OutlineView::new(OutlineViewDelegate::new(outline, editor, cx), cx)
 42                        .with_max_size(800., 1200.)
 43                })
 44            });
 45        }
 46    }
 47}
 48
 49type OutlineView = Picker<OutlineViewDelegate>;
 50
 51struct OutlineViewDelegate {
 52    active_editor: ViewHandle<Editor>,
 53    outline: Outline<Anchor>,
 54    selected_match_index: usize,
 55    prev_scroll_position: Option<Vector2F>,
 56    matches: Vec<StringMatch>,
 57    last_query: String,
 58}
 59
 60impl OutlineViewDelegate {
 61    fn new(
 62        outline: Outline<Anchor>,
 63        editor: ViewHandle<Editor>,
 64        cx: &mut ViewContext<OutlineView>,
 65    ) -> Self {
 66        Self {
 67            last_query: Default::default(),
 68            matches: Default::default(),
 69            selected_match_index: 0,
 70            prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
 71            active_editor: editor,
 72            outline,
 73        }
 74    }
 75
 76    fn restore_active_editor(&mut self, cx: &mut WindowContext) {
 77        self.active_editor.update(cx, |editor, cx| {
 78            editor.highlight_rows(None);
 79            if let Some(scroll_position) = self.prev_scroll_position {
 80                editor.set_scroll_position(scroll_position, cx);
 81            }
 82        })
 83    }
 84
 85    fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<OutlineView>) {
 86        self.selected_match_index = ix;
 87        if navigate && !self.matches.is_empty() {
 88            let selected_match = &self.matches[self.selected_match_index];
 89            let outline_item = &self.outline.items[selected_match.candidate_id];
 90            self.active_editor.update(cx, |active_editor, cx| {
 91                let snapshot = active_editor.snapshot(cx).display_snapshot;
 92                let buffer_snapshot = &snapshot.buffer_snapshot;
 93                let start = outline_item.range.start.to_point(buffer_snapshot);
 94                let end = outline_item.range.end.to_point(buffer_snapshot);
 95                let display_rows = start.to_display_point(&snapshot).row()
 96                    ..end.to_display_point(&snapshot).row() + 1;
 97                active_editor.highlight_rows(Some(display_rows));
 98                active_editor.request_autoscroll(Autoscroll::center(), cx);
 99            });
100        }
101    }
102}
103
104impl PickerDelegate for OutlineViewDelegate {
105    fn placeholder_text(&self) -> Arc<str> {
106        "Search buffer symbols...".into()
107    }
108
109    fn match_count(&self) -> usize {
110        self.matches.len()
111    }
112
113    fn selected_index(&self) -> usize {
114        self.selected_match_index
115    }
116
117    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<OutlineView>) {
118        self.set_selected_index(ix, true, cx);
119    }
120
121    fn center_selection_after_match_updates(&self) -> bool {
122        true
123    }
124
125    fn update_matches(&mut self, query: String, cx: &mut ViewContext<OutlineView>) -> Task<()> {
126        let selected_index;
127        if query.is_empty() {
128            self.restore_active_editor(cx);
129            self.matches = self
130                .outline
131                .items
132                .iter()
133                .enumerate()
134                .map(|(index, _)| StringMatch {
135                    candidate_id: index,
136                    score: Default::default(),
137                    positions: Default::default(),
138                    string: Default::default(),
139                })
140                .collect();
141
142            let editor = self.active_editor.read(cx);
143            let cursor_offset = editor.selections.newest::<usize>(cx).head();
144            let buffer = editor.buffer().read(cx).snapshot(cx);
145            selected_index = self
146                .outline
147                .items
148                .iter()
149                .enumerate()
150                .map(|(ix, item)| {
151                    let range = item.range.to_offset(&buffer);
152                    let distance_to_closest_endpoint = cmp::min(
153                        (range.start as isize - cursor_offset as isize).abs(),
154                        (range.end as isize - cursor_offset as isize).abs(),
155                    );
156                    let depth = if range.contains(&cursor_offset) {
157                        Some(item.depth)
158                    } else {
159                        None
160                    };
161                    (ix, depth, distance_to_closest_endpoint)
162                })
163                .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
164                .map(|(ix, _, _)| ix)
165                .unwrap_or(0);
166        } else {
167            self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
168            selected_index = self
169                .matches
170                .iter()
171                .enumerate()
172                .max_by_key(|(_, m)| OrderedFloat(m.score))
173                .map(|(ix, _)| ix)
174                .unwrap_or(0);
175        }
176        self.last_query = query;
177        self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
178        Task::ready(())
179    }
180
181    fn confirm(&mut self, cx: &mut ViewContext<OutlineView>) {
182        self.prev_scroll_position.take();
183        self.active_editor.update(cx, |active_editor, cx| {
184            if let Some(rows) = active_editor.highlighted_rows() {
185                let snapshot = active_editor.snapshot(cx).display_snapshot;
186                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
187                active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
188                    s.select_ranges([position..position])
189                });
190                active_editor.highlight_rows(None);
191            }
192        });
193        cx.emit(PickerEvent::Dismiss);
194    }
195
196    fn dismissed(&mut self, cx: &mut ViewContext<OutlineView>) {
197        self.restore_active_editor(cx);
198    }
199
200    fn render_match(
201        &self,
202        ix: usize,
203        mouse_state: &mut MouseState,
204        selected: bool,
205        cx: &AppContext,
206    ) -> AnyElement<Picker<Self>> {
207        let settings = cx.global::<Settings>();
208        let string_match = &self.matches[ix];
209        let style = settings.theme.picker.item.style_for(mouse_state, selected);
210        let outline_item = &self.outline.items[string_match.candidate_id];
211
212        Text::new(outline_item.text.clone(), style.label.text.clone())
213            .with_soft_wrap(false)
214            .with_highlights(combine_syntax_and_fuzzy_match_highlights(
215                &outline_item.text,
216                style.label.text.clone().into(),
217                outline_item.highlight_ranges.iter().cloned(),
218                &string_match.positions,
219            ))
220            .contained()
221            .with_padding_left(20. * outline_item.depth as f32)
222            .contained()
223            .with_style(style.container)
224            .into_any()
225    }
226}