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