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