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