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