symbol_context_picker.rs

  1use std::cmp::Reverse;
  2use std::sync::Arc;
  3use std::sync::atomic::AtomicBool;
  4
  5use anyhow::Result;
  6use fuzzy::{StringMatch, StringMatchCandidate};
  7use gpui::{
  8    App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
  9};
 10use ordered_float::OrderedFloat;
 11use picker::{Picker, PickerDelegate};
 12use project::{DocumentSymbol, Symbol};
 13use ui::{ListItem, prelude::*};
 14use util::ResultExt as _;
 15use workspace::Workspace;
 16
 17use crate::context_picker::ContextPicker;
 18use crate::context_store::ContextStore;
 19
 20pub struct SymbolContextPicker {
 21    picker: Entity<Picker<SymbolContextPickerDelegate>>,
 22}
 23
 24impl SymbolContextPicker {
 25    pub fn new(
 26        context_picker: WeakEntity<ContextPicker>,
 27        workspace: WeakEntity<Workspace>,
 28        context_store: WeakEntity<ContextStore>,
 29        window: &mut Window,
 30        cx: &mut Context<Self>,
 31    ) -> Self {
 32        let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
 33        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 34
 35        Self { picker }
 36    }
 37}
 38
 39impl Focusable for SymbolContextPicker {
 40    fn focus_handle(&self, cx: &App) -> FocusHandle {
 41        self.picker.focus_handle(cx)
 42    }
 43}
 44
 45impl Render for SymbolContextPicker {
 46    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 47        self.picker.clone()
 48    }
 49}
 50
 51pub struct SymbolContextPickerDelegate {
 52    context_picker: WeakEntity<ContextPicker>,
 53    workspace: WeakEntity<Workspace>,
 54    context_store: WeakEntity<ContextStore>,
 55    matches: Vec<SymbolEntry>,
 56    selected_index: usize,
 57}
 58
 59impl SymbolContextPickerDelegate {
 60    pub fn new(
 61        context_picker: WeakEntity<ContextPicker>,
 62        workspace: WeakEntity<Workspace>,
 63        context_store: WeakEntity<ContextStore>,
 64    ) -> Self {
 65        Self {
 66            context_picker,
 67            workspace,
 68            context_store,
 69            matches: Vec::new(),
 70            selected_index: 0,
 71        }
 72    }
 73}
 74
 75impl PickerDelegate for SymbolContextPickerDelegate {
 76    type ListItem = ListItem;
 77
 78    fn match_count(&self) -> usize {
 79        self.matches.len()
 80    }
 81
 82    fn selected_index(&self) -> usize {
 83        self.selected_index
 84    }
 85
 86    fn set_selected_index(
 87        &mut self,
 88        ix: usize,
 89        _window: &mut Window,
 90        _cx: &mut Context<Picker<Self>>,
 91    ) {
 92        self.selected_index = ix;
 93    }
 94
 95    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 96        "Search symbols…".into()
 97    }
 98
 99    fn update_matches(
100        &mut self,
101        query: String,
102        window: &mut Window,
103        cx: &mut Context<Picker<Self>>,
104    ) -> Task<()> {
105        let Some(workspace) = self.workspace.upgrade() else {
106            return Task::ready(());
107        };
108
109        let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
110        let context_store = self.context_store.clone();
111        cx.spawn_in(window, async move |this, cx| {
112            let symbols = search_task.await;
113
114            let symbol_entries = context_store
115                .read_with(cx, |context_store, cx| {
116                    compute_symbol_entries(symbols, context_store, cx)
117                })
118                .log_err()
119                .unwrap_or_default();
120
121            this.update(cx, |this, _cx| {
122                this.delegate.matches = symbol_entries;
123            })
124            .log_err();
125        })
126    }
127
128    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
129        let Some(mat) = self.matches.get(self.selected_index) else {
130            return;
131        };
132        let Some(workspace) = self.workspace.upgrade() else {
133            return;
134        };
135
136        let add_symbol_task = add_symbol(
137            mat.symbol.clone(),
138            true,
139            workspace,
140            self.context_store.clone(),
141            cx,
142        );
143
144        let selected_index = self.selected_index;
145        cx.spawn(async move |this, cx| {
146            let included = add_symbol_task.await?;
147            this.update(cx, |this, _| {
148                if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
149                    mat.is_included = included;
150                }
151            })
152        })
153        .detach_and_log_err(cx);
154    }
155
156    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
157        self.context_picker
158            .update(cx, |_, cx| {
159                cx.emit(DismissEvent);
160            })
161            .ok();
162    }
163
164    fn render_match(
165        &self,
166        ix: usize,
167        selected: bool,
168        _window: &mut Window,
169        _: &mut Context<Picker<Self>>,
170    ) -> Option<Self::ListItem> {
171        let mat = &self.matches[ix];
172
173        Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
174            render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
175        ))
176    }
177}
178
179pub(crate) struct SymbolEntry {
180    pub symbol: Symbol,
181    pub is_included: bool,
182}
183
184pub(crate) fn add_symbol(
185    symbol: Symbol,
186    remove_if_exists: bool,
187    workspace: Entity<Workspace>,
188    context_store: WeakEntity<ContextStore>,
189    cx: &mut App,
190) -> Task<Result<bool>> {
191    let project = workspace.read(cx).project().clone();
192    let open_buffer_task = project.update(cx, |project, cx| {
193        project.open_buffer(symbol.path.clone(), cx)
194    });
195    cx.spawn(async move |cx| {
196        let buffer = open_buffer_task.await?;
197        let document_symbols = project
198            .update(cx, |project, cx| project.document_symbols(&buffer, cx))?
199            .await?;
200
201        // Try to find a matching document symbol. Document symbols include
202        // not only the symbol itself (e.g. function name), but they also
203        // include the context that they contain (e.g. function body).
204        let (name, range, enclosing_range) = if let Some(DocumentSymbol {
205            name,
206            range,
207            selection_range,
208            ..
209        }) =
210            find_matching_symbol(&symbol, document_symbols.as_slice())
211        {
212            (name, selection_range, range)
213        } else {
214            // If we do not find a matching document symbol, fall back to
215            // just the symbol itself
216            (symbol.name, symbol.range.clone(), symbol.range)
217        };
218
219        let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
220            (
221                buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
222                buffer.anchor_after(enclosing_range.start)
223                    ..buffer.anchor_before(enclosing_range.end),
224            )
225        })?;
226
227        context_store.update(cx, move |context_store, cx| {
228            context_store.add_symbol(
229                buffer,
230                name.into(),
231                range,
232                enclosing_range,
233                remove_if_exists,
234                cx,
235            )
236        })
237    })
238}
239
240fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
241    let mut candidates = candidates.iter();
242    let mut candidate = candidates.next()?;
243
244    loop {
245        if candidate.range.start > symbol.range.end {
246            return None;
247        }
248        if candidate.range.end < symbol.range.start {
249            candidate = candidates.next()?;
250            continue;
251        }
252        if candidate.selection_range == symbol.range {
253            return Some(candidate.clone());
254        }
255        if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
256            candidates = candidate.children.iter();
257            candidate = candidates.next()?;
258            continue;
259        }
260        return None;
261    }
262}
263
264pub struct SymbolMatch {
265    pub symbol: Symbol,
266}
267
268pub(crate) fn search_symbols(
269    query: String,
270    cancellation_flag: Arc<AtomicBool>,
271    workspace: &Entity<Workspace>,
272    cx: &mut App,
273) -> Task<Vec<SymbolMatch>> {
274    let symbols_task = workspace.update(cx, |workspace, cx| {
275        workspace
276            .project()
277            .update(cx, |project, cx| project.symbols(&query, cx))
278    });
279    let project = workspace.read(cx).project().clone();
280    cx.spawn(async move |cx| {
281        let Some(symbols) = symbols_task.await.log_err() else {
282            return Vec::new();
283        };
284        let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
285            project
286                .update(cx, |project, cx| {
287                    symbols
288                        .iter()
289                        .enumerate()
290                        .map(|(id, symbol)| {
291                            StringMatchCandidate::new(id, &symbol.label.filter_text())
292                        })
293                        .partition(|candidate| {
294                            project
295                                .entry_for_path(&symbols[candidate.id].path, cx)
296                                .map_or(false, |e| !e.is_ignored)
297                        })
298                })
299                .log_err()
300        else {
301            return Vec::new();
302        };
303
304        const MAX_MATCHES: usize = 100;
305        let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
306            &visible_match_candidates,
307            &query,
308            false,
309            MAX_MATCHES,
310            &cancellation_flag,
311            cx.background_executor().clone(),
312        ));
313        let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
314            &external_match_candidates,
315            &query,
316            false,
317            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
318            &cancellation_flag,
319            cx.background_executor().clone(),
320        ));
321        let sort_key_for_match = |mat: &StringMatch| {
322            let symbol = &symbols[mat.candidate_id];
323            (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
324        };
325
326        visible_matches.sort_unstable_by_key(sort_key_for_match);
327        external_matches.sort_unstable_by_key(sort_key_for_match);
328        let mut matches = visible_matches;
329        matches.append(&mut external_matches);
330
331        matches
332            .into_iter()
333            .map(|mut mat| {
334                let symbol = symbols[mat.candidate_id].clone();
335                let filter_start = symbol.label.filter_range.start;
336                for position in &mut mat.positions {
337                    *position += filter_start;
338                }
339                SymbolMatch { symbol }
340            })
341            .collect()
342    })
343}
344
345fn compute_symbol_entries(
346    symbols: Vec<SymbolMatch>,
347    context_store: &ContextStore,
348    cx: &App,
349) -> Vec<SymbolEntry> {
350    symbols
351        .into_iter()
352        .map(|SymbolMatch { symbol, .. }| SymbolEntry {
353            is_included: context_store.includes_symbol(&symbol, cx),
354            symbol,
355        })
356        .collect::<Vec<_>>()
357}
358
359pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
360    let path = entry
361        .symbol
362        .path
363        .path
364        .file_name()
365        .map(|s| s.to_string_lossy())
366        .unwrap_or_default();
367    let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
368
369    h_flex()
370        .id(id)
371        .gap_1p5()
372        .w_full()
373        .child(
374            Icon::new(IconName::Code)
375                .size(IconSize::Small)
376                .color(Color::Muted),
377        )
378        .child(
379            h_flex()
380                .gap_1()
381                .child(Label::new(&entry.symbol.name))
382                .child(
383                    Label::new(symbol_location)
384                        .size(LabelSize::Small)
385                        .color(Color::Muted),
386                ),
387        )
388        .when(entry.is_included, |el| {
389            el.child(
390                h_flex()
391                    .w_full()
392                    .justify_end()
393                    .gap_0p5()
394                    .child(
395                        Icon::new(IconName::Check)
396                            .size(IconSize::Small)
397                            .color(Color::Success),
398                    )
399                    .child(Label::new("Added").size(LabelSize::Small)),
400            )
401        })
402}