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