project_symbols.rs

  1use editor::{
  2    combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
  3    Autoscroll, Bias, Editor, EditorSettings,
  4};
  5use fuzzy::{StringMatch, StringMatchCandidate};
  6use gpui::{
  7    action,
  8    elements::*,
  9    keymap::{self, Binding},
 10    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
 11    ViewContext, ViewHandle, WeakViewHandle,
 12};
 13use ordered_float::OrderedFloat;
 14use postage::watch;
 15use project::{Project, Symbol};
 16use std::{
 17    borrow::Cow,
 18    cmp::{self, Reverse},
 19    sync::Arc,
 20};
 21use util::ResultExt;
 22use workspace::{
 23    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
 24    Settings, Workspace,
 25};
 26
 27action!(Toggle);
 28
 29pub fn init(cx: &mut MutableAppContext) {
 30    cx.add_bindings([
 31        Binding::new("cmd-t", Toggle, None),
 32        Binding::new("escape", Toggle, Some("ProjectSymbolsView")),
 33    ]);
 34    cx.add_action(ProjectSymbolsView::toggle);
 35    cx.add_action(ProjectSymbolsView::confirm);
 36    cx.add_action(ProjectSymbolsView::select_prev);
 37    cx.add_action(ProjectSymbolsView::select_next);
 38    cx.add_action(ProjectSymbolsView::select_first);
 39    cx.add_action(ProjectSymbolsView::select_last);
 40}
 41
 42pub struct ProjectSymbolsView {
 43    handle: WeakViewHandle<Self>,
 44    project: ModelHandle<Project>,
 45    settings: watch::Receiver<Settings>,
 46    selected_match_index: usize,
 47    list_state: UniformListState,
 48    symbols: Vec<Symbol>,
 49    match_candidates: Vec<StringMatchCandidate>,
 50    matches: Vec<StringMatch>,
 51    pending_symbols_task: Task<Option<()>>,
 52    query_editor: ViewHandle<Editor>,
 53}
 54
 55pub enum Event {
 56    Dismissed,
 57    Selected(Symbol),
 58}
 59
 60impl Entity for ProjectSymbolsView {
 61    type Event = Event;
 62}
 63
 64impl View for ProjectSymbolsView {
 65    fn ui_name() -> &'static str {
 66        "ProjectSymbolsView"
 67    }
 68
 69    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
 70        let mut cx = Self::default_keymap_context();
 71        cx.set.insert("menu".into());
 72        cx
 73    }
 74
 75    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 76        let settings = self.settings.borrow();
 77
 78        Flex::new(Axis::Vertical)
 79            .with_child(
 80                Container::new(ChildView::new(&self.query_editor).boxed())
 81                    .with_style(settings.theme.selector.input_editor.container)
 82                    .boxed(),
 83            )
 84            .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
 85            .contained()
 86            .with_style(settings.theme.selector.container)
 87            .constrained()
 88            .with_max_width(500.0)
 89            .with_max_height(420.0)
 90            .aligned()
 91            .top()
 92            .named("project symbols view")
 93    }
 94
 95    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 96        cx.focus(&self.query_editor);
 97    }
 98}
 99
