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