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::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
  7    View, ViewContext, ViewHandle,
  8};
  9use ordered_float::OrderedFloat;
 10use picker::{Picker, PickerDelegate};
 11use project::{Project, Symbol};
 12use settings::Settings;
 13use std::{borrow::Cow, cmp::Reverse};
 14use util::ResultExt;
 15use workspace::Workspace;
 16
 17actions!(project_symbols, [Toggle]);
 18
 19pub fn init(cx: &mut MutableAppContext) {
 20    cx.add_action(ProjectSymbolsView::toggle);
 21    Picker::<ProjectSymbolsView>::init(cx);
 22}
 23
 24pub struct ProjectSymbolsView {
 25    picker: ViewHandle<Picker<Self>>,
 26    project: ModelHandle<Project>,
 27    selected_match_index: usize,
 28    symbols: Vec<Symbol>,
 29    match_candidates: Vec<StringMatchCandidate>,
 30    show_worktree_root_name: bool,
 31    matches: Vec<StringMatch>,
 32}
 33
 34pub enum Event {
 35    Dismissed,
 36    Selected(Symbol),
 37}
 38
 39impl Entity for ProjectSymbolsView {
 40    type Event = Event;
 41}
 42
 43impl View for ProjectSymbolsView {
 44    fn ui_name() -> &'static str {
 45        "ProjectSymbolsView"
 46    }
 47
 48    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 49        ChildView::new(self.picker.clone()).boxed()
 50    }
 51
 52    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 53        cx.focus(&self.picker);
 54    }
 55}
 56
 57impl ProjectSymbolsView {
 58    fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
 59        let handle = cx.weak_handle();
 60        Self {
 61            project,
 62            picker: cx.add_view(|cx| Picker::new(handle, cx)),
 63            selected_match_index: 0,
 64            symbols: Default::default(),
 65            match_candidates: Default::default(),
 66            matches: Default::default(),
 67            show_worktree_root_name: false,
 68        }
 69    }
 70
 71    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 72        workspace.toggle_modal(cx, |cx, workspace| {
 73            let project = workspace.project().clone();
 74            let symbols = cx.add_view(|cx| Self::new(project, cx));
 75            cx.subscribe(&symbols, Self::on_event).detach();
 76            symbols
 77        });
 78    }
 79
 80    fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
 81        let mut matches = if query.is_empty() {
 82            self.match_candidates
 83                .iter()
 84                .enumerate()
 85                .map(|(candidate_id, candidate)| StringMatch {
 86                    candidate_id,
 87                    score: Default::default(),
 88                    positions: Default::default(),
 89                    string: candidate.string.clone(),
 90                })
 91                .collect()
 92        } else {
 93            smol::block_on(fuzzy::match_strings(
 94                &self.match_candidates,
 95                query,
 96                false,
 97                100,
 98                &Default::default(),
 99                cx.background().clone(),
100            ))
101        };
102
103        matches.sort_unstable_by_key(|mat| {
104            let label = &self.symbols[mat.candidate_id].label;
105            (
106                Reverse(OrderedFloat(mat.score)),
107                &label.text[label.filter_range.clone()],
108            )
109        });
110
111        for mat in &mut matches {
112            let filter_start = self.symbols[mat.candidate_id].label.filter_range.start;
113            for position in &mut mat.positions {
114                *position += filter_start;
115            }
116        }
117
118        self.matches = matches;
119        self.set_selected_index(0, cx);
120        cx.notify();
121    }
122
123    fn on_event(
124        workspace: &mut Workspace,
125        _: ViewHandle<Self>,
126        event: &Event,
127        cx: &mut ViewContext<Workspace>,
128    ) {
129        match event {
130            Event::Dismissed => workspace.dismiss_modal(cx),
131            Event::Selected(symbol) => {
132                let buffer = workspace
133                    .project()
134                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
135
136                let symbol = symbol.clone();
137                cx.spawn(|workspace, mut cx| async move {
138                    let buffer = buffer.await?;
139                    workspace.update(&mut cx, |workspace, cx| {
140                        let position = buffer
141                            .read(cx)
142                            .clip_point_utf16(symbol.range.start, Bias::Left);
143
144                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
145                        editor.update(cx, |editor, cx| {
146                            editor.select_ranges(
147                                [position..position],
148                                Some(Autoscroll::Center),
149                                cx,
150                            );
151                        });
152                    });
153                    Ok::<_, anyhow::Error>(())
154                })
155                .detach_and_log_err(cx);
156                workspace.dismiss_modal(cx);
157            }
158        }
159    }
160}
161
162impl PickerDelegate for ProjectSymbolsView {
163    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
164        if let Some(symbol) = self
165            .matches
166            .get(self.selected_match_index)
167            .map(|mat| self.symbols[mat.candidate_id].clone())
168        {
169            cx.emit(Event::Selected(symbol));
170        }
171    }
172
173    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
174        cx.emit(Event::Dismissed);
175    }
176
177    fn match_count(&self) -> usize {
178        self.matches.len()
179    }
180
181    fn selected_index(&self) -> usize {
182        self.selected_match_index
183    }
184
185    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
186        self.selected_match_index = ix;
187        cx.notify();
188    }
189
190    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
191        self.filter(&query, cx);
192        self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
193        let symbols = self
194            .project
195            .update(cx, |project, cx| project.symbols(&query, cx));
196        cx.spawn_weak(|this, mut cx| async move {
197            let symbols = symbols.await.log_err();
198            if let Some(this) = this.upgrade(&cx) {
199                if let Some(symbols) = symbols {
200                    this.update(&mut cx, |this, cx| {
201                        this.match_candidates = symbols
202                            .iter()
203                            .enumerate()
204                            .map(|(id, symbol)| {
205                                StringMatchCandidate::new(
206                                    id,
207                                    symbol.label.text[symbol.label.filter_range.clone()]
208                                        .to_string(),
209                                )
210                            })
211                            .collect();
212                        this.symbols = symbols;
213                        this.filter(&query, cx);
214                    });
215                }
216            }
217        })
218    }
219
220    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
221        let string_match = &self.matches[ix];
222        let settings = cx.global::<Settings>();
223        let style = if selected {
224            &settings.theme.selector.active_item
225        } else {
226            &settings.theme.selector.item
227        };
228        let symbol = &self.symbols[string_match.candidate_id];
229        let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
230
231        let mut path = symbol.path.to_string_lossy();
232        if self.show_worktree_root_name {
233            let project = self.project.read(cx);
234            if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
235                path = Cow::Owned(format!(
236                    "{}{}{}",
237                    worktree.read(cx).root_name(),
238                    std::path::MAIN_SEPARATOR,
239                    path.as_ref()
240                ));
241            }
242        }
243
244        Flex::column()
245            .with_child(
246                Text::new(symbol.label.text.clone(), style.label.text.clone())
247                    .with_soft_wrap(false)
248                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
249                        &symbol.label.text,
250                        style.label.text.clone().into(),
251                        syntax_runs,
252                        &string_match.positions,
253                    ))
254                    .boxed(),
255            )
256            .with_child(
257                // Avoid styling the path differently when it is selected, since
258                // the symbol's syntax highlighting doesn't change when selected.
259                Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(),
260            )
261            .contained()
262            .with_style(style.container)
263            .boxed()
264    }
265}