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::lsp_store::CompletionDocumentation;
15use project::{CodeAction, Completion, TaskSourceKind};
16use project::{CompletionDisplayOptions, CompletionSource};
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::hover_popover::{hover_markdown_style, open_markdown_url};
36use crate::{
37 CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
38 EditorStyle, ResolvedTasks,
39 actions::{ConfirmCodeAction, ConfirmCompletion},
40 split_words, styled_runs_for_code_label,
41};
42use settings::SnippetSortOrder;
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 const 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 display_options: CompletionDisplayOptions,
236 snippet_sort_order: SnippetSortOrder,
237}
238
239#[derive(Clone, Debug, PartialEq)]
240enum MarkdownCacheKey {
241 ForCandidate {
242 candidate_id: usize,
243 },
244 ForCompletionMatch {
245 new_text: String,
246 markdown_source: SharedString,
247 },
248}
249
250#[derive(Copy, Clone, Debug, PartialEq, Eq)]
251pub enum CompletionsMenuSource {
252 Normal,
253 SnippetChoices,
254 Words { ignore_threshold: bool },
255}
256
257// TODO: There should really be a wrapper around fuzzy match tasks that does this.
258impl Drop for CompletionsMenu {
259 fn drop(&mut self) {
260 self.cancel_filter.store(true, Ordering::Relaxed);
261 }
262}
263
264impl CompletionsMenu {
265 pub fn new(
266 id: CompletionId,
267 source: CompletionsMenuSource,
268 sort_completions: bool,
269 show_completion_documentation: bool,
270 initial_position: Anchor,
271 initial_query: Option<Arc<String>>,
272 is_incomplete: bool,
273 buffer: Entity<Buffer>,
274 completions: Box<[Completion]>,
275 display_options: CompletionDisplayOptions,
276 snippet_sort_order: SnippetSortOrder,
277 language_registry: Option<Arc<LanguageRegistry>>,
278 language: Option<LanguageName>,
279 cx: &mut Context<Editor>,
280 ) -> Self {
281 let match_candidates = completions
282 .iter()
283 .enumerate()
284 .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
285 .collect();
286
287 let completions_menu = Self {
288 id,
289 source,
290 sort_completions,
291 initial_position,
292 initial_query,
293 is_incomplete,
294 buffer,
295 show_completion_documentation,
296 completions: RefCell::new(completions).into(),
297 match_candidates,
298 entries: Rc::new(RefCell::new(Box::new([]))),
299 selected_item: 0,
300 filter_task: Task::ready(()),
301 cancel_filter: Arc::new(AtomicBool::new(false)),
302 scroll_handle: UniformListScrollHandle::new(),
303 scroll_handle_aside: ScrollHandle::new(),
304 resolve_completions: true,
305 last_rendered_range: RefCell::new(None).into(),
306 markdown_cache: RefCell::new(VecDeque::new()).into(),
307 language_registry,
308 language,
309 display_options,
310 snippet_sort_order,
311 };
312
313 completions_menu.start_markdown_parse_for_nearby_entries(cx);
314
315 completions_menu
316 }
317
318 pub fn new_snippet_choices(
319 id: CompletionId,
320 sort_completions: bool,
321 choices: &Vec<String>,
322 selection: Range<Anchor>,
323 buffer: Entity<Buffer>,
324 snippet_sort_order: SnippetSortOrder,
325 ) -> Self {
326 let completions = choices
327 .iter()
328 .map(|choice| Completion {
329 replace_range: selection.start.text_anchor..selection.end.text_anchor,
330 new_text: choice.to_string(),
331 label: CodeLabel::plain(choice.to_string(), None),
332 icon_path: None,
333 documentation: None,
334 confirm: None,
335 insert_text_mode: None,
336 source: CompletionSource::Custom,
337 })
338 .collect();
339
340 let match_candidates = choices
341 .iter()
342 .enumerate()
343 .map(|(id, completion)| StringMatchCandidate::new(id, completion))
344 .collect();
345 let entries = choices
346 .iter()
347 .enumerate()
348 .map(|(id, completion)| StringMatch {
349 candidate_id: id,
350 score: 1.,
351 positions: vec![],
352 string: completion.clone(),
353 })
354 .collect();
355 Self {
356 id,
357 source: CompletionsMenuSource::SnippetChoices,
358 sort_completions,
359 initial_position: selection.start,
360 initial_query: None,
361 is_incomplete: false,
362 buffer,
363 completions: RefCell::new(completions).into(),
364 match_candidates,
365 entries: RefCell::new(entries).into(),
366 selected_item: 0,
367 filter_task: Task::ready(()),
368 cancel_filter: Arc::new(AtomicBool::new(false)),
369 scroll_handle: UniformListScrollHandle::new(),
370 scroll_handle_aside: ScrollHandle::new(),
371 resolve_completions: false,
372 show_completion_documentation: false,
373 last_rendered_range: RefCell::new(None).into(),
374 markdown_cache: RefCell::new(VecDeque::new()).into(),
375 language_registry: None,
376 language: None,
377 display_options: CompletionDisplayOptions::default(),
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 const 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 widest_completion_ix = if self.display_options.dynamic_width {
741 let completions = self.completions.borrow();
742 let widest_completion_ix = self
743 .entries
744 .borrow()
745 .iter()
746 .enumerate()
747 .max_by_key(|(_, mat)| {
748 let completion = &completions[mat.candidate_id];
749 let documentation = &completion.documentation;
750
751 let mut len = completion.label.text.chars().count();
752 if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
753 if show_completion_documentation {
754 len += text.chars().count();
755 }
756 }
757
758 len
759 })
760 .map(|(ix, _)| ix);
761 drop(completions);
762 widest_completion_ix
763 } else {
764 None
765 };
766
767 let selected_item = self.selected_item;
768 let completions = self.completions.clone();
769 let entries = self.entries.clone();
770 let last_rendered_range = self.last_rendered_range.clone();
771 let style = style.clone();
772 let list = uniform_list(
773 "completions",
774 self.entries.borrow().len(),
775 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
776 last_rendered_range.borrow_mut().replace(range.clone());
777 let start_ix = range.start;
778 let completions_guard = completions.borrow_mut();
779
780 entries.borrow()[range]
781 .iter()
782 .enumerate()
783 .map(|(ix, mat)| {
784 let item_ix = start_ix + ix;
785 let completion = &completions_guard[mat.candidate_id];
786 let documentation = if show_completion_documentation {
787 &completion.documentation
788 } else {
789 &None
790 };
791
792 let filter_start = completion.label.filter_range.start;
793 let highlights = gpui::combine_highlights(
794 mat.ranges().map(|range| {
795 (
796 filter_start + range.start..filter_start + range.end,
797 FontWeight::BOLD.into(),
798 )
799 }),
800 styled_runs_for_code_label(&completion.label, &style.syntax).map(
801 |(range, mut highlight)| {
802 // Ignore font weight for syntax highlighting, as we'll use it
803 // for fuzzy matches.
804 highlight.font_weight = None;
805 if completion
806 .source
807 .lsp_completion(false)
808 .and_then(|lsp_completion| lsp_completion.deprecated)
809 .unwrap_or(false)
810 {
811 highlight.strikethrough = Some(StrikethroughStyle {
812 thickness: 1.0.into(),
813 ..Default::default()
814 });
815 highlight.color = Some(cx.theme().colors().text_muted);
816 }
817
818 (range, highlight)
819 },
820 ),
821 );
822
823 let completion_label = StyledText::new(completion.label.text.clone())
824 .with_default_highlights(&style.text, highlights);
825
826 let documentation_label = match documentation {
827 Some(CompletionDocumentation::SingleLine(text))
828 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
829 single_line: text,
830 ..
831 }) => {
832 if text.trim().is_empty() {
833 None
834 } else {
835 Some(
836 Label::new(text.clone())
837 .ml_4()
838 .size(LabelSize::Small)
839 .color(Color::Muted),
840 )
841 }
842 }
843 _ => None,
844 };
845
846 let start_slot = completion
847 .color()
848 .map(|color| {
849 div()
850 .flex_shrink_0()
851 .size_3p5()
852 .rounded_xs()
853 .bg(color)
854 .into_any_element()
855 })
856 .or_else(|| {
857 completion.icon_path.as_ref().map(|path| {
858 Icon::from_path(path)
859 .size(IconSize::XSmall)
860 .color(Color::Muted)
861 .into_any_element()
862 })
863 });
864
865 div().min_w(px(280.)).max_w(px(540.)).child(
866 ListItem::new(mat.candidate_id)
867 .inset(true)
868 .toggle_state(item_ix == selected_item)
869 .on_click(cx.listener(move |editor, _event, window, cx| {
870 cx.stop_propagation();
871 if let Some(task) = editor.confirm_completion(
872 &ConfirmCompletion {
873 item_ix: Some(item_ix),
874 },
875 window,
876 cx,
877 ) {
878 task.detach_and_log_err(cx)
879 }
880 }))
881 .start_slot::<AnyElement>(start_slot)
882 .child(h_flex().overflow_hidden().child(completion_label))
883 .end_slot::<Label>(documentation_label),
884 )
885 })
886 .collect()
887 }),
888 )
889 .occlude()
890 .max_h(max_height_in_lines as f32 * window.line_height())
891 .track_scroll(self.scroll_handle.clone())
892 .with_sizing_behavior(ListSizingBehavior::Infer)
893 .map(|this| {
894 if self.display_options.dynamic_width {
895 this.with_width_from_item(widest_completion_ix)
896 } else {
897 this.w(rems(34.))
898 }
899 });
900
901 Popover::new().child(list).into_any_element()
902 }
903
904 fn render_aside(
905 &mut self,
906 max_size: Size<Pixels>,
907 window: &mut Window,
908 cx: &mut Context<Editor>,
909 ) -> Option<AnyElement> {
910 if !self.show_completion_documentation {
911 return None;
912 }
913
914 let mat = &self.entries.borrow()[self.selected_item];
915 let completions = self.completions.borrow_mut();
916 let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
917 Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
918 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
919 plain_text: Some(text),
920 ..
921 }) => div().child(text.clone()),
922 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
923 let Some((false, markdown)) = self.get_or_create_markdown(
924 mat.candidate_id,
925 Some(source),
926 true,
927 &completions,
928 cx,
929 ) else {
930 return None;
931 };
932 Self::render_markdown(markdown, window, cx)
933 }
934 None => {
935 // Handle the case where documentation hasn't yet been resolved but there's a
936 // `new_text` match in the cache.
937 //
938 // TODO: It's inconsistent that documentation caching based on matching `new_text`
939 // only works for markdown. Consider generally caching the results of resolving
940 // completions.
941 let Some((false, markdown)) =
942 self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
943 else {
944 return None;
945 };
946 Self::render_markdown(markdown, window, cx)
947 }
948 Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
949 Some(CompletionDocumentation::SingleLine(_)) => return None,
950 Some(CompletionDocumentation::Undocumented) => return None,
951 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
952 plain_text: None,
953 ..
954 }) => {
955 return None;
956 }
957 };
958
959 Some(
960 Popover::new()
961 .child(
962 multiline_docs
963 .id("multiline_docs")
964 .px(MENU_ASIDE_X_PADDING / 2.)
965 .max_w(max_size.width)
966 .max_h(max_size.height)
967 .overflow_y_scroll()
968 .track_scroll(&self.scroll_handle_aside)
969 .occlude(),
970 )
971 .into_any_element(),
972 )
973 }
974
975 fn render_markdown(
976 markdown: Entity<Markdown>,
977 window: &mut Window,
978 cx: &mut Context<Editor>,
979 ) -> Div {
980 div().child(
981 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
982 .code_block_renderer(markdown::CodeBlockRenderer::Default {
983 copy_button: false,
984 copy_button_on_hover: false,
985 border: false,
986 })
987 .on_url_click(open_markdown_url),
988 )
989 }
990
991 pub fn filter(
992 &mut self,
993 query: Option<Arc<String>>,
994 provider: Option<Rc<dyn CompletionProvider>>,
995 window: &mut Window,
996 cx: &mut Context<Editor>,
997 ) {
998 self.cancel_filter.store(true, Ordering::Relaxed);
999 if let Some(query) = query {
1000 self.cancel_filter = Arc::new(AtomicBool::new(false));
1001 let matches = self.do_async_filtering(query, cx);
1002 let id = self.id;
1003 self.filter_task = cx.spawn_in(window, async move |editor, cx| {
1004 let matches = matches.await;
1005 editor
1006 .update_in(cx, |editor, window, cx| {
1007 editor.with_completions_menu_matching_id(id, |this| {
1008 if let Some(this) = this {
1009 this.set_filter_results(matches, provider, window, cx);
1010 }
1011 });
1012 })
1013 .ok();
1014 });
1015 } else {
1016 self.filter_task = Task::ready(());
1017 let matches = self.unfiltered_matches();
1018 self.set_filter_results(matches, provider, window, cx);
1019 }
1020 }
1021
1022 pub fn do_async_filtering(
1023 &self,
1024 query: Arc<String>,
1025 cx: &Context<Editor>,
1026 ) -> Task<Vec<StringMatch>> {
1027 let matches_task = cx.background_spawn({
1028 let query = query.clone();
1029 let match_candidates = self.match_candidates.clone();
1030 let cancel_filter = self.cancel_filter.clone();
1031 let background_executor = cx.background_executor().clone();
1032 async move {
1033 fuzzy::match_strings(
1034 &match_candidates,
1035 &query,
1036 query.chars().any(|c| c.is_uppercase()),
1037 false,
1038 1000,
1039 &cancel_filter,
1040 background_executor,
1041 )
1042 .await
1043 }
1044 });
1045
1046 let completions = self.completions.clone();
1047 let sort_completions = self.sort_completions;
1048 let snippet_sort_order = self.snippet_sort_order;
1049 cx.foreground_executor().spawn(async move {
1050 let mut matches = matches_task.await;
1051
1052 if sort_completions {
1053 matches = Self::sort_string_matches(
1054 matches,
1055 Some(&query),
1056 snippet_sort_order,
1057 completions.borrow().as_ref(),
1058 );
1059 }
1060
1061 matches
1062 })
1063 }
1064
1065 /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
1066 pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
1067 let mut matches = self
1068 .match_candidates
1069 .iter()
1070 .enumerate()
1071 .map(|(candidate_id, candidate)| StringMatch {
1072 candidate_id,
1073 score: Default::default(),
1074 positions: Default::default(),
1075 string: candidate.string.clone(),
1076 })
1077 .collect();
1078
1079 if self.sort_completions {
1080 matches = Self::sort_string_matches(
1081 matches,
1082 None,
1083 self.snippet_sort_order,
1084 self.completions.borrow().as_ref(),
1085 );
1086 }
1087
1088 matches
1089 }
1090
1091 pub fn set_filter_results(
1092 &mut self,
1093 matches: Vec<StringMatch>,
1094 provider: Option<Rc<dyn CompletionProvider>>,
1095 window: &mut Window,
1096 cx: &mut Context<Editor>,
1097 ) {
1098 *self.entries.borrow_mut() = matches.into_boxed_slice();
1099 self.selected_item = 0;
1100 self.handle_selection_changed(provider.as_deref(), window, cx);
1101 }
1102
1103 pub fn sort_string_matches(
1104 matches: Vec<StringMatch>,
1105 query: Option<&str>,
1106 snippet_sort_order: SnippetSortOrder,
1107 completions: &[Completion],
1108 ) -> Vec<StringMatch> {
1109 let mut matches = matches;
1110
1111 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1112 enum MatchTier<'a> {
1113 WordStartMatch {
1114 sort_exact: Reverse<i32>,
1115 sort_snippet: Reverse<i32>,
1116 sort_score: Reverse<OrderedFloat<f64>>,
1117 sort_positions: Vec<usize>,
1118 sort_text: Option<&'a str>,
1119 sort_kind: usize,
1120 sort_label: &'a str,
1121 },
1122 OtherMatch {
1123 sort_score: Reverse<OrderedFloat<f64>>,
1124 },
1125 }
1126
1127 let query_start_lower = query
1128 .as_ref()
1129 .and_then(|q| q.chars().next())
1130 .and_then(|c| c.to_lowercase().next());
1131
1132 if snippet_sort_order == SnippetSortOrder::None {
1133 matches.retain(|string_match| {
1134 let completion = &completions[string_match.candidate_id];
1135
1136 let is_snippet = matches!(
1137 &completion.source,
1138 CompletionSource::Lsp { lsp_completion, .. }
1139 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1140 );
1141
1142 !is_snippet
1143 });
1144 }
1145
1146 matches.sort_unstable_by_key(|string_match| {
1147 let completion = &completions[string_match.candidate_id];
1148
1149 let is_snippet = matches!(
1150 &completion.source,
1151 CompletionSource::Lsp { lsp_completion, .. }
1152 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1153 );
1154
1155 let sort_text = match &completion.source {
1156 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1157 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1158 _ => None,
1159 };
1160
1161 let (sort_kind, sort_label) = completion.sort_key();
1162
1163 let score = string_match.score;
1164 let sort_score = Reverse(OrderedFloat(score));
1165
1166 let query_start_doesnt_match_split_words = query_start_lower
1167 .map(|query_char| {
1168 !split_words(&string_match.string).any(|word| {
1169 word.chars().next().and_then(|c| c.to_lowercase().next())
1170 == Some(query_char)
1171 })
1172 })
1173 .unwrap_or(false);
1174
1175 if query_start_doesnt_match_split_words {
1176 MatchTier::OtherMatch { sort_score }
1177 } else {
1178 let sort_snippet = match snippet_sort_order {
1179 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1180 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1181 SnippetSortOrder::Inline => Reverse(0),
1182 SnippetSortOrder::None => Reverse(0),
1183 };
1184 let sort_positions = string_match.positions.clone();
1185 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1186 1
1187 } else {
1188 0
1189 });
1190
1191 MatchTier::WordStartMatch {
1192 sort_exact,
1193 sort_snippet,
1194 sort_score,
1195 sort_positions,
1196 sort_text,
1197 sort_kind,
1198 sort_label,
1199 }
1200 }
1201 });
1202
1203 matches
1204 }
1205
1206 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1207 self.markdown_cache = prev_menu.markdown_cache.clone();
1208
1209 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1210 let prev_completions = prev_menu.completions.borrow();
1211 self.markdown_cache
1212 .borrow_mut()
1213 .retain_mut(|(key, _markdown)| match key {
1214 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1215 MarkdownCacheKey::ForCandidate { candidate_id } => {
1216 if let Some(completion) = prev_completions.get(*candidate_id) {
1217 match &completion.documentation {
1218 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1219 *key = MarkdownCacheKey::ForCompletionMatch {
1220 new_text: completion.new_text.clone(),
1221 markdown_source: source.clone(),
1222 };
1223 true
1224 }
1225 _ => false,
1226 }
1227 } else {
1228 false
1229 }
1230 }
1231 });
1232 }
1233
1234 pub fn scroll_aside(
1235 &mut self,
1236 amount: ScrollAmount,
1237 window: &mut Window,
1238 cx: &mut Context<Editor>,
1239 ) {
1240 let mut offset = self.scroll_handle_aside.offset();
1241
1242 offset.y -= amount.pixels(
1243 window.line_height(),
1244 self.scroll_handle_aside.bounds().size.height - px(16.),
1245 ) / 2.0;
1246
1247 cx.notify();
1248 self.scroll_handle_aside.set_offset(offset);
1249 }
1250}
1251
1252#[derive(Clone)]
1253pub struct AvailableCodeAction {
1254 pub excerpt_id: ExcerptId,
1255 pub action: CodeAction,
1256 pub provider: Rc<dyn CodeActionProvider>,
1257}
1258
1259#[derive(Clone)]
1260pub struct CodeActionContents {
1261 tasks: Option<Rc<ResolvedTasks>>,
1262 actions: Option<Rc<[AvailableCodeAction]>>,
1263 debug_scenarios: Vec<DebugScenario>,
1264 pub(crate) context: TaskContext,
1265}
1266
1267impl CodeActionContents {
1268 pub(crate) fn new(
1269 tasks: Option<ResolvedTasks>,
1270 actions: Option<Rc<[AvailableCodeAction]>>,
1271 debug_scenarios: Vec<DebugScenario>,
1272 context: TaskContext,
1273 ) -> Self {
1274 Self {
1275 tasks: tasks.map(Rc::new),
1276 actions,
1277 debug_scenarios,
1278 context,
1279 }
1280 }
1281
1282 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1283 self.tasks.as_deref()
1284 }
1285
1286 fn len(&self) -> usize {
1287 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1288 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1289 tasks_len + code_actions_len + self.debug_scenarios.len()
1290 }
1291
1292 pub fn is_empty(&self) -> bool {
1293 self.len() == 0
1294 }
1295
1296 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1297 self.tasks
1298 .iter()
1299 .flat_map(|tasks| {
1300 tasks
1301 .templates
1302 .iter()
1303 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1304 })
1305 .chain(self.actions.iter().flat_map(|actions| {
1306 actions.iter().map(|available| CodeActionsItem::CodeAction {
1307 excerpt_id: available.excerpt_id,
1308 action: available.action.clone(),
1309 provider: available.provider.clone(),
1310 })
1311 }))
1312 .chain(
1313 self.debug_scenarios
1314 .iter()
1315 .cloned()
1316 .map(CodeActionsItem::DebugScenario),
1317 )
1318 }
1319
1320 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1321 if let Some(tasks) = &self.tasks {
1322 if let Some((kind, task)) = tasks.templates.get(index) {
1323 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1324 } else {
1325 index -= tasks.templates.len();
1326 }
1327 }
1328 if let Some(actions) = &self.actions {
1329 if let Some(available) = actions.get(index) {
1330 return Some(CodeActionsItem::CodeAction {
1331 excerpt_id: available.excerpt_id,
1332 action: available.action.clone(),
1333 provider: available.provider.clone(),
1334 });
1335 } else {
1336 index -= actions.len();
1337 }
1338 }
1339
1340 self.debug_scenarios
1341 .get(index)
1342 .cloned()
1343 .map(CodeActionsItem::DebugScenario)
1344 }
1345}
1346
1347#[derive(Clone)]
1348pub enum CodeActionsItem {
1349 Task(TaskSourceKind, ResolvedTask),
1350 CodeAction {
1351 excerpt_id: ExcerptId,
1352 action: CodeAction,
1353 provider: Rc<dyn CodeActionProvider>,
1354 },
1355 DebugScenario(DebugScenario),
1356}
1357
1358impl CodeActionsItem {
1359 const fn as_task(&self) -> Option<&ResolvedTask> {
1360 let Self::Task(_, task) = self else {
1361 return None;
1362 };
1363 Some(task)
1364 }
1365
1366 const fn as_code_action(&self) -> Option<&CodeAction> {
1367 let Self::CodeAction { action, .. } = self else {
1368 return None;
1369 };
1370 Some(action)
1371 }
1372 const fn as_debug_scenario(&self) -> Option<&DebugScenario> {
1373 let Self::DebugScenario(scenario) = self else {
1374 return None;
1375 };
1376 Some(scenario)
1377 }
1378
1379 pub fn label(&self) -> String {
1380 match self {
1381 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1382 Self::Task(_, task) => task.resolved_label.clone(),
1383 Self::DebugScenario(scenario) => scenario.label.to_string(),
1384 }
1385 }
1386}
1387
1388pub struct CodeActionsMenu {
1389 pub actions: CodeActionContents,
1390 pub buffer: Entity<Buffer>,
1391 pub selected_item: usize,
1392 pub scroll_handle: UniformListScrollHandle,
1393 pub deployed_from: Option<CodeActionSource>,
1394}
1395
1396impl CodeActionsMenu {
1397 fn select_first(&mut self, cx: &mut Context<Editor>) {
1398 self.selected_item = if self.scroll_handle.y_flipped() {
1399 self.actions.len() - 1
1400 } else {
1401 0
1402 };
1403 self.scroll_handle
1404 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1405 cx.notify()
1406 }
1407
1408 fn select_last(&mut self, cx: &mut Context<Editor>) {
1409 self.selected_item = if self.scroll_handle.y_flipped() {
1410 0
1411 } else {
1412 self.actions.len() - 1
1413 };
1414 self.scroll_handle
1415 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1416 cx.notify()
1417 }
1418
1419 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1420 self.selected_item = if self.scroll_handle.y_flipped() {
1421 self.next_match_index()
1422 } else {
1423 self.prev_match_index()
1424 };
1425 self.scroll_handle
1426 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1427 cx.notify();
1428 }
1429
1430 fn select_next(&mut self, cx: &mut Context<Editor>) {
1431 self.selected_item = if self.scroll_handle.y_flipped() {
1432 self.prev_match_index()
1433 } else {
1434 self.next_match_index()
1435 };
1436 self.scroll_handle
1437 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1438 cx.notify();
1439 }
1440
1441 fn prev_match_index(&self) -> usize {
1442 if self.selected_item > 0 {
1443 self.selected_item - 1
1444 } else {
1445 self.actions.len() - 1
1446 }
1447 }
1448
1449 fn next_match_index(&self) -> usize {
1450 if self.selected_item + 1 < self.actions.len() {
1451 self.selected_item + 1
1452 } else {
1453 0
1454 }
1455 }
1456
1457 pub fn visible(&self) -> bool {
1458 !self.actions.is_empty()
1459 }
1460
1461 const fn origin(&self) -> ContextMenuOrigin {
1462 match &self.deployed_from {
1463 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1464 ContextMenuOrigin::GutterIndicator(*row)
1465 }
1466 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1467 None => ContextMenuOrigin::Cursor,
1468 }
1469 }
1470
1471 fn render(
1472 &self,
1473 _style: &EditorStyle,
1474 max_height_in_lines: u32,
1475 window: &mut Window,
1476 cx: &mut Context<Editor>,
1477 ) -> AnyElement {
1478 let actions = self.actions.clone();
1479 let selected_item = self.selected_item;
1480 let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1481
1482 let list = uniform_list(
1483 "code_actions_menu",
1484 self.actions.len(),
1485 cx.processor(move |_this, range: Range<usize>, _, cx| {
1486 actions
1487 .iter()
1488 .skip(range.start)
1489 .take(range.end - range.start)
1490 .enumerate()
1491 .map(|(ix, action)| {
1492 let item_ix = range.start + ix;
1493 let selected = item_ix == selected_item;
1494 let colors = cx.theme().colors();
1495 div().min_w(px(220.)).max_w(px(540.)).child(
1496 ListItem::new(item_ix)
1497 .inset(true)
1498 .toggle_state(selected)
1499 .when_some(action.as_code_action(), |this, action| {
1500 this.child(
1501 h_flex()
1502 .overflow_hidden()
1503 .when(is_quick_action_bar, |this| this.text_ui(cx))
1504 .child(
1505 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1506 action.lsp_action.title().replace("\n", ""),
1507 )
1508 .when(selected, |this| {
1509 this.text_color(colors.text_accent)
1510 }),
1511 )
1512 })
1513 .when_some(action.as_task(), |this, task| {
1514 this.child(
1515 h_flex()
1516 .overflow_hidden()
1517 .when(is_quick_action_bar, |this| this.text_ui(cx))
1518 .child(task.resolved_label.replace("\n", ""))
1519 .when(selected, |this| {
1520 this.text_color(colors.text_accent)
1521 }),
1522 )
1523 })
1524 .when_some(action.as_debug_scenario(), |this, scenario| {
1525 this.child(
1526 h_flex()
1527 .overflow_hidden()
1528 .when(is_quick_action_bar, |this| this.text_ui(cx))
1529 .child("debug: ")
1530 .child(scenario.label.clone())
1531 .when(selected, |this| {
1532 this.text_color(colors.text_accent)
1533 }),
1534 )
1535 })
1536 .on_click(cx.listener(move |editor, _, window, cx| {
1537 cx.stop_propagation();
1538 if let Some(task) = editor.confirm_code_action(
1539 &ConfirmCodeAction {
1540 item_ix: Some(item_ix),
1541 },
1542 window,
1543 cx,
1544 ) {
1545 task.detach_and_log_err(cx)
1546 }
1547 })),
1548 )
1549 })
1550 .collect()
1551 }),
1552 )
1553 .occlude()
1554 .max_h(max_height_in_lines as f32 * window.line_height())
1555 .track_scroll(self.scroll_handle.clone())
1556 .with_width_from_item(
1557 self.actions
1558 .iter()
1559 .enumerate()
1560 .max_by_key(|(_, action)| match action {
1561 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1562 CodeActionsItem::CodeAction { action, .. } => {
1563 action.lsp_action.title().chars().count()
1564 }
1565 CodeActionsItem::DebugScenario(scenario) => {
1566 format!("debug: {}", scenario.label).chars().count()
1567 }
1568 })
1569 .map(|(ix, _)| ix),
1570 )
1571 .with_sizing_behavior(ListSizingBehavior::Infer);
1572
1573 Popover::new().child(list).into_any_element()
1574 }
1575}