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}