symbol_context_picker.rs

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