1use crate::scroll::ScrollAmount;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{
4 AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
5 SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
6 uniform_list,
7};
8use itertools::Itertools;
9use language::CodeLabel;
10use language::{Buffer, LanguageName, LanguageRegistry};
11use markdown::{Markdown, MarkdownElement};
12use multi_buffer::{Anchor, ExcerptId};
13use ordered_float::OrderedFloat;
14use project::CompletionSource;
15use project::lsp_store::CompletionDocumentation;
16use project::{CodeAction, Completion, TaskSourceKind};
17use task::DebugScenario;
18use task::TaskContext;
19
20use std::collections::VecDeque;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::{
24 cell::RefCell,
25 cmp::{Reverse, min},
26 iter,
27 ops::Range,
28 rc::Rc,
29};
30use task::ResolvedTask;
31use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
32use util::ResultExt;
33
34use crate::CodeActionSource;
35use crate::editor_settings::SnippetSortOrder;
36use crate::hover_popover::{hover_markdown_style, open_markdown_url};
37use crate::{
38 CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
39 EditorStyle, ResolvedTasks,
40 actions::{ConfirmCodeAction, ConfirmCompletion},
41 split_words, styled_runs_for_code_label,
42};
43
44pub const MENU_GAP: Pixels = px(4.);
45pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
46pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
47pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
48
49// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
50// documentation not yet being parsed.
51//
52// The size of the cache is set to 16, which is roughly 3 times more than the number of items
53// fetched around the current selection. This way documentation is more often ready for render when
54// revisiting previous entries, such as when pressing backspace.
55const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
56const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
57const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
58
59// Number of items beyond the visible items to resolve documentation.
60const RESOLVE_BEFORE_ITEMS: usize = 4;
61const RESOLVE_AFTER_ITEMS: usize = 4;
62
63pub enum CodeContextMenu {
64 Completions(CompletionsMenu),
65 CodeActions(CodeActionsMenu),
66}
67
68impl CodeContextMenu {
69 pub fn select_first(
70 &mut self,
71 provider: Option<&dyn CompletionProvider>,
72 window: &mut Window,
73 cx: &mut Context<Editor>,
74 ) -> bool {
75 if self.visible() {
76 match self {
77 CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
78 CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
79 }
80 true
81 } else {
82 false
83 }
84 }
85
86 pub fn select_prev(
87 &mut self,
88 provider: Option<&dyn CompletionProvider>,
89 window: &mut Window,
90 cx: &mut Context<Editor>,
91 ) -> bool {
92 if self.visible() {
93 match self {
94 CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
95 CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
96 }
97 true
98 } else {
99 false
100 }
101 }
102
103 pub fn select_next(
104 &mut self,
105 provider: Option<&dyn CompletionProvider>,
106 window: &mut Window,
107 cx: &mut Context<Editor>,
108 ) -> bool {
109 if self.visible() {
110 match self {
111 CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
112 CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
113 }
114 true
115 } else {
116 false
117 }
118 }
119
120 pub fn select_last(
121 &mut self,
122 provider: Option<&dyn CompletionProvider>,
123 window: &mut Window,
124 cx: &mut Context<Editor>,
125 ) -> bool {
126 if self.visible() {
127 match self {
128 CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
129 CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
130 }
131 true
132 } else {
133 false
134 }
135 }
136
137 pub fn visible(&self) -> bool {
138 match self {
139 CodeContextMenu::Completions(menu) => menu.visible(),
140 CodeContextMenu::CodeActions(menu) => menu.visible(),
141 }
142 }
143
144 pub fn origin(&self) -> ContextMenuOrigin {
145 match self {
146 CodeContextMenu::Completions(menu) => menu.origin(),
147 CodeContextMenu::CodeActions(menu) => menu.origin(),
148 }
149 }
150
151 pub fn render(
152 &self,
153 style: &EditorStyle,
154 max_height_in_lines: u32,
155 window: &mut Window,
156 cx: &mut Context<Editor>,
157 ) -> AnyElement {
158 match self {
159 CodeContextMenu::Completions(menu) => {
160 menu.render(style, max_height_in_lines, window, cx)
161 }
162 CodeContextMenu::CodeActions(menu) => {
163 menu.render(style, max_height_in_lines, window, cx)
164 }
165 }
166 }
167
168 pub fn render_aside(
169 &mut self,
170 max_size: Size<Pixels>,
171 window: &mut Window,
172 cx: &mut Context<Editor>,
173 ) -> Option<AnyElement> {
174 match self {
175 CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
176 CodeContextMenu::CodeActions(_) => None,
177 }
178 }
179
180 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
181 match self {
182 CodeContextMenu::Completions(completions_menu) => completions_menu
183 .get_or_create_entry_markdown(completions_menu.selected_item, cx)
184 .as_ref()
185 .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
186 CodeContextMenu::CodeActions(_) => false,
187 }
188 }
189
190 pub fn scroll_aside(
191 &mut self,
192 scroll_amount: ScrollAmount,
193 window: &mut Window,
194 cx: &mut Context<Editor>,
195 ) {
196 match self {
197 CodeContextMenu::Completions(completions_menu) => {
198 completions_menu.scroll_aside(scroll_amount, window, cx)
199 }
200 CodeContextMenu::CodeActions(_) => (),
201 }
202 }
203}
204
205pub enum ContextMenuOrigin {
206 Cursor,
207 GutterIndicator(DisplayRow),
208 QuickActionBar,
209}
210
211pub struct CompletionsMenu {
212 pub id: CompletionId,
213 pub source: CompletionsMenuSource,
214 sort_completions: bool,
215 pub initial_position: Anchor,
216 pub initial_query: Option<Arc<String>>,
217 pub is_incomplete: bool,
218 pub buffer: Entity<Buffer>,
219 pub completions: Rc<RefCell<Box<[Completion]>>>,
220 match_candidates: Arc<[StringMatchCandidate]>,
221 pub entries: Rc<RefCell<Box<[StringMatch]>>>,
222 pub selected_item: usize,
223 filter_task: Task<()>,
224 cancel_filter: Arc<AtomicBool>,
225 scroll_handle: UniformListScrollHandle,
226 // The `ScrollHandle` used on the Markdown documentation rendered on the
227 // side of the completions menu.
228 pub scroll_handle_aside: ScrollHandle,
229 resolve_completions: bool,
230 show_completion_documentation: bool,
231 last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
232 markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
233 language_registry: Option<Arc<LanguageRegistry>>,
234 language: Option<LanguageName>,
235 snippet_sort_order: SnippetSortOrder,
236}
237
238#[derive(Clone, Debug, PartialEq)]
239enum MarkdownCacheKey {
240 ForCandidate {
241 candidate_id: usize,
242 },
243 ForCompletionMatch {
244 new_text: String,
245 markdown_source: SharedString,
246 },
247}
248
249#[derive(Copy, Clone, Debug, PartialEq, Eq)]
250pub enum CompletionsMenuSource {
251 Normal,
252 SnippetChoices,
253 Words,
254}
255
256// TODO: There should really be a wrapper around fuzzy match tasks that does this.
257impl Drop for CompletionsMenu {
258 fn drop(&mut self) {
259 self.cancel_filter.store(true, Ordering::Relaxed);
260 }
261}
262
263impl CompletionsMenu {
264 pub fn new(
265 id: CompletionId,
266 source: CompletionsMenuSource,
267 sort_completions: bool,
268 show_completion_documentation: bool,
269 initial_position: Anchor,
270 initial_query: Option<Arc<String>>,
271 is_incomplete: bool,
272 buffer: Entity<Buffer>,
273 completions: Box<[Completion]>,
274 snippet_sort_order: SnippetSortOrder,
275 language_registry: Option<Arc<LanguageRegistry>>,
276 language: Option<LanguageName>,
277 cx: &mut Context<Editor>,
278 ) -> Self {
279 let match_candidates = completions
280 .iter()
281 .enumerate()
282 .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
283 .collect();
284
285 let completions_menu = Self {
286 id,
287 source,
288 sort_completions,
289 initial_position,
290 initial_query,
291 is_incomplete,
292 buffer,
293 show_completion_documentation,
294 completions: RefCell::new(completions).into(),
295 match_candidates,
296 entries: Rc::new(RefCell::new(Box::new([]))),
297 selected_item: 0,
298 filter_task: Task::ready(()),
299 cancel_filter: Arc::new(AtomicBool::new(false)),
300 scroll_handle: UniformListScrollHandle::new(),
301 scroll_handle_aside: ScrollHandle::new(),
302 resolve_completions: true,
303 last_rendered_range: RefCell::new(None).into(),
304 markdown_cache: RefCell::new(VecDeque::new()).into(),
305 language_registry,
306 language,
307 snippet_sort_order,
308 };
309
310 completions_menu.start_markdown_parse_for_nearby_entries(cx);
311
312 completions_menu
313 }
314
315 pub fn new_snippet_choices(
316 id: CompletionId,
317 sort_completions: bool,
318 choices: &Vec<String>,
319 selection: Range<Anchor>,
320 buffer: Entity<Buffer>,
321 snippet_sort_order: SnippetSortOrder,
322 ) -> Self {
323 let completions = choices
324 .iter()
325 .map(|choice| Completion {
326 replace_range: selection.start.text_anchor..selection.end.text_anchor,
327 new_text: choice.to_string(),
328 label: CodeLabel {
329 text: choice.to_string(),
330 runs: Default::default(),
331 filter_range: Default::default(),
332 },
333 icon_path: None,
334 documentation: None,
335 confirm: None,
336 insert_text_mode: None,
337 source: CompletionSource::Custom,
338 })
339 .collect();
340
341 let match_candidates = choices
342 .iter()
343 .enumerate()
344 .map(|(id, completion)| StringMatchCandidate::new(id, completion))
345 .collect();
346 let entries = choices
347 .iter()
348 .enumerate()
349 .map(|(id, completion)| StringMatch {
350 candidate_id: id,
351 score: 1.,
352 positions: vec![],
353 string: completion.clone(),
354 })
355 .collect();
356 Self {
357 id,
358 source: CompletionsMenuSource::SnippetChoices,
359 sort_completions,
360 initial_position: selection.start,
361 initial_query: None,
362 is_incomplete: false,
363 buffer,
364 completions: RefCell::new(completions).into(),
365 match_candidates,
366 entries: RefCell::new(entries).into(),
367 selected_item: 0,
368 filter_task: Task::ready(()),
369 cancel_filter: Arc::new(AtomicBool::new(false)),
370 scroll_handle: UniformListScrollHandle::new(),
371 scroll_handle_aside: ScrollHandle::new(),
372 resolve_completions: false,
373 show_completion_documentation: false,
374 last_rendered_range: RefCell::new(None).into(),
375 markdown_cache: RefCell::new(VecDeque::new()).into(),
376 language_registry: None,
377 language: None,
378 snippet_sort_order,
379 }
380 }
381
382 fn select_first(
383 &mut self,
384 provider: Option<&dyn CompletionProvider>,
385 window: &mut Window,
386 cx: &mut Context<Editor>,
387 ) {
388 let index = if self.scroll_handle.y_flipped() {
389 self.entries.borrow().len() - 1
390 } else {
391 0
392 };
393 self.update_selection_index(index, provider, window, cx);
394 }
395
396 fn select_last(
397 &mut self,
398 provider: Option<&dyn CompletionProvider>,
399 window: &mut Window,
400 cx: &mut Context<Editor>,
401 ) {
402 let index = if self.scroll_handle.y_flipped() {
403 0
404 } else {
405 self.entries.borrow().len() - 1
406 };
407 self.update_selection_index(index, provider, window, cx);
408 }
409
410 fn select_prev(
411 &mut self,
412 provider: Option<&dyn CompletionProvider>,
413 window: &mut Window,
414 cx: &mut Context<Editor>,
415 ) {
416 let index = if self.scroll_handle.y_flipped() {
417 self.next_match_index()
418 } else {
419 self.prev_match_index()
420 };
421 self.update_selection_index(index, provider, window, cx);
422 }
423
424 fn select_next(
425 &mut self,
426 provider: Option<&dyn CompletionProvider>,
427 window: &mut Window,
428 cx: &mut Context<Editor>,
429 ) {
430 let index = if self.scroll_handle.y_flipped() {
431 self.prev_match_index()
432 } else {
433 self.next_match_index()
434 };
435 self.update_selection_index(index, provider, window, cx);
436 }
437
438 fn update_selection_index(
439 &mut self,
440 match_index: usize,
441 provider: Option<&dyn CompletionProvider>,
442 window: &mut Window,
443 cx: &mut Context<Editor>,
444 ) {
445 if self.selected_item != match_index {
446 self.selected_item = match_index;
447 self.handle_selection_changed(provider, window, cx);
448 }
449 }
450
451 fn prev_match_index(&self) -> usize {
452 if self.selected_item > 0 {
453 self.selected_item - 1
454 } else {
455 self.entries.borrow().len() - 1
456 }
457 }
458
459 fn next_match_index(&self) -> usize {
460 if self.selected_item + 1 < self.entries.borrow().len() {
461 self.selected_item + 1
462 } else {
463 0
464 }
465 }
466
467 fn handle_selection_changed(
468 &mut self,
469 provider: Option<&dyn CompletionProvider>,
470 window: &mut Window,
471 cx: &mut Context<Editor>,
472 ) {
473 self.scroll_handle
474 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
475 if let Some(provider) = provider {
476 let entries = self.entries.borrow();
477 let entry = if self.selected_item < entries.len() {
478 Some(&entries[self.selected_item])
479 } else {
480 None
481 };
482 provider.selection_changed(entry, window, cx);
483 }
484 self.resolve_visible_completions(provider, cx);
485 self.start_markdown_parse_for_nearby_entries(cx);
486 cx.notify();
487 }
488
489 pub fn resolve_visible_completions(
490 &mut self,
491 provider: Option<&dyn CompletionProvider>,
492 cx: &mut Context<Editor>,
493 ) {
494 if !self.resolve_completions {
495 return;
496 }
497 let Some(provider) = provider else {
498 return;
499 };
500
501 let entries = self.entries.borrow();
502 if entries.is_empty() {
503 return;
504 }
505 if self.selected_item >= entries.len() {
506 log::error!(
507 "bug: completion selected_item >= entries.len(): {} >= {}",
508 self.selected_item,
509 entries.len()
510 );
511 self.selected_item = entries.len() - 1;
512 }
513
514 // Attempt to resolve completions for every item that will be displayed. This matters
515 // because single line documentation may be displayed inline with the completion.
516 //
517 // When navigating to the very beginning or end of completions, `last_rendered_range` may
518 // have no overlap with the completions that will be displayed, so instead use a range based
519 // on the last rendered count.
520 const APPROXIMATE_VISIBLE_COUNT: usize = 12;
521 let last_rendered_range = self.last_rendered_range.borrow().clone();
522 let visible_count = last_rendered_range
523 .clone()
524 .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
525 let entry_range = if self.selected_item == 0 {
526 0..min(visible_count, entries.len())
527 } else if self.selected_item == entries.len() - 1 {
528 entries.len().saturating_sub(visible_count)..entries.len()
529 } else {
530 last_rendered_range.map_or(0..0, |range| {
531 min(range.start, entries.len())..min(range.end, entries.len())
532 })
533 };
534
535 // Expand the range to resolve more completions than are predicted to be visible, to reduce
536 // jank on navigation.
537 let entry_indices = util::expanded_and_wrapped_usize_range(
538 entry_range,
539 RESOLVE_BEFORE_ITEMS,
540 RESOLVE_AFTER_ITEMS,
541 entries.len(),
542 );
543
544 // Avoid work by sometimes filtering out completions that already have documentation.
545 // This filtering doesn't happen if the completions are currently being updated.
546 let completions = self.completions.borrow();
547 let candidate_ids = entry_indices
548 .map(|i| entries[i].candidate_id)
549 .filter(|i| completions[*i].documentation.is_none());
550
551 // Current selection is always resolved even if it already has documentation, to handle
552 // out-of-spec language servers that return more results later.
553 let selected_candidate_id = entries[self.selected_item].candidate_id;
554 let candidate_ids = iter::once(selected_candidate_id)
555 .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
556 .collect::<Vec<usize>>();
557 drop(entries);
558
559 if candidate_ids.is_empty() {
560 return;
561 }
562
563 let resolve_task = provider.resolve_completions(
564 self.buffer.clone(),
565 candidate_ids,
566 self.completions.clone(),
567 cx,
568 );
569
570 let completion_id = self.id;
571 cx.spawn(async move |editor, cx| {
572 if let Some(true) = resolve_task.await.log_err() {
573 editor
574 .update(cx, |editor, cx| {
575 // `resolve_completions` modified state affecting display.
576 cx.notify();
577 editor.with_completions_menu_matching_id(completion_id, |menu| {
578 if let Some(menu) = menu {
579 menu.start_markdown_parse_for_nearby_entries(cx)
580 }
581 });
582 })
583 .ok();
584 }
585 })
586 .detach();
587 }
588
589 fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
590 // Enqueue parse tasks of nearer items first.
591 //
592 // TODO: This means that the nearer items will actually be further back in the cache, which
593 // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
594 // selection to the front (when `is_render = true`).
595 let entry_indices = util::wrapped_usize_outward_from(
596 self.selected_item,
597 MARKDOWN_CACHE_BEFORE_ITEMS,
598 MARKDOWN_CACHE_AFTER_ITEMS,
599 self.entries.borrow().len(),
600 );
601
602 for index in entry_indices {
603 self.get_or_create_entry_markdown(index, cx);
604 }
605 }
606
607 fn get_or_create_entry_markdown(
608 &self,
609 index: usize,
610 cx: &mut Context<Editor>,
611 ) -> Option<Entity<Markdown>> {
612 let entries = self.entries.borrow();
613 if index >= entries.len() {
614 return None;
615 }
616 let candidate_id = entries[index].candidate_id;
617 let completions = self.completions.borrow();
618 match &completions[candidate_id].documentation {
619 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
620 .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
621 .map(|(_, markdown)| markdown),
622 Some(_) => None,
623 _ => None,
624 }
625 }
626
627 fn get_or_create_markdown(
628 &self,
629 candidate_id: usize,
630 source: Option<&SharedString>,
631 is_render: bool,
632 completions: &[Completion],
633 cx: &mut Context<Editor>,
634 ) -> Option<(bool, Entity<Markdown>)> {
635 let mut markdown_cache = self.markdown_cache.borrow_mut();
636
637 let mut has_completion_match_cache_entry = false;
638 let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
639 MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
640 MarkdownCacheKey::ForCompletionMatch { .. } => {
641 has_completion_match_cache_entry = true;
642 false
643 }
644 });
645
646 if has_completion_match_cache_entry && matching_entry.is_none() {
647 if let Some(source) = source {
648 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
649 matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
650 if markdown_source == source)
651 });
652 } else {
653 // Heuristic guess that documentation can be reused when new_text matches. This is
654 // to mitigate documentation flicker while typing. If this is wrong, then resolution
655 // should cause the correct documentation to be displayed soon.
656 let completion = &completions[candidate_id];
657 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
658 matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
659 if new_text == &completion.new_text)
660 });
661 }
662 }
663
664 if let Some((cache_index, (key, markdown))) = matching_entry {
665 let markdown = markdown.clone();
666
667 // Since the markdown source matches, the key can now be ForCandidate.
668 if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
669 markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
670 }
671
672 if is_render && cache_index != 0 {
673 // Move the current selection's cache entry to the front.
674 markdown_cache.rotate_right(1);
675 let cache_len = markdown_cache.len();
676 markdown_cache.swap(0, (cache_index + 1) % cache_len);
677 }
678
679 let is_parsing = markdown.update(cx, |markdown, cx| {
680 if let Some(source) = source {
681 // `reset` is called as it's possible for documentation to change due to resolve
682 // requests. It does nothing if `source` is unchanged.
683 markdown.reset(source.clone(), cx);
684 }
685 markdown.is_parsing()
686 });
687 return Some((is_parsing, markdown));
688 }
689
690 let Some(source) = source else {
691 // Can't create markdown as there is no source.
692 return None;
693 };
694
695 if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
696 let markdown = cx.new(|cx| {
697 Markdown::new(
698 source.clone(),
699 self.language_registry.clone(),
700 self.language.clone(),
701 cx,
702 )
703 });
704 // Handles redraw when the markdown is done parsing. The current render is for a
705 // deferred draw, and so without this did not redraw when `markdown` notified.
706 cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
707 markdown_cache.push_front((
708 MarkdownCacheKey::ForCandidate { candidate_id },
709 markdown.clone(),
710 ));
711 Some((true, markdown))
712 } else {
713 debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
714 // Moves the last cache entry to the start. The ring buffer is full, so this does no
715 // copying and just shifts indexes.
716 markdown_cache.rotate_right(1);
717 markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
718 let markdown = &markdown_cache[0].1;
719 markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
720 Some((true, markdown.clone()))
721 }
722 }
723
724 pub fn visible(&self) -> bool {
725 !self.entries.borrow().is_empty()
726 }
727
728 fn origin(&self) -> ContextMenuOrigin {
729 ContextMenuOrigin::Cursor
730 }
731
732 fn render(
733 &self,
734 style: &EditorStyle,
735 max_height_in_lines: u32,
736 window: &mut Window,
737 cx: &mut Context<Editor>,
738 ) -> AnyElement {
739 let show_completion_documentation = self.show_completion_documentation;
740 let selected_item = self.selected_item;
741 let completions = self.completions.clone();
742 let entries = self.entries.clone();
743 let last_rendered_range = self.last_rendered_range.clone();
744 let style = style.clone();
745 let list = uniform_list(
746 "completions",
747 self.entries.borrow().len(),
748 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
749 last_rendered_range.borrow_mut().replace(range.clone());
750 let start_ix = range.start;
751 let completions_guard = completions.borrow_mut();
752
753 entries.borrow()[range]
754 .iter()
755 .enumerate()
756 .map(|(ix, mat)| {
757 let item_ix = start_ix + ix;
758 let completion = &completions_guard[mat.candidate_id];
759 let documentation = if show_completion_documentation {
760 &completion.documentation
761 } else {
762 &None
763 };
764
765 let filter_start = completion.label.filter_range.start;
766 let highlights = gpui::combine_highlights(
767 mat.ranges().map(|range| {
768 (
769 filter_start + range.start..filter_start + range.end,
770 FontWeight::BOLD.into(),
771 )
772 }),
773 styled_runs_for_code_label(&completion.label, &style.syntax).map(
774 |(range, mut highlight)| {
775 // Ignore font weight for syntax highlighting, as we'll use it
776 // for fuzzy matches.
777 highlight.font_weight = None;
778 if completion
779 .source
780 .lsp_completion(false)
781 .and_then(|lsp_completion| lsp_completion.deprecated)
782 .unwrap_or(false)
783 {
784 highlight.strikethrough = Some(StrikethroughStyle {
785 thickness: 1.0.into(),
786 ..Default::default()
787 });
788 highlight.color = Some(cx.theme().colors().text_muted);
789 }
790
791 (range, highlight)
792 },
793 ),
794 );
795
796 let completion_label = StyledText::new(completion.label.text.clone())
797 .with_default_highlights(&style.text, highlights);
798
799 let documentation_label = match documentation {
800 Some(CompletionDocumentation::SingleLine(text))
801 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
802 single_line: text,
803 ..
804 }) => {
805 if text.trim().is_empty() {
806 None
807 } else {
808 Some(
809 Label::new(text.clone())
810 .ml_4()
811 .size(LabelSize::Small)
812 .color(Color::Muted),
813 )
814 }
815 }
816 _ => None,
817 };
818
819 let start_slot = completion
820 .color()
821 .map(|color| {
822 div()
823 .flex_shrink_0()
824 .size_3p5()
825 .rounded_xs()
826 .bg(color)
827 .into_any_element()
828 })
829 .or_else(|| {
830 completion.icon_path.as_ref().map(|path| {
831 Icon::from_path(path)
832 .size(IconSize::XSmall)
833 .color(Color::Muted)
834 .into_any_element()
835 })
836 });
837
838 div().min_w(px(280.)).max_w(px(540.)).child(
839 ListItem::new(mat.candidate_id)
840 .inset(true)
841 .toggle_state(item_ix == selected_item)
842 .on_click(cx.listener(move |editor, _event, window, cx| {
843 cx.stop_propagation();
844 if let Some(task) = editor.confirm_completion(
845 &ConfirmCompletion {
846 item_ix: Some(item_ix),
847 },
848 window,
849 cx,
850 ) {
851 task.detach_and_log_err(cx)
852 }
853 }))
854 .start_slot::<AnyElement>(start_slot)
855 .child(h_flex().overflow_hidden().child(completion_label))
856 .end_slot::<Label>(documentation_label),
857 )
858 })
859 .collect()
860 }),
861 )
862 .occlude()
863 .max_h(max_height_in_lines as f32 * window.line_height())
864 .track_scroll(self.scroll_handle.clone())
865 .with_sizing_behavior(ListSizingBehavior::Infer)
866 .w(rems(34.));
867
868 Popover::new().child(list).into_any_element()
869 }
870
871 fn render_aside(
872 &mut self,
873 max_size: Size<Pixels>,
874 window: &mut Window,
875 cx: &mut Context<Editor>,
876 ) -> Option<AnyElement> {
877 if !self.show_completion_documentation {
878 return None;
879 }
880
881 let mat = &self.entries.borrow()[self.selected_item];
882 let completions = self.completions.borrow_mut();
883 let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
884 Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
885 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
886 plain_text: Some(text),
887 ..
888 }) => div().child(text.clone()),
889 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
890 let Some((false, markdown)) = self.get_or_create_markdown(
891 mat.candidate_id,
892 Some(source),
893 true,
894 &completions,
895 cx,
896 ) else {
897 return None;
898 };
899 Self::render_markdown(markdown, window, cx)
900 }
901 None => {
902 // Handle the case where documentation hasn't yet been resolved but there's a
903 // `new_text` match in the cache.
904 //
905 // TODO: It's inconsistent that documentation caching based on matching `new_text`
906 // only works for markdown. Consider generally caching the results of resolving
907 // completions.
908 let Some((false, markdown)) =
909 self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
910 else {
911 return None;
912 };
913 Self::render_markdown(markdown, window, cx)
914 }
915 Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
916 Some(CompletionDocumentation::SingleLine(_)) => return None,
917 Some(CompletionDocumentation::Undocumented) => return None,
918 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
919 plain_text: None,
920 ..
921 }) => {
922 return None;
923 }
924 };
925
926 Some(
927 Popover::new()
928 .child(
929 multiline_docs
930 .id("multiline_docs")
931 .px(MENU_ASIDE_X_PADDING / 2.)
932 .max_w(max_size.width)
933 .max_h(max_size.height)
934 .overflow_y_scroll()
935 .track_scroll(&self.scroll_handle_aside)
936 .occlude(),
937 )
938 .into_any_element(),
939 )
940 }
941
942 fn render_markdown(
943 markdown: Entity<Markdown>,
944 window: &mut Window,
945 cx: &mut Context<Editor>,
946 ) -> Div {
947 div().child(
948 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
949 .code_block_renderer(markdown::CodeBlockRenderer::Default {
950 copy_button: false,
951 copy_button_on_hover: false,
952 border: false,
953 })
954 .on_url_click(open_markdown_url),
955 )
956 }
957
958 pub fn filter(
959 &mut self,
960 query: Option<Arc<String>>,
961 provider: Option<Rc<dyn CompletionProvider>>,
962 window: &mut Window,
963 cx: &mut Context<Editor>,
964 ) {
965 self.cancel_filter.store(true, Ordering::Relaxed);
966 if let Some(query) = query {
967 self.cancel_filter = Arc::new(AtomicBool::new(false));
968 let matches = self.do_async_filtering(query, cx);
969 let id = self.id;
970 self.filter_task = cx.spawn_in(window, async move |editor, cx| {
971 let matches = matches.await;
972 editor
973 .update_in(cx, |editor, window, cx| {
974 editor.with_completions_menu_matching_id(id, |this| {
975 if let Some(this) = this {
976 this.set_filter_results(matches, provider, window, cx);
977 }
978 });
979 })
980 .ok();
981 });
982 } else {
983 self.filter_task = Task::ready(());
984 let matches = self.unfiltered_matches();
985 self.set_filter_results(matches, provider, window, cx);
986 }
987 }
988
989 pub fn do_async_filtering(
990 &self,
991 query: Arc<String>,
992 cx: &Context<Editor>,
993 ) -> Task<Vec<StringMatch>> {
994 let matches_task = cx.background_spawn({
995 let query = query.clone();
996 let match_candidates = self.match_candidates.clone();
997 let cancel_filter = self.cancel_filter.clone();
998 let background_executor = cx.background_executor().clone();
999 async move {
1000 fuzzy::match_strings(
1001 &match_candidates,
1002 &query,
1003 query.chars().any(|c| c.is_uppercase()),
1004 false,
1005 1000,
1006 &cancel_filter,
1007 background_executor,
1008 )
1009 .await
1010 }
1011 });
1012
1013 let completions = self.completions.clone();
1014 let sort_completions = self.sort_completions;
1015 let snippet_sort_order = self.snippet_sort_order;
1016 cx.foreground_executor().spawn(async move {
1017 let mut matches = matches_task.await;
1018
1019 if sort_completions {
1020 matches = Self::sort_string_matches(
1021 matches,
1022 Some(&query),
1023 snippet_sort_order,
1024 completions.borrow().as_ref(),
1025 );
1026 }
1027
1028 matches
1029 })
1030 }
1031
1032 /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
1033 pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
1034 let mut matches = self
1035 .match_candidates
1036 .iter()
1037 .enumerate()
1038 .map(|(candidate_id, candidate)| StringMatch {
1039 candidate_id,
1040 score: Default::default(),
1041 positions: Default::default(),
1042 string: candidate.string.clone(),
1043 })
1044 .collect();
1045
1046 if self.sort_completions {
1047 matches = Self::sort_string_matches(
1048 matches,
1049 None,
1050 self.snippet_sort_order,
1051 self.completions.borrow().as_ref(),
1052 );
1053 }
1054
1055 matches
1056 }
1057
1058 pub fn set_filter_results(
1059 &mut self,
1060 matches: Vec<StringMatch>,
1061 provider: Option<Rc<dyn CompletionProvider>>,
1062 window: &mut Window,
1063 cx: &mut Context<Editor>,
1064 ) {
1065 *self.entries.borrow_mut() = matches.into_boxed_slice();
1066 self.selected_item = 0;
1067 self.handle_selection_changed(provider.as_deref(), window, cx);
1068 }
1069
1070 pub fn sort_string_matches(
1071 matches: Vec<StringMatch>,
1072 query: Option<&str>,
1073 snippet_sort_order: SnippetSortOrder,
1074 completions: &[Completion],
1075 ) -> Vec<StringMatch> {
1076 let mut matches = matches;
1077
1078 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1079 enum MatchTier<'a> {
1080 WordStartMatch {
1081 sort_exact: Reverse<i32>,
1082 sort_snippet: Reverse<i32>,
1083 sort_score: Reverse<OrderedFloat<f64>>,
1084 sort_positions: Vec<usize>,
1085 sort_text: Option<&'a str>,
1086 sort_kind: usize,
1087 sort_label: &'a str,
1088 },
1089 OtherMatch {
1090 sort_score: Reverse<OrderedFloat<f64>>,
1091 },
1092 }
1093
1094 let query_start_lower = query
1095 .as_ref()
1096 .and_then(|q| q.chars().next())
1097 .and_then(|c| c.to_lowercase().next());
1098
1099 if snippet_sort_order == SnippetSortOrder::None {
1100 matches.retain(|string_match| {
1101 let completion = &completions[string_match.candidate_id];
1102
1103 let is_snippet = matches!(
1104 &completion.source,
1105 CompletionSource::Lsp { lsp_completion, .. }
1106 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1107 );
1108
1109 !is_snippet
1110 });
1111 }
1112
1113 matches.sort_unstable_by_key(|string_match| {
1114 let completion = &completions[string_match.candidate_id];
1115
1116 let is_snippet = matches!(
1117 &completion.source,
1118 CompletionSource::Lsp { lsp_completion, .. }
1119 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1120 );
1121
1122 let sort_text = match &completion.source {
1123 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1124 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1125 _ => None,
1126 };
1127
1128 let (sort_kind, sort_label) = completion.sort_key();
1129
1130 let score = string_match.score;
1131 let sort_score = Reverse(OrderedFloat(score));
1132
1133 let query_start_doesnt_match_split_words = query_start_lower
1134 .map(|query_char| {
1135 !split_words(&string_match.string).any(|word| {
1136 word.chars().next().and_then(|c| c.to_lowercase().next())
1137 == Some(query_char)
1138 })
1139 })
1140 .unwrap_or(false);
1141
1142 if query_start_doesnt_match_split_words {
1143 MatchTier::OtherMatch { sort_score }
1144 } else {
1145 let sort_snippet = match snippet_sort_order {
1146 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1147 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1148 SnippetSortOrder::Inline => Reverse(0),
1149 SnippetSortOrder::None => Reverse(0),
1150 };
1151 let sort_positions = string_match.positions.clone();
1152 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1153 1
1154 } else {
1155 0
1156 });
1157
1158 MatchTier::WordStartMatch {
1159 sort_exact,
1160 sort_snippet,
1161 sort_score,
1162 sort_positions,
1163 sort_text,
1164 sort_kind,
1165 sort_label,
1166 }
1167 }
1168 });
1169
1170 matches
1171 }
1172
1173 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1174 self.markdown_cache = prev_menu.markdown_cache.clone();
1175
1176 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1177 let prev_completions = prev_menu.completions.borrow();
1178 self.markdown_cache
1179 .borrow_mut()
1180 .retain_mut(|(key, _markdown)| match key {
1181 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1182 MarkdownCacheKey::ForCandidate { candidate_id } => {
1183 if let Some(completion) = prev_completions.get(*candidate_id) {
1184 match &completion.documentation {
1185 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1186 *key = MarkdownCacheKey::ForCompletionMatch {
1187 new_text: completion.new_text.clone(),
1188 markdown_source: source.clone(),
1189 };
1190 true
1191 }
1192 _ => false,
1193 }
1194 } else {
1195 false
1196 }
1197 }
1198 });
1199 }
1200
1201 pub fn scroll_aside(
1202 &mut self,
1203 amount: ScrollAmount,
1204 window: &mut Window,
1205 cx: &mut Context<Editor>,
1206 ) {
1207 let mut offset = self.scroll_handle_aside.offset();
1208
1209 offset.y -= amount.pixels(
1210 window.line_height(),
1211 self.scroll_handle_aside.bounds().size.height - px(16.),
1212 ) / 2.0;
1213
1214 cx.notify();
1215 self.scroll_handle_aside.set_offset(offset);
1216 }
1217}
1218
1219#[derive(Clone)]
1220pub struct AvailableCodeAction {
1221 pub excerpt_id: ExcerptId,
1222 pub action: CodeAction,
1223 pub provider: Rc<dyn CodeActionProvider>,
1224}
1225
1226#[derive(Clone)]
1227pub struct CodeActionContents {
1228 tasks: Option<Rc<ResolvedTasks>>,
1229 actions: Option<Rc<[AvailableCodeAction]>>,
1230 debug_scenarios: Vec<DebugScenario>,
1231 pub(crate) context: TaskContext,
1232}
1233
1234impl CodeActionContents {
1235 pub(crate) fn new(
1236 tasks: Option<ResolvedTasks>,
1237 actions: Option<Rc<[AvailableCodeAction]>>,
1238 debug_scenarios: Vec<DebugScenario>,
1239 context: TaskContext,
1240 ) -> Self {
1241 Self {
1242 tasks: tasks.map(Rc::new),
1243 actions,
1244 debug_scenarios,
1245 context,
1246 }
1247 }
1248
1249 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1250 self.tasks.as_deref()
1251 }
1252
1253 fn len(&self) -> usize {
1254 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1255 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1256 tasks_len + code_actions_len + self.debug_scenarios.len()
1257 }
1258
1259 pub fn is_empty(&self) -> bool {
1260 self.len() == 0
1261 }
1262
1263 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1264 self.tasks
1265 .iter()
1266 .flat_map(|tasks| {
1267 tasks
1268 .templates
1269 .iter()
1270 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1271 })
1272 .chain(self.actions.iter().flat_map(|actions| {
1273 actions.iter().map(|available| CodeActionsItem::CodeAction {
1274 excerpt_id: available.excerpt_id,
1275 action: available.action.clone(),
1276 provider: available.provider.clone(),
1277 })
1278 }))
1279 .chain(
1280 self.debug_scenarios
1281 .iter()
1282 .cloned()
1283 .map(CodeActionsItem::DebugScenario),
1284 )
1285 }
1286
1287 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1288 if let Some(tasks) = &self.tasks {
1289 if let Some((kind, task)) = tasks.templates.get(index) {
1290 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1291 } else {
1292 index -= tasks.templates.len();
1293 }
1294 }
1295 if let Some(actions) = &self.actions {
1296 if let Some(available) = actions.get(index) {
1297 return Some(CodeActionsItem::CodeAction {
1298 excerpt_id: available.excerpt_id,
1299 action: available.action.clone(),
1300 provider: available.provider.clone(),
1301 });
1302 } else {
1303 index -= actions.len();
1304 }
1305 }
1306
1307 self.debug_scenarios
1308 .get(index)
1309 .cloned()
1310 .map(CodeActionsItem::DebugScenario)
1311 }
1312}
1313
1314#[derive(Clone)]
1315pub enum CodeActionsItem {
1316 Task(TaskSourceKind, ResolvedTask),
1317 CodeAction {
1318 excerpt_id: ExcerptId,
1319 action: CodeAction,
1320 provider: Rc<dyn CodeActionProvider>,
1321 },
1322 DebugScenario(DebugScenario),
1323}
1324
1325impl CodeActionsItem {
1326 fn as_task(&self) -> Option<&ResolvedTask> {
1327 let Self::Task(_, task) = self else {
1328 return None;
1329 };
1330 Some(task)
1331 }
1332
1333 fn as_code_action(&self) -> Option<&CodeAction> {
1334 let Self::CodeAction { action, .. } = self else {
1335 return None;
1336 };
1337 Some(action)
1338 }
1339 fn as_debug_scenario(&self) -> Option<&DebugScenario> {
1340 let Self::DebugScenario(scenario) = self else {
1341 return None;
1342 };
1343 Some(scenario)
1344 }
1345
1346 pub fn label(&self) -> String {
1347 match self {
1348 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1349 Self::Task(_, task) => task.resolved_label.clone(),
1350 Self::DebugScenario(scenario) => scenario.label.to_string(),
1351 }
1352 }
1353}
1354
1355pub struct CodeActionsMenu {
1356 pub actions: CodeActionContents,
1357 pub buffer: Entity<Buffer>,
1358 pub selected_item: usize,
1359 pub scroll_handle: UniformListScrollHandle,
1360 pub deployed_from: Option<CodeActionSource>,
1361}
1362
1363impl CodeActionsMenu {
1364 fn select_first(&mut self, cx: &mut Context<Editor>) {
1365 self.selected_item = if self.scroll_handle.y_flipped() {
1366 self.actions.len() - 1
1367 } else {
1368 0
1369 };
1370 self.scroll_handle
1371 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1372 cx.notify()
1373 }
1374
1375 fn select_last(&mut self, cx: &mut Context<Editor>) {
1376 self.selected_item = if self.scroll_handle.y_flipped() {
1377 0
1378 } else {
1379 self.actions.len() - 1
1380 };
1381 self.scroll_handle
1382 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1383 cx.notify()
1384 }
1385
1386 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1387 self.selected_item = if self.scroll_handle.y_flipped() {
1388 self.next_match_index()
1389 } else {
1390 self.prev_match_index()
1391 };
1392 self.scroll_handle
1393 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1394 cx.notify();
1395 }
1396
1397 fn select_next(&mut self, cx: &mut Context<Editor>) {
1398 self.selected_item = if self.scroll_handle.y_flipped() {
1399 self.prev_match_index()
1400 } else {
1401 self.next_match_index()
1402 };
1403 self.scroll_handle
1404 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1405 cx.notify();
1406 }
1407
1408 fn prev_match_index(&self) -> usize {
1409 if self.selected_item > 0 {
1410 self.selected_item - 1
1411 } else {
1412 self.actions.len() - 1
1413 }
1414 }
1415
1416 fn next_match_index(&self) -> usize {
1417 if self.selected_item + 1 < self.actions.len() {
1418 self.selected_item + 1
1419 } else {
1420 0
1421 }
1422 }
1423
1424 pub fn visible(&self) -> bool {
1425 !self.actions.is_empty()
1426 }
1427
1428 fn origin(&self) -> ContextMenuOrigin {
1429 match &self.deployed_from {
1430 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1431 ContextMenuOrigin::GutterIndicator(*row)
1432 }
1433 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1434 None => ContextMenuOrigin::Cursor,
1435 }
1436 }
1437
1438 fn render(
1439 &self,
1440 _style: &EditorStyle,
1441 max_height_in_lines: u32,
1442 window: &mut Window,
1443 cx: &mut Context<Editor>,
1444 ) -> AnyElement {
1445 let actions = self.actions.clone();
1446 let selected_item = self.selected_item;
1447 let list = uniform_list(
1448 "code_actions_menu",
1449 self.actions.len(),
1450 cx.processor(move |_this, range: Range<usize>, _, cx| {
1451 actions
1452 .iter()
1453 .skip(range.start)
1454 .take(range.end - range.start)
1455 .enumerate()
1456 .map(|(ix, action)| {
1457 let item_ix = range.start + ix;
1458 let selected = item_ix == selected_item;
1459 let colors = cx.theme().colors();
1460 div().min_w(px(220.)).max_w(px(540.)).child(
1461 ListItem::new(item_ix)
1462 .inset(true)
1463 .toggle_state(selected)
1464 .when_some(action.as_code_action(), |this, action| {
1465 this.child(
1466 h_flex()
1467 .overflow_hidden()
1468 .child(
1469 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1470 action.lsp_action.title().replace("\n", ""),
1471 )
1472 .when(selected, |this| {
1473 this.text_color(colors.text_accent)
1474 }),
1475 )
1476 })
1477 .when_some(action.as_task(), |this, task| {
1478 this.child(
1479 h_flex()
1480 .overflow_hidden()
1481 .child(task.resolved_label.replace("\n", ""))
1482 .when(selected, |this| {
1483 this.text_color(colors.text_accent)
1484 }),
1485 )
1486 })
1487 .when_some(action.as_debug_scenario(), |this, scenario| {
1488 this.child(
1489 h_flex()
1490 .overflow_hidden()
1491 .child("debug: ")
1492 .child(scenario.label.clone())
1493 .when(selected, |this| {
1494 this.text_color(colors.text_accent)
1495 }),
1496 )
1497 })
1498 .on_click(cx.listener(move |editor, _, window, cx| {
1499 cx.stop_propagation();
1500 if let Some(task) = editor.confirm_code_action(
1501 &ConfirmCodeAction {
1502 item_ix: Some(item_ix),
1503 },
1504 window,
1505 cx,
1506 ) {
1507 task.detach_and_log_err(cx)
1508 }
1509 })),
1510 )
1511 })
1512 .collect()
1513 }),
1514 )
1515 .occlude()
1516 .max_h(max_height_in_lines as f32 * window.line_height())
1517 .track_scroll(self.scroll_handle.clone())
1518 .with_width_from_item(
1519 self.actions
1520 .iter()
1521 .enumerate()
1522 .max_by_key(|(_, action)| match action {
1523 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1524 CodeActionsItem::CodeAction { action, .. } => {
1525 action.lsp_action.title().chars().count()
1526 }
1527 CodeActionsItem::DebugScenario(scenario) => {
1528 format!("debug: {}", scenario.label).chars().count()
1529 }
1530 })
1531 .map(|(ix, _)| ix),
1532 )
1533 .with_sizing_behavior(ListSizingBehavior::Infer);
1534
1535 Popover::new().child(list).into_any_element()
1536 }
1537}