project_symbols.rs

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