1use crate::scroll::ScrollAmount;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{
4 AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
5 SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
6 uniform_list,
7};
8use itertools::Itertools;
9use language::CodeLabel;
10use language::{Buffer, LanguageName, LanguageRegistry};
11use lsp::CompletionItemTag;
12use markdown::{Markdown, MarkdownElement};
13use multi_buffer::{Anchor, ExcerptId};
14use ordered_float::OrderedFloat;
15use project::lsp_store::CompletionDocumentation;
16use project::{CodeAction, Completion, TaskSourceKind};
17use project::{CompletionDisplayOptions, CompletionSource};
18use task::DebugScenario;
19use task::TaskContext;
20
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::{
24 cell::RefCell,
25 cmp::{Reverse, min},
26 iter,
27 ops::Range,
28 rc::Rc,
29};
30use task::ResolvedTask;
31use ui::{
32 Color, IntoElement, ListItem, Pixels, Popover, ScrollAxes, Scrollbars, Styled, WithScrollbar,
33 prelude::*,
34};
35use util::ResultExt;
36
37use crate::hover_popover::{hover_markdown_style, open_markdown_url};
38use crate::{
39 CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
40 ResolvedTasks,
41 actions::{ConfirmCodeAction, ConfirmCompletion},
42 split_words, styled_runs_for_code_label,
43};
44use crate::{CodeActionSource, EditorSettings};
45use collections::{HashSet, VecDeque};
46use settings::{Settings, SnippetSortOrder};
47
48pub const MENU_GAP: Pixels = px(4.);
49pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
50pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
51pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
52pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
53pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
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 widest_completion_ix = if self.display_options.dynamic_width {
790 let completions = self.completions.borrow();
791 let widest_completion_ix = self
792 .entries
793 .borrow()
794 .iter()
795 .enumerate()
796 .max_by_key(|(_, mat)| {
797 let completion = &completions[mat.candidate_id];
798 let documentation = &completion.documentation;
799
800 let mut len = completion.label.text.chars().count();
801 if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
802 if show_completion_documentation {
803 len += text.chars().count();
804 }
805 }
806
807 len
808 })
809 .map(|(ix, _)| ix);
810 drop(completions);
811 widest_completion_ix
812 } else {
813 None
814 };
815
816 let selected_item = self.selected_item;
817 let completions = self.completions.clone();
818 let entries = self.entries.clone();
819 let last_rendered_range = self.last_rendered_range.clone();
820 let style = style.clone();
821 let list = uniform_list(
822 "completions",
823 self.entries.borrow().len(),
824 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
825 last_rendered_range.borrow_mut().replace(range.clone());
826 let start_ix = range.start;
827 let completions_guard = completions.borrow_mut();
828
829 entries.borrow()[range]
830 .iter()
831 .enumerate()
832 .map(|(ix, mat)| {
833 let item_ix = start_ix + ix;
834 let completion = &completions_guard[mat.candidate_id];
835 let documentation = if show_completion_documentation {
836 &completion.documentation
837 } else {
838 &None
839 };
840
841 let filter_start = completion.label.filter_range.start;
842 let highlights = gpui::combine_highlights(
843 mat.ranges().map(|range| {
844 (
845 filter_start + range.start..filter_start + range.end,
846 FontWeight::BOLD.into(),
847 )
848 }),
849 styled_runs_for_code_label(
850 &completion.label,
851 &style.syntax,
852 &style.local_player,
853 )
854 .map(|(range, mut highlight)| {
855 // Ignore font weight for syntax highlighting, as we'll use it
856 // for fuzzy matches.
857 highlight.font_weight = None;
858 if completion
859 .source
860 .lsp_completion(false)
861 .and_then(|lsp_completion| {
862 match (lsp_completion.deprecated, &lsp_completion.tags) {
863 (Some(true), _) => Some(true),
864 (_, Some(tags)) => {
865 Some(tags.contains(&CompletionItemTag::DEPRECATED))
866 }
867 _ => None,
868 }
869 })
870 .unwrap_or(false)
871 {
872 highlight.strikethrough = Some(StrikethroughStyle {
873 thickness: 1.0.into(),
874 ..Default::default()
875 });
876 highlight.color = Some(cx.theme().colors().text_muted);
877 }
878
879 (range, highlight)
880 }),
881 );
882
883 let completion_label = StyledText::new(completion.label.text.clone())
884 .with_default_highlights(&style.text, highlights);
885
886 let documentation_label = match documentation {
887 Some(CompletionDocumentation::SingleLine(text))
888 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
889 single_line: text,
890 ..
891 }) => {
892 if text.trim().is_empty() {
893 None
894 } else {
895 Some(
896 Label::new(text.clone())
897 .ml_4()
898 .size(LabelSize::Small)
899 .color(Color::Muted),
900 )
901 }
902 }
903 _ => None,
904 };
905
906 let start_slot = completion
907 .color()
908 .map(|color| {
909 div()
910 .flex_shrink_0()
911 .size_3p5()
912 .rounded_xs()
913 .bg(color)
914 .into_any_element()
915 })
916 .or_else(|| {
917 completion.icon_path.as_ref().map(|path| {
918 Icon::from_path(path)
919 .size(IconSize::XSmall)
920 .color(Color::Muted)
921 .into_any_element()
922 })
923 });
924
925 div()
926 .min_w(COMPLETION_MENU_MIN_WIDTH)
927 .max_w(COMPLETION_MENU_MAX_WIDTH)
928 .child(
929 ListItem::new(mat.candidate_id)
930 .inset(true)
931 .toggle_state(item_ix == selected_item)
932 .on_click(cx.listener(move |editor, _event, window, cx| {
933 cx.stop_propagation();
934 if let Some(task) = editor.confirm_completion(
935 &ConfirmCompletion {
936 item_ix: Some(item_ix),
937 },
938 window,
939 cx,
940 ) {
941 task.detach_and_log_err(cx)
942 }
943 }))
944 .start_slot::<AnyElement>(start_slot)
945 .child(h_flex().overflow_hidden().child(completion_label))
946 .end_slot::<Label>(documentation_label),
947 )
948 })
949 .collect()
950 }),
951 )
952 .occlude()
953 .max_h(max_height_in_lines as f32 * window.line_height())
954 .track_scroll(&self.scroll_handle)
955 .with_sizing_behavior(ListSizingBehavior::Infer)
956 .map(|this| {
957 if self.display_options.dynamic_width {
958 this.with_width_from_item(widest_completion_ix)
959 } else {
960 this.w(rems(34.))
961 }
962 });
963
964 Popover::new()
965 .child(
966 div().child(list).custom_scrollbars(
967 Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
968 .show_along(ScrollAxes::Vertical)
969 .tracked_scroll_handle(&self.scroll_handle),
970 window,
971 cx,
972 ),
973 )
974 .into_any_element()
975 }
976
977 fn render_aside(
978 &mut self,
979 max_size: Size<Pixels>,
980 window: &mut Window,
981 cx: &mut Context<Editor>,
982 ) -> Option<AnyElement> {
983 if !self.show_completion_documentation {
984 return None;
985 }
986
987 let mat = &self.entries.borrow()[self.selected_item];
988 let completions = self.completions.borrow();
989 let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
990 Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
991 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
992 plain_text: Some(text),
993 ..
994 }) => div().child(text.clone()),
995 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
996 let Some((false, markdown)) = self.get_or_create_markdown(
997 mat.candidate_id,
998 Some(source),
999 true,
1000 &completions,
1001 cx,
1002 ) else {
1003 return None;
1004 };
1005 Self::render_markdown(markdown, window, cx)
1006 }
1007 None => {
1008 // Handle the case where documentation hasn't yet been resolved but there's a
1009 // `new_text` match in the cache.
1010 //
1011 // TODO: It's inconsistent that documentation caching based on matching `new_text`
1012 // only works for markdown. Consider generally caching the results of resolving
1013 // completions.
1014 let Some((false, markdown)) =
1015 self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
1016 else {
1017 return None;
1018 };
1019 Self::render_markdown(markdown, window, cx)
1020 }
1021 Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
1022 Some(CompletionDocumentation::SingleLine(_)) => return None,
1023 Some(CompletionDocumentation::Undocumented) => return None,
1024 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1025 plain_text: None,
1026 ..
1027 }) => {
1028 return None;
1029 }
1030 };
1031
1032 Some(
1033 Popover::new()
1034 .child(
1035 multiline_docs
1036 .id("multiline_docs")
1037 .px(MENU_ASIDE_X_PADDING / 2.)
1038 .max_w(max_size.width)
1039 .max_h(max_size.height)
1040 .overflow_y_scroll()
1041 .track_scroll(&self.scroll_handle_aside)
1042 .occlude(),
1043 )
1044 .into_any_element(),
1045 )
1046 }
1047
1048 fn render_markdown(
1049 markdown: Entity<Markdown>,
1050 window: &mut Window,
1051 cx: &mut Context<Editor>,
1052 ) -> Div {
1053 div().child(
1054 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1055 .code_block_renderer(markdown::CodeBlockRenderer::Default {
1056 copy_button: false,
1057 copy_button_on_hover: false,
1058 border: false,
1059 })
1060 .on_url_click(open_markdown_url),
1061 )
1062 }
1063
1064 pub fn filter(
1065 &mut self,
1066 query: Arc<String>,
1067 query_end: text::Anchor,
1068 buffer: &Entity<Buffer>,
1069 provider: Option<Rc<dyn CompletionProvider>>,
1070 window: &mut Window,
1071 cx: &mut Context<Editor>,
1072 ) {
1073 self.cancel_filter.store(true, Ordering::Relaxed);
1074 self.cancel_filter = Arc::new(AtomicBool::new(false));
1075 let matches = self.do_async_filtering(query, query_end, buffer, cx);
1076 let id = self.id;
1077 self.filter_task = cx.spawn_in(window, async move |editor, cx| {
1078 let matches = matches.await;
1079 editor
1080 .update_in(cx, |editor, window, cx| {
1081 editor.with_completions_menu_matching_id(id, |this| {
1082 if let Some(this) = this {
1083 this.set_filter_results(matches, provider, window, cx);
1084 }
1085 });
1086 })
1087 .ok();
1088 });
1089 }
1090
1091 pub fn do_async_filtering(
1092 &self,
1093 query: Arc<String>,
1094 query_end: text::Anchor,
1095 buffer: &Entity<Buffer>,
1096 cx: &Context<Editor>,
1097 ) -> Task<Vec<StringMatch>> {
1098 let buffer_snapshot = buffer.read(cx).snapshot();
1099 let background_executor = cx.background_executor().clone();
1100 let match_candidates = self.match_candidates.clone();
1101 let cancel_filter = self.cancel_filter.clone();
1102 let default_query = query.clone();
1103
1104 let matches_task = cx.background_spawn(async move {
1105 let queries_and_candidates = match_candidates
1106 .iter()
1107 .map(|(query_start, candidates)| {
1108 let query_for_batch = match query_start {
1109 Some(start) => {
1110 Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
1111 }
1112 None => default_query.clone(),
1113 };
1114 (query_for_batch, candidates)
1115 })
1116 .collect_vec();
1117
1118 let mut results = vec![];
1119 for (query, match_candidates) in queries_and_candidates {
1120 results.extend(
1121 fuzzy::match_strings(
1122 &match_candidates,
1123 &query,
1124 query.chars().any(|c| c.is_uppercase()),
1125 false,
1126 1000,
1127 &cancel_filter,
1128 background_executor.clone(),
1129 )
1130 .await,
1131 );
1132 }
1133 results
1134 });
1135
1136 let completions = self.completions.clone();
1137 let sort_completions = self.sort_completions;
1138 let snippet_sort_order = self.snippet_sort_order;
1139 cx.foreground_executor().spawn(async move {
1140 let mut matches = matches_task.await;
1141
1142 let completions_ref = completions.borrow();
1143
1144 if sort_completions {
1145 matches = Self::sort_string_matches(
1146 matches,
1147 Some(&query), // used for non-snippets only
1148 snippet_sort_order,
1149 &completions_ref,
1150 );
1151 }
1152
1153 // Remove duplicate snippet prefixes (e.g., "cool code" will match
1154 // the text "c c" in two places; we should only show the longer one)
1155 let mut snippets_seen = HashSet::<(usize, usize)>::default();
1156 matches.retain(|result| {
1157 match completions_ref[result.candidate_id].snippet_deduplication_key {
1158 Some(key) => snippets_seen.insert(key),
1159 None => true,
1160 }
1161 });
1162
1163 matches
1164 })
1165 }
1166
1167 pub fn set_filter_results(
1168 &mut self,
1169 matches: Vec<StringMatch>,
1170 provider: Option<Rc<dyn CompletionProvider>>,
1171 window: &mut Window,
1172 cx: &mut Context<Editor>,
1173 ) {
1174 *self.entries.borrow_mut() = matches.into_boxed_slice();
1175 self.selected_item = 0;
1176 self.handle_selection_changed(provider.as_deref(), window, cx);
1177 }
1178
1179 pub fn sort_string_matches(
1180 matches: Vec<StringMatch>,
1181 query: Option<&str>,
1182 snippet_sort_order: SnippetSortOrder,
1183 completions: &[Completion],
1184 ) -> Vec<StringMatch> {
1185 let mut matches = matches;
1186
1187 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1188 enum MatchTier<'a> {
1189 WordStartMatch {
1190 sort_exact: Reverse<i32>,
1191 sort_snippet: Reverse<i32>,
1192 sort_score: Reverse<OrderedFloat<f64>>,
1193 sort_positions: Vec<usize>,
1194 sort_text: Option<&'a str>,
1195 sort_kind: usize,
1196 sort_label: &'a str,
1197 },
1198 OtherMatch {
1199 sort_score: Reverse<OrderedFloat<f64>>,
1200 },
1201 }
1202
1203 let query_start_lower = query
1204 .as_ref()
1205 .and_then(|q| q.chars().next())
1206 .and_then(|c| c.to_lowercase().next());
1207
1208 if snippet_sort_order == SnippetSortOrder::None {
1209 matches
1210 .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
1211 }
1212
1213 matches.sort_unstable_by_key(|string_match| {
1214 let completion = &completions[string_match.candidate_id];
1215
1216 let sort_text = match &completion.source {
1217 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1218 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1219 _ => None,
1220 };
1221
1222 let (sort_kind, sort_label) = completion.sort_key();
1223
1224 let score = string_match.score;
1225 let sort_score = Reverse(OrderedFloat(score));
1226
1227 // Snippets do their own first-letter matching logic elsewhere.
1228 let is_snippet = completion.is_snippet_kind();
1229 let query_start_doesnt_match_split_words = !is_snippet
1230 && query_start_lower
1231 .map(|query_char| {
1232 !split_words(&string_match.string).any(|word| {
1233 word.chars().next().and_then(|c| c.to_lowercase().next())
1234 == Some(query_char)
1235 })
1236 })
1237 .unwrap_or(false);
1238
1239 if query_start_doesnt_match_split_words {
1240 MatchTier::OtherMatch { sort_score }
1241 } else {
1242 let sort_snippet = match snippet_sort_order {
1243 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1244 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1245 SnippetSortOrder::Inline => Reverse(0),
1246 SnippetSortOrder::None => Reverse(0),
1247 };
1248 let sort_positions = string_match.positions.clone();
1249 // This exact matching won't work for multi-word snippets, but it's fine
1250 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1251 1
1252 } else {
1253 0
1254 });
1255
1256 MatchTier::WordStartMatch {
1257 sort_exact,
1258 sort_snippet,
1259 sort_score,
1260 sort_positions,
1261 sort_text,
1262 sort_kind,
1263 sort_label,
1264 }
1265 }
1266 });
1267
1268 matches
1269 }
1270
1271 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1272 self.markdown_cache = prev_menu.markdown_cache.clone();
1273
1274 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1275 let prev_completions = prev_menu.completions.borrow();
1276 self.markdown_cache
1277 .borrow_mut()
1278 .retain_mut(|(key, _markdown)| match key {
1279 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1280 MarkdownCacheKey::ForCandidate { candidate_id } => {
1281 if let Some(completion) = prev_completions.get(*candidate_id) {
1282 match &completion.documentation {
1283 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1284 *key = MarkdownCacheKey::ForCompletionMatch {
1285 new_text: completion.new_text.clone(),
1286 markdown_source: source.clone(),
1287 };
1288 true
1289 }
1290 _ => false,
1291 }
1292 } else {
1293 false
1294 }
1295 }
1296 });
1297 }
1298
1299 pub fn scroll_aside(
1300 &mut self,
1301 amount: ScrollAmount,
1302 window: &mut Window,
1303 cx: &mut Context<Editor>,
1304 ) {
1305 let mut offset = self.scroll_handle_aside.offset();
1306
1307 offset.y -= amount.pixels(
1308 window.line_height(),
1309 self.scroll_handle_aside.bounds().size.height - px(16.),
1310 ) / 2.0;
1311
1312 cx.notify();
1313 self.scroll_handle_aside.set_offset(offset);
1314 }
1315}
1316
1317#[derive(Clone)]
1318pub struct AvailableCodeAction {
1319 pub excerpt_id: ExcerptId,
1320 pub action: CodeAction,
1321 pub provider: Rc<dyn CodeActionProvider>,
1322}
1323
1324#[derive(Clone)]
1325pub struct CodeActionContents {
1326 tasks: Option<Rc<ResolvedTasks>>,
1327 actions: Option<Rc<[AvailableCodeAction]>>,
1328 debug_scenarios: Vec<DebugScenario>,
1329 pub(crate) context: TaskContext,
1330}
1331
1332impl CodeActionContents {
1333 pub(crate) fn new(
1334 tasks: Option<ResolvedTasks>,
1335 actions: Option<Rc<[AvailableCodeAction]>>,
1336 debug_scenarios: Vec<DebugScenario>,
1337 context: TaskContext,
1338 ) -> Self {
1339 Self {
1340 tasks: tasks.map(Rc::new),
1341 actions,
1342 debug_scenarios,
1343 context,
1344 }
1345 }
1346
1347 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1348 self.tasks.as_deref()
1349 }
1350
1351 fn len(&self) -> usize {
1352 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1353 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1354 tasks_len + code_actions_len + self.debug_scenarios.len()
1355 }
1356
1357 pub fn is_empty(&self) -> bool {
1358 self.len() == 0
1359 }
1360
1361 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1362 self.tasks
1363 .iter()
1364 .flat_map(|tasks| {
1365 tasks
1366 .templates
1367 .iter()
1368 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1369 })
1370 .chain(self.actions.iter().flat_map(|actions| {
1371 actions.iter().map(|available| CodeActionsItem::CodeAction {
1372 excerpt_id: available.excerpt_id,
1373 action: available.action.clone(),
1374 provider: available.provider.clone(),
1375 })
1376 }))
1377 .chain(
1378 self.debug_scenarios
1379 .iter()
1380 .cloned()
1381 .map(CodeActionsItem::DebugScenario),
1382 )
1383 }
1384
1385 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1386 if let Some(tasks) = &self.tasks {
1387 if let Some((kind, task)) = tasks.templates.get(index) {
1388 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1389 } else {
1390 index -= tasks.templates.len();
1391 }
1392 }
1393 if let Some(actions) = &self.actions {
1394 if let Some(available) = actions.get(index) {
1395 return Some(CodeActionsItem::CodeAction {
1396 excerpt_id: available.excerpt_id,
1397 action: available.action.clone(),
1398 provider: available.provider.clone(),
1399 });
1400 } else {
1401 index -= actions.len();
1402 }
1403 }
1404
1405 self.debug_scenarios
1406 .get(index)
1407 .cloned()
1408 .map(CodeActionsItem::DebugScenario)
1409 }
1410}
1411
1412#[derive(Clone)]
1413pub enum CodeActionsItem {
1414 Task(TaskSourceKind, ResolvedTask),
1415 CodeAction {
1416 excerpt_id: ExcerptId,
1417 action: CodeAction,
1418 provider: Rc<dyn CodeActionProvider>,
1419 },
1420 DebugScenario(DebugScenario),
1421}
1422
1423impl CodeActionsItem {
1424 pub fn label(&self) -> String {
1425 match self {
1426 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1427 Self::Task(_, task) => task.resolved_label.clone(),
1428 Self::DebugScenario(scenario) => scenario.label.to_string(),
1429 }
1430 }
1431
1432 pub fn menu_label(&self) -> String {
1433 match self {
1434 Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1435 Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1436 Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1437 }
1438 }
1439}
1440
1441pub struct CodeActionsMenu {
1442 pub actions: CodeActionContents,
1443 pub buffer: Entity<Buffer>,
1444 pub selected_item: usize,
1445 pub scroll_handle: UniformListScrollHandle,
1446 pub deployed_from: Option<CodeActionSource>,
1447}
1448
1449impl CodeActionsMenu {
1450 fn select_first(&mut self, cx: &mut Context<Editor>) {
1451 self.selected_item = if self.scroll_handle.y_flipped() {
1452 self.actions.len() - 1
1453 } else {
1454 0
1455 };
1456 self.scroll_handle
1457 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1458 cx.notify()
1459 }
1460
1461 fn select_last(&mut self, cx: &mut Context<Editor>) {
1462 self.selected_item = if self.scroll_handle.y_flipped() {
1463 0
1464 } else {
1465 self.actions.len() - 1
1466 };
1467 self.scroll_handle
1468 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1469 cx.notify()
1470 }
1471
1472 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1473 self.selected_item = if self.scroll_handle.y_flipped() {
1474 self.next_match_index()
1475 } else {
1476 self.prev_match_index()
1477 };
1478 self.scroll_handle
1479 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1480 cx.notify();
1481 }
1482
1483 fn select_next(&mut self, cx: &mut Context<Editor>) {
1484 self.selected_item = if self.scroll_handle.y_flipped() {
1485 self.prev_match_index()
1486 } else {
1487 self.next_match_index()
1488 };
1489 self.scroll_handle
1490 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1491 cx.notify();
1492 }
1493
1494 fn prev_match_index(&self) -> usize {
1495 if self.selected_item > 0 {
1496 self.selected_item - 1
1497 } else {
1498 self.actions.len() - 1
1499 }
1500 }
1501
1502 fn next_match_index(&self) -> usize {
1503 if self.selected_item + 1 < self.actions.len() {
1504 self.selected_item + 1
1505 } else {
1506 0
1507 }
1508 }
1509
1510 pub fn visible(&self) -> bool {
1511 !self.actions.is_empty()
1512 }
1513
1514 fn origin(&self) -> ContextMenuOrigin {
1515 match &self.deployed_from {
1516 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1517 ContextMenuOrigin::GutterIndicator(*row)
1518 }
1519 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1520 None => ContextMenuOrigin::Cursor,
1521 }
1522 }
1523
1524 fn render(
1525 &self,
1526 _style: &EditorStyle,
1527 max_height_in_lines: u32,
1528 window: &mut Window,
1529 cx: &mut Context<Editor>,
1530 ) -> AnyElement {
1531 let actions = self.actions.clone();
1532 let selected_item = self.selected_item;
1533 let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1534
1535 let list = uniform_list(
1536 "code_actions_menu",
1537 self.actions.len(),
1538 cx.processor(move |_this, range: Range<usize>, _, cx| {
1539 actions
1540 .iter()
1541 .skip(range.start)
1542 .take(range.end - range.start)
1543 .enumerate()
1544 .map(|(ix, action)| {
1545 let item_ix = range.start + ix;
1546 let selected = item_ix == selected_item;
1547 let colors = cx.theme().colors();
1548
1549 ListItem::new(item_ix)
1550 .inset(true)
1551 .toggle_state(selected)
1552 .overflow_x()
1553 .child(
1554 div()
1555 .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1556 .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1557 .overflow_hidden()
1558 .text_ellipsis()
1559 .when(is_quick_action_bar, |this| this.text_ui(cx))
1560 .when(selected, |this| this.text_color(colors.text_accent))
1561 .child(action.menu_label()),
1562 )
1563 .on_click(cx.listener(move |editor, _, window, cx| {
1564 cx.stop_propagation();
1565 if let Some(task) = editor.confirm_code_action(
1566 &ConfirmCodeAction {
1567 item_ix: Some(item_ix),
1568 },
1569 window,
1570 cx,
1571 ) {
1572 task.detach_and_log_err(cx)
1573 }
1574 }))
1575 })
1576 .collect()
1577 }),
1578 )
1579 .occlude()
1580 .max_h(max_height_in_lines as f32 * window.line_height())
1581 .track_scroll(&self.scroll_handle)
1582 .with_width_from_item(
1583 self.actions
1584 .iter()
1585 .enumerate()
1586 .max_by_key(|(_, action)| match action {
1587 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1588 CodeActionsItem::CodeAction { action, .. } => {
1589 action.lsp_action.title().chars().count()
1590 }
1591 CodeActionsItem::DebugScenario(scenario) => {
1592 format!("debug: {}", scenario.label).chars().count()
1593 }
1594 })
1595 .map(|(ix, _)| ix),
1596 )
1597 .with_sizing_behavior(ListSizingBehavior::Infer);
1598
1599 Popover::new().child(list).into_any_element()
1600 }
1601
1602 fn render_aside(
1603 &mut self,
1604 max_size: Size<Pixels>,
1605 window: &mut Window,
1606 _cx: &mut Context<Editor>,
1607 ) -> Option<AnyElement> {
1608 let Some(action) = self.actions.get(self.selected_item) else {
1609 return None;
1610 };
1611
1612 let label = action.menu_label();
1613 let text_system = window.text_system();
1614 let mut line_wrapper = text_system.line_wrapper(
1615 window.text_style().font(),
1616 window.text_style().font_size.to_pixels(window.rem_size()),
1617 );
1618 let is_truncated =
1619 line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
1620
1621 if is_truncated.is_none() {
1622 return None;
1623 }
1624
1625 Some(
1626 Popover::new()
1627 .child(
1628 div()
1629 .child(label)
1630 .id("code_actions_menu_extended")
1631 .px(MENU_ASIDE_X_PADDING / 2.)
1632 .max_w(max_size.width)
1633 .max_h(max_size.height)
1634 .occlude(),
1635 )
1636 .into_any_element(),
1637 )
1638 }
1639}