project_symbols.rs

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