use std::cmp::Reverse; use std::sync::Arc; use std::sync::atomic::AtomicBool; use anyhow::Result; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use project::{DocumentSymbol, Symbol}; use ui::{ListItem, prelude::*}; use util::ResultExt as _; use workspace::Workspace; use crate::context_picker::ContextPicker; use agent::context::AgentContextHandle; use agent::context_store::ContextStore; pub struct SymbolContextPicker { picker: Entity>, } impl SymbolContextPicker { pub fn new( context_picker: WeakEntity, workspace: WeakEntity, context_store: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); Self { picker } } } impl Focusable for SymbolContextPicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for SymbolContextPicker { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.picker.clone() } } pub struct SymbolContextPickerDelegate { context_picker: WeakEntity, workspace: WeakEntity, context_store: WeakEntity, matches: Vec, selected_index: usize, } impl SymbolContextPickerDelegate { pub fn new( context_picker: WeakEntity, workspace: WeakEntity, context_store: WeakEntity, ) -> Self { Self { context_picker, workspace, context_store, matches: Vec::new(), selected_index: 0, } } } impl PickerDelegate for SymbolContextPickerDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { self.matches.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _cx: &mut Context>, ) { self.selected_index = ix; } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search symbols…".into() } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context>, ) -> Task<()> { let Some(workspace) = self.workspace.upgrade() else { return Task::ready(()); }; let search_task = search_symbols(query, Arc::::default(), &workspace, cx); let context_store = self.context_store.clone(); cx.spawn_in(window, async move |this, cx| { let symbols = search_task.await; let symbol_entries = context_store .read_with(cx, |context_store, cx| { compute_symbol_entries(symbols, context_store, cx) }) .log_err() .unwrap_or_default(); this.update(cx, |this, _cx| { this.delegate.matches = symbol_entries; }) .log_err(); }) } fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) { let Some(mat) = self.matches.get(self.selected_index) else { return; }; let Some(workspace) = self.workspace.upgrade() else { return; }; let add_symbol_task = add_symbol( mat.symbol.clone(), true, workspace, self.context_store.clone(), cx, ); let selected_index = self.selected_index; cx.spawn(async move |this, cx| { let (_, included) = add_symbol_task.await?; this.update(cx, |this, _| { if let Some(mat) = this.delegate.matches.get_mut(selected_index) { mat.is_included = included; } }) }) .detach_and_log_err(cx); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.context_picker .update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _: &mut Context>, ) -> Option { let mat = &self.matches[ix]; Some(ListItem::new(ix).inset(true).toggle_state(selected).child( render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat), )) } } pub(crate) struct SymbolEntry { pub symbol: Symbol, pub is_included: bool, } pub(crate) fn add_symbol( symbol: Symbol, remove_if_exists: bool, workspace: Entity, context_store: WeakEntity, cx: &mut App, ) -> Task, bool)>> { let project = workspace.read(cx).project().clone(); let open_buffer_task = project.update(cx, |project, cx| { project.open_buffer(symbol.path.clone(), cx) }); cx.spawn(async move |cx| { let buffer = open_buffer_task.await?; let document_symbols = project .update(cx, |project, cx| project.document_symbols(&buffer, cx))? .await?; // Try to find a matching document symbol. Document symbols include // not only the symbol itself (e.g. function name), but they also // include the context that they contain (e.g. function body). let (name, range, enclosing_range) = if let Some(DocumentSymbol { name, range, selection_range, .. }) = find_matching_symbol(&symbol, document_symbols.as_slice()) { (name, selection_range, range) } else { // If we do not find a matching document symbol, fall back to // just the symbol itself (symbol.name, symbol.range.clone(), symbol.range) }; let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| { ( buffer.anchor_after(range.start)..buffer.anchor_before(range.end), buffer.anchor_after(enclosing_range.start) ..buffer.anchor_before(enclosing_range.end), ) })?; context_store.update(cx, move |context_store, cx| { context_store.add_symbol( buffer, name.into(), range, enclosing_range, remove_if_exists, cx, ) }) }) } fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option { let mut candidates = candidates.iter(); let mut candidate = candidates.next()?; loop { if candidate.range.start > symbol.range.end { return None; } if candidate.range.end < symbol.range.start { candidate = candidates.next()?; continue; } if candidate.selection_range == symbol.range { return Some(candidate.clone()); } if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end { candidates = candidate.children.iter(); candidate = candidates.next()?; continue; } return None; } } pub struct SymbolMatch { pub symbol: Symbol, } pub(crate) fn search_symbols( query: String, cancellation_flag: Arc, workspace: &Entity, cx: &mut App, ) -> Task> { let symbols_task = workspace.update(cx, |workspace, cx| { workspace .project() .update(cx, |project, cx| project.symbols(&query, cx)) }); let project = workspace.read(cx).project().clone(); cx.spawn(async move |cx| { let Some(symbols) = symbols_task.await.log_err() else { return Vec::new(); }; let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> = project .update(cx, |project, cx| { symbols .iter() .enumerate() .map(|(id, symbol)| { StringMatchCandidate::new(id, symbol.label.filter_text()) }) .partition(|candidate| { project .entry_for_path(&symbols[candidate.id].path, cx) .is_some_and(|e| !e.is_ignored) }) }) .log_err() else { return Vec::new(); }; const MAX_MATCHES: usize = 100; let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( &visible_match_candidates, &query, false, true, MAX_MATCHES, &cancellation_flag, cx.background_executor().clone(), )); let mut external_matches = cx.background_executor().block(fuzzy::match_strings( &external_match_candidates, &query, false, true, MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), &cancellation_flag, cx.background_executor().clone(), )); let sort_key_for_match = |mat: &StringMatch| { let symbol = &symbols[mat.candidate_id]; (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text()) }; visible_matches.sort_unstable_by_key(sort_key_for_match); external_matches.sort_unstable_by_key(sort_key_for_match); let mut matches = visible_matches; matches.append(&mut external_matches); matches .into_iter() .map(|mut mat| { let symbol = symbols[mat.candidate_id].clone(); let filter_start = symbol.label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } SymbolMatch { symbol } }) .collect() }) } fn compute_symbol_entries( symbols: Vec, context_store: &ContextStore, cx: &App, ) -> Vec { symbols .into_iter() .map(|SymbolMatch { symbol, .. }| SymbolEntry { is_included: context_store.includes_symbol(&symbol, cx), symbol, }) .collect::>() } pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful
{ let path = entry .symbol .path .path .file_name() .map(|s| s.to_string_lossy()) .unwrap_or_default(); let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1); h_flex() .id(id) .gap_1p5() .w_full() .child( Icon::new(IconName::Code) .size(IconSize::Small) .color(Color::Muted), ) .child( h_flex() .gap_1() .child(Label::new(&entry.symbol.name)) .child( Label::new(symbol_location) .size(LabelSize::Small) .color(Color::Muted), ), ) .when(entry.is_included, |el| { el.child( h_flex() .w_full() .justify_end() .gap_0p5() .child( Icon::new(IconName::Check) .size(IconSize::Small) .color(Color::Success), ) .child(Label::new("Added").size(LabelSize::Small)), ) }) }