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}