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