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