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