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