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