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