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