1use fuzzy::{StringMatch, StringMatchCandidate};
2use gpui::{
3 AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
4 Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
5};
6use gpui::{AsyncWindowContext, WeakEntity};
7use itertools::Itertools;
8use language::CodeLabel;
9use language::{Buffer, LanguageName, LanguageRegistry};
10use markdown::{Markdown, MarkdownElement};
11use multi_buffer::{Anchor, ExcerptId};
12use ordered_float::OrderedFloat;
13use project::CompletionSource;
14use project::lsp_store::CompletionDocumentation;
15use project::{CodeAction, Completion, TaskSourceKind};
16use task::DebugScenario;
17use task::TaskContext;
18
19use std::collections::VecDeque;
20use std::sync::Arc;
21use std::{
22 cell::RefCell,
23 cmp::{Reverse, min},
24 iter,
25 ops::Range,
26 rc::Rc,
27};
28use task::ResolvedTask;
29use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
30use util::ResultExt;
31
32use crate::CodeActionSource;
33use crate::editor_settings::SnippetSortOrder;
34use crate::hover_popover::{hover_markdown_style, open_markdown_url};
35use crate::{
36 CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
37 EditorStyle, ResolvedTasks,
38 actions::{ConfirmCodeAction, ConfirmCompletion},
39 split_words, styled_runs_for_code_label,
40};
41
42pub const MENU_GAP: Pixels = px(4.);
43pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
44pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
45pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
46
47// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
48// documentation not yet being parsed.
49//
50// The size of the cache is set to the number of items fetched around the current selection plus one
51// for the current selection and another to avoid cases where and adjacent selection exits the
52// cache. The only current benefit of a larger cache would be doing less markdown parsing when the
53// selection revisits items.
54//
55// One future benefit of a larger cache would be reducing flicker on backspace. This would require
56// not recreating the menu on every change, by not re-querying the language server when
57// `is_incomplete = false`.
58const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2;
59const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
60const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
61
62// Number of items beyond the visible items to resolve documentation.
63const RESOLVE_BEFORE_ITEMS: usize = 4;
64const RESOLVE_AFTER_ITEMS: usize = 4;
65
66pub enum CodeContextMenu {
67 Completions(CompletionsMenu),
68 CodeActions(CodeActionsMenu),
69}
70
71impl CodeContextMenu {
72 pub fn select_first(
73 &mut self,
74 provider: Option<&dyn CompletionProvider>,
75 window: &mut Window,
76 cx: &mut Context<Editor>,
77 ) -> bool {
78 if self.visible() {
79 match self {
80 CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
81 CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
82 }
83 true
84 } else {
85 false
86 }
87 }
88
89 pub fn select_prev(
90 &mut self,
91 provider: Option<&dyn CompletionProvider>,
92 window: &mut Window,
93 cx: &mut Context<Editor>,
94 ) -> bool {
95 if self.visible() {
96 match self {
97 CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
98 CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
99 }
100 true
101 } else {
102 false
103 }
104 }
105
106 pub fn select_next(
107 &mut self,
108 provider: Option<&dyn CompletionProvider>,
109 window: &mut Window,
110 cx: &mut Context<Editor>,
111 ) -> bool {
112 if self.visible() {
113 match self {
114 CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
115 CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
116 }
117 true
118 } else {
119 false
120 }
121 }
122
123 pub fn select_last(
124 &mut self,
125 provider: Option<&dyn CompletionProvider>,
126 window: &mut Window,
127 cx: &mut Context<Editor>,
128 ) -> bool {
129 if self.visible() {
130 match self {
131 CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
132 CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
133 }
134 true
135 } else {
136 false
137 }
138 }
139
140 pub fn visible(&self) -> bool {
141 match self {
142 CodeContextMenu::Completions(menu) => menu.visible(),
143 CodeContextMenu::CodeActions(menu) => menu.visible(),
144 }
145 }
146
147 pub fn origin(&self) -> ContextMenuOrigin {
148 match self {
149 CodeContextMenu::Completions(menu) => menu.origin(),
150 CodeContextMenu::CodeActions(menu) => menu.origin(),
151 }
152 }
153
154 pub fn render(
155 &self,
156 style: &EditorStyle,
157 max_height_in_lines: u32,
158 window: &mut Window,
159 cx: &mut Context<Editor>,
160 ) -> AnyElement {
161 match self {
162 CodeContextMenu::Completions(menu) => {
163 menu.render(style, max_height_in_lines, window, cx)
164 }
165 CodeContextMenu::CodeActions(menu) => {
166 menu.render(style, max_height_in_lines, window, cx)
167 }
168 }
169 }
170
171 pub fn render_aside(
172 &mut self,
173 max_size: Size<Pixels>,
174 window: &mut Window,
175 cx: &mut Context<Editor>,
176 ) -> Option<AnyElement> {
177 match self {
178 CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
179 CodeContextMenu::CodeActions(_) => None,
180 }
181 }
182
183 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
184 match self {
185 CodeContextMenu::Completions(completions_menu) => completions_menu
186 .get_or_create_entry_markdown(completions_menu.selected_item, cx)
187 .as_ref()
188 .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
189 CodeContextMenu::CodeActions(_) => false,
190 }
191 }
192}
193
194pub enum ContextMenuOrigin {
195 Cursor,
196 GutterIndicator(DisplayRow),
197 QuickActionBar,
198}
199
200#[derive(Clone)]
201pub struct CompletionsMenu {
202 pub id: CompletionId,
203 sort_completions: bool,
204 pub initial_position: Anchor,
205 pub buffer: Entity<Buffer>,
206 pub completions: Rc<RefCell<Box<[Completion]>>>,
207 match_candidates: Rc<[StringMatchCandidate]>,
208 pub entries: Rc<RefCell<Vec<StringMatch>>>,
209 pub selected_item: usize,
210 scroll_handle: UniformListScrollHandle,
211 resolve_completions: bool,
212 show_completion_documentation: bool,
213 pub(super) ignore_completion_provider: bool,
214 last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
215 markdown_cache: Rc<RefCell<VecDeque<(usize, Entity<Markdown>)>>>,
216 language_registry: Option<Arc<LanguageRegistry>>,
217 language: Option<LanguageName>,
218 snippet_sort_order: SnippetSortOrder,
219}
220
221impl CompletionsMenu {
222 pub fn new(
223 id: CompletionId,
224 sort_completions: bool,
225 show_completion_documentation: bool,
226 ignore_completion_provider: bool,
227 initial_position: Anchor,
228 buffer: Entity<Buffer>,
229 completions: Box<[Completion]>,
230 snippet_sort_order: SnippetSortOrder,
231 language_registry: Option<Arc<LanguageRegistry>>,
232 language: Option<LanguageName>,
233 cx: &mut Context<Editor>,
234 ) -> Self {
235 let match_candidates = completions
236 .iter()
237 .enumerate()
238 .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
239 .collect();
240
241 let completions_menu = Self {
242 id,
243 sort_completions,
244 initial_position,
245 buffer,
246 show_completion_documentation,
247 ignore_completion_provider,
248 completions: RefCell::new(completions).into(),
249 match_candidates,
250 entries: RefCell::new(Vec::new()).into(),
251 selected_item: 0,
252 scroll_handle: UniformListScrollHandle::new(),
253 resolve_completions: true,
254 last_rendered_range: RefCell::new(None).into(),
255 markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(),
256 language_registry,
257 language,
258 snippet_sort_order,
259 };
260
261 completions_menu.start_markdown_parse_for_nearby_entries(cx);
262
263 completions_menu
264 }
265
266 pub fn new_snippet_choices(
267 id: CompletionId,
268 sort_completions: bool,
269 choices: &Vec<String>,
270 selection: Range<Anchor>,
271 buffer: Entity<Buffer>,
272 snippet_sort_order: SnippetSortOrder,
273 ) -> Self {
274 let completions = choices
275 .iter()
276 .map(|choice| Completion {
277 replace_range: selection.start.text_anchor..selection.end.text_anchor,
278 new_text: choice.to_string(),
279 label: CodeLabel {
280 text: choice.to_string(),
281 runs: Default::default(),
282 filter_range: Default::default(),
283 },
284 icon_path: None,
285 documentation: None,
286 confirm: None,
287 insert_text_mode: None,
288 source: CompletionSource::Custom,
289 })
290 .collect();
291
292 let match_candidates = choices
293 .iter()
294 .enumerate()
295 .map(|(id, completion)| StringMatchCandidate::new(id, &completion))
296 .collect();
297 let entries = choices
298 .iter()
299 .enumerate()
300 .map(|(id, completion)| StringMatch {
301 candidate_id: id,
302 score: 1.,
303 positions: vec![],
304 string: completion.clone(),
305 })
306 .collect::<Vec<_>>();
307 Self {
308 id,
309 sort_completions,
310 initial_position: selection.start,
311 buffer,
312 completions: RefCell::new(completions).into(),
313 match_candidates,
314 entries: RefCell::new(entries).into(),
315 selected_item: 0,
316 scroll_handle: UniformListScrollHandle::new(),
317 resolve_completions: false,
318 show_completion_documentation: false,
319 ignore_completion_provider: false,
320 last_rendered_range: RefCell::new(None).into(),
321 markdown_cache: RefCell::new(VecDeque::new()).into(),
322 language_registry: None,
323 language: None,
324 snippet_sort_order,
325 }
326 }
327
328 fn select_first(
329 &mut self,
330 provider: Option<&dyn CompletionProvider>,
331 window: &mut Window,
332 cx: &mut Context<Editor>,
333 ) {
334 let index = if self.scroll_handle.y_flipped() {
335 self.entries.borrow().len() - 1
336 } else {
337 0
338 };
339 self.update_selection_index(index, provider, window, cx);
340 }
341
342 fn select_last(
343 &mut self,
344 provider: Option<&dyn CompletionProvider>,
345 window: &mut Window,
346 cx: &mut Context<Editor>,
347 ) {
348 let index = if self.scroll_handle.y_flipped() {
349 0
350 } else {
351 self.entries.borrow().len() - 1
352 };
353 self.update_selection_index(index, provider, window, cx);
354 }
355
356 fn select_prev(
357 &mut self,
358 provider: Option<&dyn CompletionProvider>,
359 window: &mut Window,
360 cx: &mut Context<Editor>,
361 ) {
362 let index = if self.scroll_handle.y_flipped() {
363 self.next_match_index()
364 } else {
365 self.prev_match_index()
366 };
367 self.update_selection_index(index, provider, window, cx);
368 }
369
370 fn select_next(
371 &mut self,
372 provider: Option<&dyn CompletionProvider>,
373 window: &mut Window,
374 cx: &mut Context<Editor>,
375 ) {
376 let index = if self.scroll_handle.y_flipped() {
377 self.prev_match_index()
378 } else {
379 self.next_match_index()
380 };
381 self.update_selection_index(index, provider, window, cx);
382 }
383
384 fn update_selection_index(
385 &mut self,
386 match_index: usize,
387 provider: Option<&dyn CompletionProvider>,
388 window: &mut Window,
389 cx: &mut Context<Editor>,
390 ) {
391 if self.selected_item != match_index {
392 self.selected_item = match_index;
393 self.scroll_handle
394 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
395 self.resolve_visible_completions(provider, cx);
396 self.start_markdown_parse_for_nearby_entries(cx);
397 if let Some(provider) = provider {
398 self.handle_selection_changed(provider, window, cx);
399 }
400 cx.notify();
401 }
402 }
403
404 fn prev_match_index(&self) -> usize {
405 if self.selected_item > 0 {
406 self.selected_item - 1
407 } else {
408 self.entries.borrow().len() - 1
409 }
410 }
411
412 fn next_match_index(&self) -> usize {
413 if self.selected_item + 1 < self.entries.borrow().len() {
414 self.selected_item + 1
415 } else {
416 0
417 }
418 }
419
420 fn handle_selection_changed(
421 &self,
422 provider: &dyn CompletionProvider,
423 window: &mut Window,
424 cx: &mut App,
425 ) {
426 let entries = self.entries.borrow();
427 let entry = if self.selected_item < entries.len() {
428 Some(&entries[self.selected_item])
429 } else {
430 None
431 };
432 provider.selection_changed(entry, window, cx);
433 }
434
435 pub fn resolve_visible_completions(
436 &mut self,
437 provider: Option<&dyn CompletionProvider>,
438 cx: &mut Context<Editor>,
439 ) {
440 if !self.resolve_completions {
441 return;
442 }
443 let Some(provider) = provider else {
444 return;
445 };
446
447 // Attempt to resolve completions for every item that will be displayed. This matters
448 // because single line documentation may be displayed inline with the completion.
449 //
450 // When navigating to the very beginning or end of completions, `last_rendered_range` may
451 // have no overlap with the completions that will be displayed, so instead use a range based
452 // on the last rendered count.
453 const APPROXIMATE_VISIBLE_COUNT: usize = 12;
454 let last_rendered_range = self.last_rendered_range.borrow().clone();
455 let visible_count = last_rendered_range
456 .clone()
457 .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
458 let entries = self.entries.borrow();
459 let entry_range = if self.selected_item == 0 {
460 0..min(visible_count, entries.len())
461 } else if self.selected_item == entries.len() - 1 {
462 entries.len().saturating_sub(visible_count)..entries.len()
463 } else {
464 last_rendered_range.map_or(0..0, |range| {
465 min(range.start, entries.len())..min(range.end, entries.len())
466 })
467 };
468
469 // Expand the range to resolve more completions than are predicted to be visible, to reduce
470 // jank on navigation.
471 let entry_indices = util::expanded_and_wrapped_usize_range(
472 entry_range.clone(),
473 RESOLVE_BEFORE_ITEMS,
474 RESOLVE_AFTER_ITEMS,
475 entries.len(),
476 );
477
478 // Avoid work by sometimes filtering out completions that already have documentation.
479 // This filtering doesn't happen if the completions are currently being updated.
480 let completions = self.completions.borrow();
481 let candidate_ids = entry_indices
482 .map(|i| entries[i].candidate_id)
483 .filter(|i| completions[*i].documentation.is_none());
484
485 // Current selection is always resolved even if it already has documentation, to handle
486 // out-of-spec language servers that return more results later.
487 let selected_candidate_id = entries[self.selected_item].candidate_id;
488 let candidate_ids = iter::once(selected_candidate_id)
489 .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
490 .collect::<Vec<usize>>();
491 drop(entries);
492
493 if candidate_ids.is_empty() {
494 return;
495 }
496
497 let resolve_task = provider.resolve_completions(
498 self.buffer.clone(),
499 candidate_ids,
500 self.completions.clone(),
501 cx,
502 );
503
504 let completion_id = self.id;
505 cx.spawn(async move |editor, cx| {
506 if let Some(true) = resolve_task.await.log_err() {
507 editor
508 .update(cx, |editor, cx| {
509 // `resolve_completions` modified state affecting display.
510 cx.notify();
511 editor.with_completions_menu_matching_id(
512 completion_id,
513 || (),
514 |this| this.start_markdown_parse_for_nearby_entries(cx),
515 );
516 })
517 .ok();
518 }
519 })
520 .detach();
521 }
522
523 fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
524 // Enqueue parse tasks of nearer items first.
525 //
526 // TODO: This means that the nearer items will actually be further back in the cache, which
527 // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
528 // selection to the front (when `is_render = true`).
529 let entry_indices = util::wrapped_usize_outward_from(
530 self.selected_item,
531 MARKDOWN_CACHE_BEFORE_ITEMS,
532 MARKDOWN_CACHE_AFTER_ITEMS,
533 self.entries.borrow().len(),
534 );
535
536 for index in entry_indices {
537 self.get_or_create_entry_markdown(index, cx);
538 }
539 }
540
541 fn get_or_create_entry_markdown(
542 &self,
543 index: usize,
544 cx: &mut Context<Editor>,
545 ) -> Option<Entity<Markdown>> {
546 let entries = self.entries.borrow();
547 if index >= entries.len() {
548 return None;
549 }
550 let candidate_id = entries[index].candidate_id;
551 match &self.completions.borrow()[candidate_id].documentation {
552 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some(
553 self.get_or_create_markdown(candidate_id, source.clone(), false, cx)
554 .1,
555 ),
556 Some(_) => None,
557 _ => None,
558 }
559 }
560
561 fn get_or_create_markdown(
562 &self,
563 candidate_id: usize,
564 source: SharedString,
565 is_render: bool,
566 cx: &mut Context<Editor>,
567 ) -> (bool, Entity<Markdown>) {
568 let mut markdown_cache = self.markdown_cache.borrow_mut();
569 if let Some((cache_index, (_, markdown))) = markdown_cache
570 .iter()
571 .find_position(|(id, _)| *id == candidate_id)
572 {
573 let markdown = if is_render && cache_index != 0 {
574 // Move the current selection's cache entry to the front.
575 markdown_cache.rotate_right(1);
576 let cache_len = markdown_cache.len();
577 markdown_cache.swap(0, (cache_index + 1) % cache_len);
578 &markdown_cache[0].1
579 } else {
580 markdown
581 };
582
583 let is_parsing = markdown.update(cx, |markdown, cx| {
584 // `reset` is called as it's possible for documentation to change due to resolve
585 // requests. It does nothing if `source` is unchanged.
586 markdown.reset(source, cx);
587 markdown.is_parsing()
588 });
589 return (is_parsing, markdown.clone());
590 }
591
592 if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
593 let markdown = cx.new(|cx| {
594 Markdown::new(
595 source,
596 self.language_registry.clone(),
597 self.language.clone(),
598 cx,
599 )
600 });
601 // Handles redraw when the markdown is done parsing. The current render is for a
602 // deferred draw, and so without this did not redraw when `markdown` notified.
603 cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
604 markdown_cache.push_front((candidate_id, markdown.clone()));
605 (true, markdown)
606 } else {
607 debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
608 // Moves the last cache entry to the start. The ring buffer is full, so this does no
609 // copying and just shifts indexes.
610 markdown_cache.rotate_right(1);
611 markdown_cache[0].0 = candidate_id;
612 let markdown = &markdown_cache[0].1;
613 markdown.update(cx, |markdown, cx| markdown.reset(source, cx));
614 (true, markdown.clone())
615 }
616 }
617
618 pub fn visible(&self) -> bool {
619 !self.entries.borrow().is_empty()
620 }
621
622 fn origin(&self) -> ContextMenuOrigin {
623 ContextMenuOrigin::Cursor
624 }
625
626 fn render(
627 &self,
628 style: &EditorStyle,
629 max_height_in_lines: u32,
630 window: &mut Window,
631 cx: &mut Context<Editor>,
632 ) -> AnyElement {
633 let show_completion_documentation = self.show_completion_documentation;
634 let selected_item = self.selected_item;
635 let completions = self.completions.clone();
636 let entries = self.entries.clone();
637 let last_rendered_range = self.last_rendered_range.clone();
638 let style = style.clone();
639 let list = uniform_list(
640 cx.entity().clone(),
641 "completions",
642 self.entries.borrow().len(),
643 move |_editor, range, _window, cx| {
644 last_rendered_range.borrow_mut().replace(range.clone());
645 let start_ix = range.start;
646 let completions_guard = completions.borrow_mut();
647
648 entries.borrow()[range]
649 .iter()
650 .enumerate()
651 .map(|(ix, mat)| {
652 let item_ix = start_ix + ix;
653 let completion = &completions_guard[mat.candidate_id];
654 let documentation = if show_completion_documentation {
655 &completion.documentation
656 } else {
657 &None
658 };
659
660 let filter_start = completion.label.filter_range.start;
661 let highlights = gpui::combine_highlights(
662 mat.ranges().map(|range| {
663 (
664 filter_start + range.start..filter_start + range.end,
665 FontWeight::BOLD.into(),
666 )
667 }),
668 styled_runs_for_code_label(&completion.label, &style.syntax).map(
669 |(range, mut highlight)| {
670 // Ignore font weight for syntax highlighting, as we'll use it
671 // for fuzzy matches.
672 highlight.font_weight = None;
673 if completion
674 .source
675 .lsp_completion(false)
676 .and_then(|lsp_completion| lsp_completion.deprecated)
677 .unwrap_or(false)
678 {
679 highlight.strikethrough = Some(StrikethroughStyle {
680 thickness: 1.0.into(),
681 ..Default::default()
682 });
683 highlight.color = Some(cx.theme().colors().text_muted);
684 }
685
686 (range, highlight)
687 },
688 ),
689 );
690
691 let completion_label = StyledText::new(completion.label.text.clone())
692 .with_default_highlights(&style.text, highlights);
693
694 let documentation_label = match documentation {
695 Some(CompletionDocumentation::SingleLine(text))
696 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
697 single_line: text,
698 ..
699 }) => {
700 if text.trim().is_empty() {
701 None
702 } else {
703 Some(
704 Label::new(text.clone())
705 .ml_4()
706 .size(LabelSize::Small)
707 .color(Color::Muted),
708 )
709 }
710 }
711 _ => None,
712 };
713
714 let start_slot = completion
715 .color()
716 .map(|color| {
717 div()
718 .flex_shrink_0()
719 .size_3p5()
720 .rounded_xs()
721 .bg(color)
722 .into_any_element()
723 })
724 .or_else(|| {
725 completion.icon_path.as_ref().map(|path| {
726 Icon::from_path(path)
727 .size(IconSize::XSmall)
728 .color(Color::Muted)
729 .into_any_element()
730 })
731 });
732
733 div().min_w(px(280.)).max_w(px(540.)).child(
734 ListItem::new(mat.candidate_id)
735 .inset(true)
736 .toggle_state(item_ix == selected_item)
737 .on_click(cx.listener(move |editor, _event, window, cx| {
738 cx.stop_propagation();
739 if let Some(task) = editor.confirm_completion(
740 &ConfirmCompletion {
741 item_ix: Some(item_ix),
742 },
743 window,
744 cx,
745 ) {
746 task.detach_and_log_err(cx)
747 }
748 }))
749 .start_slot::<AnyElement>(start_slot)
750 .child(h_flex().overflow_hidden().child(completion_label))
751 .end_slot::<Label>(documentation_label),
752 )
753 })
754 .collect()
755 },
756 )
757 .occlude()
758 .max_h(max_height_in_lines as f32 * window.line_height())
759 .track_scroll(self.scroll_handle.clone())
760 .with_sizing_behavior(ListSizingBehavior::Infer)
761 .w(rems(34.));
762
763 Popover::new().child(list).into_any_element()
764 }
765
766 fn render_aside(
767 &mut self,
768 max_size: Size<Pixels>,
769 window: &mut Window,
770 cx: &mut Context<Editor>,
771 ) -> Option<AnyElement> {
772 if !self.show_completion_documentation {
773 return None;
774 }
775
776 let mat = &self.entries.borrow()[self.selected_item];
777 let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
778 .documentation
779 .as_ref()?
780 {
781 CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
782 CompletionDocumentation::SingleLineAndMultiLinePlainText {
783 plain_text: Some(text),
784 ..
785 } => div().child(text.clone()),
786 CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => {
787 let (is_parsing, markdown) =
788 self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx);
789 if is_parsing {
790 return None;
791 }
792 div().child(
793 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
794 .code_block_renderer(markdown::CodeBlockRenderer::Default {
795 copy_button: false,
796 copy_button_on_hover: false,
797 border: false,
798 })
799 .on_url_click(open_markdown_url),
800 )
801 }
802 CompletionDocumentation::MultiLineMarkdown(_) => return None,
803 CompletionDocumentation::SingleLine(_) => return None,
804 CompletionDocumentation::Undocumented => return None,
805 CompletionDocumentation::SingleLineAndMultiLinePlainText {
806 plain_text: None, ..
807 } => {
808 return None;
809 }
810 };
811
812 Some(
813 Popover::new()
814 .child(
815 multiline_docs
816 .id("multiline_docs")
817 .px(MENU_ASIDE_X_PADDING / 2.)
818 .max_w(max_size.width)
819 .max_h(max_size.height)
820 .overflow_y_scroll()
821 .occlude(),
822 )
823 .into_any_element(),
824 )
825 }
826
827 pub fn sort_matches(
828 matches: &mut Vec<SortableMatch<'_>>,
829 query: Option<&str>,
830 snippet_sort_order: SnippetSortOrder,
831 ) {
832 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
833 enum MatchTier<'a> {
834 WordStartMatch {
835 sort_mixed_case_prefix_length: Reverse<usize>,
836 sort_snippet: Reverse<i32>,
837 sort_kind: usize,
838 sort_fuzzy_bracket: Reverse<usize>,
839 sort_text: Option<&'a str>,
840 sort_score: Reverse<OrderedFloat<f64>>,
841 sort_label: &'a str,
842 },
843 OtherMatch {
844 sort_score: Reverse<OrderedFloat<f64>>,
845 },
846 }
847
848 // Our goal here is to intelligently sort completion suggestions. We want to
849 // balance the raw fuzzy match score with hints from the language server
850
851 // In a fuzzy bracket, matches with a score of 1.0 are prioritized.
852 // The remaining matches are partitioned into two groups at 3/5 of the max_score.
853 let max_score = matches
854 .iter()
855 .map(|mat| mat.string_match.score)
856 .fold(0.0, f64::max);
857 let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
858
859 let query_start_lower = query
860 .and_then(|q| q.chars().next())
861 .and_then(|c| c.to_lowercase().next());
862
863 matches.sort_unstable_by_key(|mat| {
864 let score = mat.string_match.score;
865 let sort_score = Reverse(OrderedFloat(score));
866
867 let query_start_doesnt_match_split_words = query_start_lower
868 .map(|query_char| {
869 !split_words(&mat.string_match.string).any(|word| {
870 word.chars()
871 .next()
872 .and_then(|c| c.to_lowercase().next())
873 .map_or(false, |word_char| word_char == query_char)
874 })
875 })
876 .unwrap_or(false);
877
878 if query_start_doesnt_match_split_words {
879 MatchTier::OtherMatch { sort_score }
880 } else {
881 let sort_fuzzy_bracket = Reverse(if score >= fuzzy_bracket_threshold {
882 1
883 } else {
884 0
885 });
886 let sort_snippet = match snippet_sort_order {
887 SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
888 SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
889 SnippetSortOrder::Inline => Reverse(0),
890 };
891 let sort_mixed_case_prefix_length = Reverse(
892 query
893 .map(|q| {
894 q.chars()
895 .zip(mat.string_match.string.chars())
896 .enumerate()
897 .take_while(|(i, (q_char, match_char))| {
898 if *i == 0 {
899 // Case-sensitive comparison for first character
900 q_char == match_char
901 } else {
902 // Case-insensitive comparison for other characters
903 q_char.to_lowercase().eq(match_char.to_lowercase())
904 }
905 })
906 .count()
907 })
908 .unwrap_or(0),
909 );
910 MatchTier::WordStartMatch {
911 sort_mixed_case_prefix_length,
912 sort_snippet,
913 sort_kind: mat.sort_kind,
914 sort_fuzzy_bracket,
915 sort_text: mat.sort_text,
916 sort_score,
917 sort_label: mat.sort_label,
918 }
919 }
920 });
921 }
922
923 pub async fn filter(
924 &mut self,
925 query: Option<&str>,
926 provider: Option<Rc<dyn CompletionProvider>>,
927 editor: WeakEntity<Editor>,
928 cx: &mut AsyncWindowContext,
929 ) {
930 let mut matches = if let Some(query) = query {
931 fuzzy::match_strings(
932 &self.match_candidates,
933 query,
934 query.chars().any(|c| c.is_uppercase()),
935 100,
936 &Default::default(),
937 cx.background_executor().clone(),
938 )
939 .await
940 } else {
941 self.match_candidates
942 .iter()
943 .enumerate()
944 .map(|(candidate_id, candidate)| StringMatch {
945 candidate_id,
946 score: Default::default(),
947 positions: Default::default(),
948 string: candidate.string.clone(),
949 })
950 .collect()
951 };
952
953 if self.sort_completions {
954 let completions = self.completions.borrow();
955
956 let mut sortable_items: Vec<SortableMatch<'_>> = matches
957 .into_iter()
958 .map(|string_match| {
959 let completion = &completions[string_match.candidate_id];
960
961 let is_snippet = matches!(
962 &completion.source,
963 CompletionSource::Lsp { lsp_completion, .. }
964 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
965 );
966
967 let sort_text =
968 if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
969 lsp_completion.sort_text.as_deref()
970 } else {
971 None
972 };
973
974 let (sort_kind, sort_label) = completion.sort_key();
975
976 SortableMatch {
977 string_match,
978 is_snippet,
979 sort_text,
980 sort_kind,
981 sort_label,
982 }
983 })
984 .collect();
985
986 Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
987
988 matches = sortable_items
989 .into_iter()
990 .map(|sortable| sortable.string_match)
991 .collect();
992 }
993
994 *self.entries.borrow_mut() = matches;
995 self.selected_item = 0;
996 // This keeps the display consistent when y_flipped.
997 self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
998
999 if let Some(provider) = provider {
1000 cx.update(|window, cx| {
1001 // Since this is async, it's possible the menu has been closed and possibly even
1002 // another opened. `provider.selection_changed` should not be called in this case.
1003 let this_menu_still_active = editor
1004 .read_with(cx, |editor, _cx| {
1005 editor.with_completions_menu_matching_id(self.id, || false, |_| true)
1006 })
1007 .unwrap_or(false);
1008 if this_menu_still_active {
1009 self.handle_selection_changed(&*provider, window, cx);
1010 }
1011 })
1012 .ok();
1013 }
1014 }
1015}
1016
1017#[derive(Debug)]
1018pub struct SortableMatch<'a> {
1019 pub string_match: StringMatch,
1020 pub is_snippet: bool,
1021 pub sort_text: Option<&'a str>,
1022 pub sort_kind: usize,
1023 pub sort_label: &'a str,
1024}
1025
1026#[derive(Clone)]
1027pub struct AvailableCodeAction {
1028 pub excerpt_id: ExcerptId,
1029 pub action: CodeAction,
1030 pub provider: Rc<dyn CodeActionProvider>,
1031}
1032
1033#[derive(Clone)]
1034pub struct CodeActionContents {
1035 tasks: Option<Rc<ResolvedTasks>>,
1036 actions: Option<Rc<[AvailableCodeAction]>>,
1037 debug_scenarios: Vec<DebugScenario>,
1038 pub(crate) context: TaskContext,
1039}
1040
1041impl CodeActionContents {
1042 pub(crate) fn new(
1043 tasks: Option<ResolvedTasks>,
1044 actions: Option<Rc<[AvailableCodeAction]>>,
1045 debug_scenarios: Vec<DebugScenario>,
1046 context: TaskContext,
1047 ) -> Self {
1048 Self {
1049 tasks: tasks.map(Rc::new),
1050 actions,
1051 debug_scenarios,
1052 context,
1053 }
1054 }
1055
1056 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1057 self.tasks.as_deref()
1058 }
1059
1060 fn len(&self) -> usize {
1061 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1062 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1063 tasks_len + code_actions_len + self.debug_scenarios.len()
1064 }
1065
1066 fn is_empty(&self) -> bool {
1067 self.len() == 0
1068 }
1069
1070 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1071 self.tasks
1072 .iter()
1073 .flat_map(|tasks| {
1074 tasks
1075 .templates
1076 .iter()
1077 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1078 })
1079 .chain(self.actions.iter().flat_map(|actions| {
1080 actions.iter().map(|available| CodeActionsItem::CodeAction {
1081 excerpt_id: available.excerpt_id,
1082 action: available.action.clone(),
1083 provider: available.provider.clone(),
1084 })
1085 }))
1086 .chain(
1087 self.debug_scenarios
1088 .iter()
1089 .cloned()
1090 .map(CodeActionsItem::DebugScenario),
1091 )
1092 }
1093
1094 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1095 if let Some(tasks) = &self.tasks {
1096 if let Some((kind, task)) = tasks.templates.get(index) {
1097 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1098 } else {
1099 index -= tasks.templates.len();
1100 }
1101 }
1102 if let Some(actions) = &self.actions {
1103 if let Some(available) = actions.get(index) {
1104 return Some(CodeActionsItem::CodeAction {
1105 excerpt_id: available.excerpt_id,
1106 action: available.action.clone(),
1107 provider: available.provider.clone(),
1108 });
1109 } else {
1110 index -= actions.len();
1111 }
1112 }
1113
1114 self.debug_scenarios
1115 .get(index)
1116 .cloned()
1117 .map(CodeActionsItem::DebugScenario)
1118 }
1119}
1120
1121#[derive(Clone)]
1122pub enum CodeActionsItem {
1123 Task(TaskSourceKind, ResolvedTask),
1124 CodeAction {
1125 excerpt_id: ExcerptId,
1126 action: CodeAction,
1127 provider: Rc<dyn CodeActionProvider>,
1128 },
1129 DebugScenario(DebugScenario),
1130}
1131
1132impl CodeActionsItem {
1133 fn as_task(&self) -> Option<&ResolvedTask> {
1134 let Self::Task(_, task) = self else {
1135 return None;
1136 };
1137 Some(task)
1138 }
1139
1140 fn as_code_action(&self) -> Option<&CodeAction> {
1141 let Self::CodeAction { action, .. } = self else {
1142 return None;
1143 };
1144 Some(action)
1145 }
1146 fn as_debug_scenario(&self) -> Option<&DebugScenario> {
1147 let Self::DebugScenario(scenario) = self else {
1148 return None;
1149 };
1150 Some(scenario)
1151 }
1152
1153 pub fn label(&self) -> String {
1154 match self {
1155 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1156 Self::Task(_, task) => task.resolved_label.clone(),
1157 Self::DebugScenario(scenario) => scenario.label.to_string(),
1158 }
1159 }
1160}
1161
1162pub struct CodeActionsMenu {
1163 pub actions: CodeActionContents,
1164 pub buffer: Entity<Buffer>,
1165 pub selected_item: usize,
1166 pub scroll_handle: UniformListScrollHandle,
1167 pub deployed_from: Option<CodeActionSource>,
1168}
1169
1170impl CodeActionsMenu {
1171 fn select_first(&mut self, cx: &mut Context<Editor>) {
1172 self.selected_item = if self.scroll_handle.y_flipped() {
1173 self.actions.len() - 1
1174 } else {
1175 0
1176 };
1177 self.scroll_handle
1178 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1179 cx.notify()
1180 }
1181
1182 fn select_last(&mut self, cx: &mut Context<Editor>) {
1183 self.selected_item = if self.scroll_handle.y_flipped() {
1184 0
1185 } else {
1186 self.actions.len() - 1
1187 };
1188 self.scroll_handle
1189 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1190 cx.notify()
1191 }
1192
1193 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1194 self.selected_item = if self.scroll_handle.y_flipped() {
1195 self.next_match_index()
1196 } else {
1197 self.prev_match_index()
1198 };
1199 self.scroll_handle
1200 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1201 cx.notify();
1202 }
1203
1204 fn select_next(&mut self, cx: &mut Context<Editor>) {
1205 self.selected_item = if self.scroll_handle.y_flipped() {
1206 self.prev_match_index()
1207 } else {
1208 self.next_match_index()
1209 };
1210 self.scroll_handle
1211 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1212 cx.notify();
1213 }
1214
1215 fn prev_match_index(&self) -> usize {
1216 if self.selected_item > 0 {
1217 self.selected_item - 1
1218 } else {
1219 self.actions.len() - 1
1220 }
1221 }
1222
1223 fn next_match_index(&self) -> usize {
1224 if self.selected_item + 1 < self.actions.len() {
1225 self.selected_item + 1
1226 } else {
1227 0
1228 }
1229 }
1230
1231 fn visible(&self) -> bool {
1232 !self.actions.is_empty()
1233 }
1234
1235 fn origin(&self) -> ContextMenuOrigin {
1236 match &self.deployed_from {
1237 Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),
1238 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1239 None => ContextMenuOrigin::Cursor,
1240 }
1241 }
1242
1243 fn render(
1244 &self,
1245 _style: &EditorStyle,
1246 max_height_in_lines: u32,
1247 window: &mut Window,
1248 cx: &mut Context<Editor>,
1249 ) -> AnyElement {
1250 let actions = self.actions.clone();
1251 let selected_item = self.selected_item;
1252 let list = uniform_list(
1253 cx.entity().clone(),
1254 "code_actions_menu",
1255 self.actions.len(),
1256 move |_this, range, _, cx| {
1257 actions
1258 .iter()
1259 .skip(range.start)
1260 .take(range.end - range.start)
1261 .enumerate()
1262 .map(|(ix, action)| {
1263 let item_ix = range.start + ix;
1264 let selected = item_ix == selected_item;
1265 let colors = cx.theme().colors();
1266 div().min_w(px(220.)).max_w(px(540.)).child(
1267 ListItem::new(item_ix)
1268 .inset(true)
1269 .toggle_state(selected)
1270 .when_some(action.as_code_action(), |this, action| {
1271 this.child(
1272 h_flex()
1273 .overflow_hidden()
1274 .child(
1275 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1276 action.lsp_action.title().replace("\n", ""),
1277 )
1278 .when(selected, |this| {
1279 this.text_color(colors.text_accent)
1280 }),
1281 )
1282 })
1283 .when_some(action.as_task(), |this, task| {
1284 this.child(
1285 h_flex()
1286 .overflow_hidden()
1287 .child(task.resolved_label.replace("\n", ""))
1288 .when(selected, |this| {
1289 this.text_color(colors.text_accent)
1290 }),
1291 )
1292 })
1293 .when_some(action.as_debug_scenario(), |this, scenario| {
1294 this.child(
1295 h_flex()
1296 .overflow_hidden()
1297 .child("debug: ")
1298 .child(scenario.label.clone())
1299 .when(selected, |this| {
1300 this.text_color(colors.text_accent)
1301 }),
1302 )
1303 })
1304 .on_click(cx.listener(move |editor, _, window, cx| {
1305 cx.stop_propagation();
1306 if let Some(task) = editor.confirm_code_action(
1307 &ConfirmCodeAction {
1308 item_ix: Some(item_ix),
1309 },
1310 window,
1311 cx,
1312 ) {
1313 task.detach_and_log_err(cx)
1314 }
1315 })),
1316 )
1317 })
1318 .collect()
1319 },
1320 )
1321 .occlude()
1322 .max_h(max_height_in_lines as f32 * window.line_height())
1323 .track_scroll(self.scroll_handle.clone())
1324 .with_width_from_item(
1325 self.actions
1326 .iter()
1327 .enumerate()
1328 .max_by_key(|(_, action)| match action {
1329 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1330 CodeActionsItem::CodeAction { action, .. } => {
1331 action.lsp_action.title().chars().count()
1332 }
1333 CodeActionsItem::DebugScenario(scenario) => {
1334 format!("debug: {}", scenario.label).chars().count()
1335 }
1336 })
1337 .map(|(ix, _)| ix),
1338 )
1339 .with_sizing_behavior(ListSizingBehavior::Infer);
1340
1341 Popover::new().child(list).into_any_element()
1342 }
1343}