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