100impl ProjectSymbolsView {
101    fn new(
102        project: ModelHandle<Project>,
103        settings: watch::Receiver<Settings>,
104        cx: &mut ViewContext<Self>,
105    ) -> Self {
106        let query_editor = cx.add_view(|cx| {
107            Editor::single_line(
108                {
109                    let settings = settings.clone();
110                    Arc::new(move |_| {
111                        let settings = settings.borrow();
112                        EditorSettings {
113                            style: settings.theme.selector.input_editor.as_editor(),
114                            tab_size: settings.tab_size,
115                            soft_wrap: editor::SoftWrap::None,
116                        }
117                    })
118                },
119                cx,
120            )
121        });
122        cx.subscribe(&query_editor, Self::on_query_editor_event)
123            .detach();
124        let mut this = Self {
125            handle: cx.weak_handle(),
126            project,
127            settings,
128            selected_match_index: 0,
129            list_state: Default::default(),
130            symbols: Default::default(),
131            match_candidates: Default::default(),
132            matches: Default::default(),
133            pending_symbols_task: Task::ready(None),
134            query_editor,
135        };
136        this.update_matches(cx);
137        this
138    }
139
140    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
141        workspace.toggle_modal(cx, |cx, workspace| {
142            let project = workspace.project().clone();
143            let symbols = cx.add_view(|cx| Self::new(project, workspace.settings.clone(), cx));
144            cx.subscribe(&symbols, Self::on_event).detach();
145            symbols
146        });
147    }
148
149    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
150        if self.selected_match_index > 0 {
151            self.select(self.selected_match_index - 1, cx);
152        }
153    }
154
155    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
156        if self.selected_match_index + 1 < self.matches.len() {
157            self.select(self.selected_match_index + 1, cx);
158        }
159    }
160
161    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
162        self.select(0, cx);
163    }
164
165    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
166        self.select(self.matches.len().saturating_sub(1), cx);
167    }
168
169    fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
170        self.selected_match_index = index;
171        self.list_state.scroll_to(ScrollTarget::Show(index));
172        cx.notify();
173    }
174
175    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
176        if let Some(symbol) = self
177            .matches
178            .get(self.selected_match_index)
179            .map(|mat| self.symbols[mat.candidate_id].clone())
180        {
181            cx.emit(Event::Selected(symbol));
182        }
183    }
184
185    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
186        self.filter(cx);
187        let query = self.query_editor.read(cx).text(cx);
188        let symbols = self
189            .project
190            .update(cx, |project, cx| project.symbols(&query, cx));
191        self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
192            let symbols = symbols.await.log_err()?;
193            if let Some(this) = this.upgrade(&cx) {
194                this.update(&mut cx, |this, cx| {
195                    this.match_candidates = symbols
196                        .iter()
197                        .enumerate()
198                        .map(|(id, symbol)| {
199                            StringMatchCandidate::new(
200                                id,
201                                symbol.label.text[symbol.label.filter_range.clone()].to_string(),
202                            )
203                        })
204                        .collect();
205                    this.symbols = symbols;
206                    this.filter(cx);
207                });
208            }
209            None
210        });
211    }
212
213    fn filter(&mut self, cx: &mut ViewContext<Self>) {
214        let query = self.query_editor.read(cx).text(cx);
215        let mut matches = if query.is_empty() {
216            self.match_candidates
217                .iter()
218                .enumerate()
219                .map(|(candidate_id, candidate)| StringMatch {
220                    candidate_id,
221                    score: Default::default(),
222                    positions: Default::default(),
223                    string: candidate.string.clone(),
224                })
225                .collect()
226        } else {
227            smol::block_on(fuzzy::match_strings(
228                &self.match_candidates,
229                &query,
230                false,
231                100,
232                &Default::default(),
233                cx.background().clone(),
234            ))
235        };
236
237        matches.sort_unstable_by_key(|mat| {
238            let label = &self.symbols[mat.candidate_id].label;
239            (
240                Reverse(OrderedFloat(mat.score)),
241                &label.text[label.filter_range.clone()],
242            )
243        });
244
245        for mat in &mut matches {
246            let filter_start = self.symbols[mat.candidate_id].label.filter_range.start;
247            for position in &mut mat.positions {
248                *position += filter_start;
249            }
250        }
251
252        self.matches = matches;
253        self.select_first(&SelectFirst, cx);
254        cx.notify();
255    }
256
257    fn render_matches(&self) -> ElementBox {
258        if self.matches.is_empty() {
259            let settings = self.settings.borrow();
260            return Container::new(
261                Label::new(
262                    "No matches".into(),
263                    settings.theme.selector.empty.label.clone(),
264                )
265                .boxed(),
266            )
267            .with_style(settings.theme.selector.empty.container)
268            .named("empty matches");
269        }
270
271        let handle = self.handle.clone();
272        let list = UniformList::new(
273            self.list_state.clone(),
274            self.matches.len(),
275            move |mut range, items, cx| {
276                let cx = cx.as_ref();
277                let view = handle.upgrade(cx).unwrap();
278                let view = view.read(cx);
279                let start = range.start;
280                range.end = cmp::min(range.end, view.matches.len());
281
282                let show_worktree_root_name =
283                    view.project.read(cx).strong_worktrees(cx).count() > 1;
284                items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
285                    view.render_match(m, start + ix, show_worktree_root_name, cx)
286                }));
287            },
288        );
289
290        Container::new(list.boxed())
291            .with_margin_top(6.0)
292            .named("matches")
293    }
294
295    fn render_match(
296        &self,
297        string_match: &StringMatch,
298        index: usize,
299        show_worktree_root_name: bool,
300        cx: &AppContext,
301    ) -> ElementBox {
302        let settings = self.settings.borrow();
303        let style = if index == self.selected_match_index {
304            &settings.theme.selector.active_item
305        } else {
306            &settings.theme.selector.item
307        };
308        let symbol = &self.symbols[string_match.candidate_id];
309        let syntax_runs = styled_runs_for_code_label(
310            &symbol.label,
311            style.label.text.color,
312            &settings.theme.editor.syntax,
313        );
314
315        let mut path = symbol.path.to_string_lossy();
316        if show_worktree_root_name {
317            let project = self.project.read(cx);
318            if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
319                path = Cow::Owned(format!(
320                    "{}{}{}",
321                    worktree.read(cx).root_name(),
322                    std::path::MAIN_SEPARATOR,
323                    path.as_ref()
324                ));
325            }
326        }
327
328        Flex::column()
329            .with_child(
330                Text::new(symbol.label.text.clone(), style.label.text.clone())
331                    .with_soft_wrap(false)
332                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
333                        &symbol.label.text,
334                        style.label.text.clone().into(),
335                        syntax_runs,
336                        &string_match.positions,
337                    ))
338                    .boxed(),
339            )
340            .with_child(
341                // Avoid styling the path differently when it is selected, since
342                // the symbol's syntax highlighting doesn't change when selected.
343                Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(),
344            )
345            .contained()
346            .with_style(style.container)
347            .boxed()
348    }
349
350    fn on_query_editor_event(
351        &mut self,
352        _: ViewHandle<Editor>,
353        event: &editor::Event,
354        cx: &mut ViewContext<Self>,
355    ) {
356        match event {
357            editor::Event::Blurred => cx.emit(Event::Dismissed),
358            editor::Event::Edited => self.update_matches(cx),
359            _ => {}
360        }
361    }
362
363    fn on_event(
364        workspace: &mut Workspace,
365        _: ViewHandle<Self>,
366        event: &Event,
367        cx: &mut ViewContext<Workspace>,
368    ) {
369        match event {
370            Event::Dismissed => workspace.dismiss_modal(cx),
371            Event::Selected(symbol) => {
372                let buffer = workspace
373                    .project()
374                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
375                let symbol = symbol.clone();
376                cx.spawn(|workspace, mut cx| async move {
377                    let buffer = buffer.await?;
378                    workspace.update(&mut cx, |workspace, cx| {
379                        let position = buffer
380                            .read(cx)
381                            .clip_point_utf16(symbol.range.start, Bias::Left);
382                        let editor = workspace
383                            .open_item(BufferItemHandle(buffer), cx)
384                            .downcast::<Editor>()
385                            .unwrap();
386                        editor.update(cx, |editor, cx| {
387                            editor.select_ranges(
388                                [position..position],
389                                Some(Autoscroll::Center),
390                                cx,
391                            );
392                        });
393                    });
394                    Ok::<_, anyhow::Error>(())
395                })
396                .detach_and_log_err(cx);
397                workspace.dismiss_modal(cx);
398            }
399        }
400    }
401}