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_text: Option<&'a str>,
1260 sort_kind: usize,
1261 sort_label: &'a str,
1262 },
1263 OtherMatch {
1264 sort_score: Reverse<OrderedFloat<f64>>,
1265 },
1266 }
1267
1268 let query_start_lower = query
1269 .as_ref()
1270 .and_then(|q| q.chars().next())
1271 .and_then(|c| c.to_lowercase().next());
1272
1273 if snippet_sort_order == SnippetSortOrder::None {
1274 matches
1275 .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
1276 }
1277
1278 matches.sort_unstable_by_key(|string_match| {
1279 let completion = &completions[string_match.candidate_id];
1280
1281 let sort_text = match &completion.source {
1282 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1283 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1284 _ => None,
1285 };
1286
1287 let (sort_kind, sort_label) = completion.sort_key();
1288
1289 let score = string_match.score;
1290 let sort_score = Reverse(OrderedFloat(score));
1291
1292 // Snippets do their own first-letter matching logic elsewhere.
1293 let is_snippet = completion.is_snippet_kind();
1294 let query_start_doesnt_match_split_words = !is_snippet
1295 && query_start_lower
1296 .map(|query_char| {
1297 !split_words(&string_match.string).any(|word| {
1298 word.chars().next().and_then(|c| c.to_lowercase().next())
1299 == Some(query_char)
1300 })
1301 })
1302 .unwrap_or(false);
1303
1304 if query_start_doesnt_match_split_words {
1305 MatchTier::OtherMatch { sort_score }
1306 } else {
1307 let sort_snippet = match snippet_sort_order {
1308 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1309 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1310 SnippetSortOrder::Inline => Reverse(0),
1311 SnippetSortOrder::None => Reverse(0),
1312 };
1313 let sort_positions = string_match.positions.clone();
1314 // This exact matching won't work for multi-word snippets, but it's fine
1315 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1316 1
1317 } else {
1318 0
1319 });
1320
1321 MatchTier::WordStartMatch {
1322 sort_exact,
1323 sort_snippet,
1324 sort_score,
1325 sort_positions,
1326 sort_text,
1327 sort_kind,
1328 sort_label,
1329 }
1330 }
1331 });
1332
1333 matches
1334 }
1335
1336 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1337 self.markdown_cache = prev_menu.markdown_cache.clone();
1338
1339 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1340 let prev_completions = prev_menu.completions.borrow();
1341 self.markdown_cache
1342 .borrow_mut()
1343 .retain_mut(|(key, _markdown)| match key {
1344 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1345 MarkdownCacheKey::ForCandidate { candidate_id } => {
1346 if let Some(completion) = prev_completions.get(*candidate_id) {
1347 match &completion.documentation {
1348 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1349 *key = MarkdownCacheKey::ForCompletionMatch {
1350 new_text: completion.new_text.clone(),
1351 markdown_source: source.clone(),
1352 };
1353 true
1354 }
1355 _ => false,
1356 }
1357 } else {
1358 false
1359 }
1360 }
1361 });
1362 }
1363
1364 pub fn scroll_aside(
1365 &mut self,
1366 amount: ScrollAmount,
1367 window: &mut Window,
1368 cx: &mut Context<Editor>,
1369 ) {
1370 let mut offset = self.scroll_handle_aside.offset();
1371
1372 offset.y -= amount.pixels(
1373 window.line_height(),
1374 self.scroll_handle_aside.bounds().size.height - px(16.),
1375 ) / 2.0;
1376
1377 cx.notify();
1378 self.scroll_handle_aside.set_offset(offset);
1379 }
1380}
1381
1382#[derive(Clone)]
1383pub struct AvailableCodeAction {
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 action: available.action.clone(),
1437 provider: available.provider.clone(),
1438 })
1439 }))
1440 .chain(
1441 self.debug_scenarios
1442 .iter()
1443 .cloned()
1444 .map(CodeActionsItem::DebugScenario),
1445 )
1446 }
1447
1448 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1449 if let Some(tasks) = &self.tasks {
1450 if let Some((kind, task)) = tasks.templates.get(index) {
1451 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1452 } else {
1453 index -= tasks.templates.len();
1454 }
1455 }
1456 if let Some(actions) = &self.actions {
1457 if let Some(available) = actions.get(index) {
1458 return Some(CodeActionsItem::CodeAction {
1459 action: available.action.clone(),
1460 provider: available.provider.clone(),
1461 });
1462 } else {
1463 index -= actions.len();
1464 }
1465 }
1466
1467 self.debug_scenarios
1468 .get(index)
1469 .cloned()
1470 .map(CodeActionsItem::DebugScenario)
1471 }
1472}
1473
1474#[derive(Clone)]
1475pub enum CodeActionsItem {
1476 Task(TaskSourceKind, ResolvedTask),
1477 CodeAction {
1478 action: CodeAction,
1479 provider: Rc<dyn CodeActionProvider>,
1480 },
1481 DebugScenario(DebugScenario),
1482}
1483
1484impl CodeActionsItem {
1485 pub fn label(&self) -> String {
1486 match self {
1487 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1488 Self::Task(_, task) => task.resolved_label.clone(),
1489 Self::DebugScenario(scenario) => scenario.label.to_string(),
1490 }
1491 }
1492
1493 pub fn menu_label(&self) -> String {
1494 match self {
1495 Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1496 Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1497 Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1498 }
1499 }
1500}
1501
1502pub struct CodeActionsMenu {
1503 pub actions: CodeActionContents,
1504 pub buffer: Entity<Buffer>,
1505 pub selected_item: usize,
1506 pub scroll_handle: UniformListScrollHandle,
1507 pub deployed_from: Option<CodeActionSource>,
1508}
1509
1510impl CodeActionsMenu {
1511 fn select_first(&mut self, cx: &mut Context<Editor>) {
1512 self.selected_item = if self.scroll_handle.y_flipped() {
1513 self.actions.len() - 1
1514 } else {
1515 0
1516 };
1517 self.scroll_handle
1518 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1519 cx.notify()
1520 }
1521
1522 fn select_last(&mut self, cx: &mut Context<Editor>) {
1523 self.selected_item = if self.scroll_handle.y_flipped() {
1524 0
1525 } else {
1526 self.actions.len() - 1
1527 };
1528 self.scroll_handle
1529 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1530 cx.notify()
1531 }
1532
1533 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1534 self.selected_item = if self.scroll_handle.y_flipped() {
1535 self.next_match_index()
1536 } else {
1537 self.prev_match_index()
1538 };
1539 self.scroll_handle
1540 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1541 cx.notify();
1542 }
1543
1544 fn select_next(&mut self, cx: &mut Context<Editor>) {
1545 self.selected_item = if self.scroll_handle.y_flipped() {
1546 self.prev_match_index()
1547 } else {
1548 self.next_match_index()
1549 };
1550 self.scroll_handle
1551 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1552 cx.notify();
1553 }
1554
1555 fn prev_match_index(&self) -> usize {
1556 if self.selected_item > 0 {
1557 self.selected_item - 1
1558 } else {
1559 self.actions.len() - 1
1560 }
1561 }
1562
1563 fn next_match_index(&self) -> usize {
1564 if self.selected_item + 1 < self.actions.len() {
1565 self.selected_item + 1
1566 } else {
1567 0
1568 }
1569 }
1570
1571 pub fn visible(&self) -> bool {
1572 !self.actions.is_empty()
1573 }
1574
1575 fn origin(&self) -> ContextMenuOrigin {
1576 match &self.deployed_from {
1577 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1578 ContextMenuOrigin::GutterIndicator(*row)
1579 }
1580 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1581 None => ContextMenuOrigin::Cursor,
1582 }
1583 }
1584
1585 fn render(
1586 &self,
1587 _style: &EditorStyle,
1588 max_height_in_lines: u32,
1589 window: &mut Window,
1590 cx: &mut Context<Editor>,
1591 ) -> AnyElement {
1592 let actions = self.actions.clone();
1593 let selected_item = self.selected_item;
1594 let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1595
1596 let list = uniform_list(
1597 "code_actions_menu",
1598 self.actions.len(),
1599 cx.processor(move |_this, range: Range<usize>, _, cx| {
1600 actions
1601 .iter()
1602 .skip(range.start)
1603 .take(range.end - range.start)
1604 .enumerate()
1605 .map(|(ix, action)| {
1606 let item_ix = range.start + ix;
1607 let selected = item_ix == selected_item;
1608 let colors = cx.theme().colors();
1609
1610 ListItem::new(item_ix)
1611 .inset(true)
1612 .toggle_state(selected)
1613 .overflow_x()
1614 .child(
1615 div()
1616 .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1617 .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1618 .overflow_hidden()
1619 .text_ellipsis()
1620 .when(is_quick_action_bar, |this| this.text_ui(cx))
1621 .when(selected, |this| this.text_color(colors.text_accent))
1622 .child(action.menu_label()),
1623 )
1624 .on_click(cx.listener(move |editor, _, window, cx| {
1625 cx.stop_propagation();
1626 if let Some(task) = editor.confirm_code_action(
1627 &ConfirmCodeAction {
1628 item_ix: Some(item_ix),
1629 },
1630 window,
1631 cx,
1632 ) {
1633 task.detach_and_log_err(cx)
1634 }
1635 }))
1636 })
1637 .collect()
1638 }),
1639 )
1640 .occlude()
1641 .max_h(max_height_in_lines as f32 * window.line_height())
1642 .track_scroll(&self.scroll_handle)
1643 .with_width_from_item(
1644 self.actions
1645 .iter()
1646 .enumerate()
1647 .max_by_key(|(_, action)| match action {
1648 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1649 CodeActionsItem::CodeAction { action, .. } => {
1650 action.lsp_action.title().chars().count()
1651 }
1652 CodeActionsItem::DebugScenario(scenario) => {
1653 format!("debug: {}", scenario.label).chars().count()
1654 }
1655 })
1656 .map(|(ix, _)| ix),
1657 )
1658 .with_sizing_behavior(ListSizingBehavior::Infer);
1659
1660 Popover::new().child(list).into_any_element()
1661 }
1662
1663 fn render_aside(
1664 &mut self,
1665 max_size: Size<Pixels>,
1666 window: &mut Window,
1667 _cx: &mut Context<Editor>,
1668 ) -> Option<AnyElement> {
1669 let Some(action) = self.actions.get(self.selected_item) else {
1670 return None;
1671 };
1672
1673 let label = action.menu_label();
1674 let text_system = window.text_system();
1675 let mut line_wrapper = text_system.line_wrapper(
1676 window.text_style().font(),
1677 window.text_style().font_size.to_pixels(window.rem_size()),
1678 );
1679 let is_truncated = line_wrapper.should_truncate_line(
1680 &label,
1681 CODE_ACTION_MENU_MAX_WIDTH,
1682 "…",
1683 gpui::TruncateFrom::End,
1684 );
1685
1686 if is_truncated.is_none() {
1687 return None;
1688 }
1689
1690 Some(
1691 Popover::new()
1692 .child(
1693 div()
1694 .child(label)
1695 .id("code_actions_menu_extended")
1696 .px(MENU_ASIDE_X_PADDING / 2.)
1697 .max_w(max_size.width)
1698 .max_h(max_size.height)
1699 .occlude(),
1700 )
1701 .into_any_element(),
1702 )
1703 }
1704}