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