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 let markdown = 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 // Handles redraw when the markdown is done parsing. The current render is for a
661 // deferred draw and so was not getting redrawn when `markdown` notified.
662 cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
663 markdown
664 });
665 let is_parsing = markdown.update(cx, |markdown, cx| {
666 markdown.reset(parsed.clone(), cx);
667 markdown.is_parsing()
668 });
669 if is_parsing {
670 return None;
671 }
672 div().child(
673 MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
674 .code_block_renderer(markdown::CodeBlockRenderer::Default {
675 copy_button: false,
676 copy_button_on_hover: false,
677 border: false,
678 })
679 .on_url_click(open_markdown_url),
680 )
681 }
682 CompletionDocumentation::MultiLineMarkdown(_) => return None,
683 CompletionDocumentation::SingleLine(_) => return None,
684 CompletionDocumentation::Undocumented => return None,
685 CompletionDocumentation::SingleLineAndMultiLinePlainText {
686 plain_text: None, ..
687 } => {
688 return None;
689 }
690 };
691
692 Some(
693 Popover::new()
694 .child(
695 multiline_docs
696 .id("multiline_docs")
697 .px(MENU_ASIDE_X_PADDING / 2.)
698 .max_w(max_size.width)
699 .max_h(max_size.height)
700 .overflow_y_scroll()
701 .occlude(),
702 )
703 .into_any_element(),
704 )
705 }
706
707 pub fn sort_matches(
708 matches: &mut Vec<SortableMatch<'_>>,
709 query: Option<&str>,
710 snippet_sort_order: SnippetSortOrder,
711 ) {
712 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
713 enum MatchTier<'a> {
714 WordStartMatch {
715 sort_mixed_case_prefix_length: Reverse<usize>,
716 sort_snippet: Reverse<i32>,
717 sort_kind: usize,
718 sort_fuzzy_bracket: Reverse<usize>,
719 sort_text: Option<&'a str>,
720 sort_score: Reverse<OrderedFloat<f64>>,
721 sort_label: &'a str,
722 },
723 OtherMatch {
724 sort_score: Reverse<OrderedFloat<f64>>,
725 },
726 }
727
728 // Our goal here is to intelligently sort completion suggestions. We want to
729 // balance the raw fuzzy match score with hints from the language server
730
731 // In a fuzzy bracket, matches with a score of 1.0 are prioritized.
732 // The remaining matches are partitioned into two groups at 3/5 of the max_score.
733 let max_score = matches
734 .iter()
735 .map(|mat| mat.string_match.score)
736 .fold(0.0, f64::max);
737 let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
738
739 let query_start_lower = query
740 .and_then(|q| q.chars().next())
741 .and_then(|c| c.to_lowercase().next());
742
743 matches.sort_unstable_by_key(|mat| {
744 let score = mat.string_match.score;
745 let sort_score = Reverse(OrderedFloat(score));
746
747 let query_start_doesnt_match_split_words = query_start_lower
748 .map(|query_char| {
749 !split_words(&mat.string_match.string).any(|word| {
750 word.chars()
751 .next()
752 .and_then(|c| c.to_lowercase().next())
753 .map_or(false, |word_char| word_char == query_char)
754 })
755 })
756 .unwrap_or(false);
757
758 if query_start_doesnt_match_split_words {
759 MatchTier::OtherMatch { sort_score }
760 } else {
761 let sort_fuzzy_bracket = Reverse(if score >= fuzzy_bracket_threshold {
762 1
763 } else {
764 0
765 });
766 let sort_snippet = match snippet_sort_order {
767 SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
768 SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
769 SnippetSortOrder::Inline => Reverse(0),
770 };
771 let sort_mixed_case_prefix_length = Reverse(
772 query
773 .map(|q| {
774 q.chars()
775 .zip(mat.string_match.string.chars())
776 .enumerate()
777 .take_while(|(i, (q_char, match_char))| {
778 if *i == 0 {
779 // Case-sensitive comparison for first character
780 q_char == match_char
781 } else {
782 // Case-insensitive comparison for other characters
783 q_char.to_lowercase().eq(match_char.to_lowercase())
784 }
785 })
786 .count()
787 })
788 .unwrap_or(0),
789 );
790 MatchTier::WordStartMatch {
791 sort_mixed_case_prefix_length,
792 sort_snippet,
793 sort_kind: mat.sort_kind,
794 sort_fuzzy_bracket,
795 sort_text: mat.sort_text,
796 sort_score,
797 sort_label: mat.sort_label,
798 }
799 }
800 });
801 }
802
803 pub async fn filter(
804 &mut self,
805 query: Option<&str>,
806 provider: Option<Rc<dyn CompletionProvider>>,
807 editor: WeakEntity<Editor>,
808 cx: &mut AsyncWindowContext,
809 ) {
810 let mut matches = if let Some(query) = query {
811 fuzzy::match_strings(
812 &self.match_candidates,
813 query,
814 query.chars().any(|c| c.is_uppercase()),
815 100,
816 &Default::default(),
817 cx.background_executor().clone(),
818 )
819 .await
820 } else {
821 self.match_candidates
822 .iter()
823 .enumerate()
824 .map(|(candidate_id, candidate)| StringMatch {
825 candidate_id,
826 score: Default::default(),
827 positions: Default::default(),
828 string: candidate.string.clone(),
829 })
830 .collect()
831 };
832
833 if self.sort_completions {
834 let completions = self.completions.borrow();
835
836 let mut sortable_items: Vec<SortableMatch<'_>> = matches
837 .into_iter()
838 .map(|string_match| {
839 let completion = &completions[string_match.candidate_id];
840
841 let is_snippet = matches!(
842 &completion.source,
843 CompletionSource::Lsp { lsp_completion, .. }
844 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
845 );
846
847 let sort_text =
848 if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
849 lsp_completion.sort_text.as_deref()
850 } else {
851 None
852 };
853
854 let (sort_kind, sort_label) = completion.sort_key();
855
856 SortableMatch {
857 string_match,
858 is_snippet,
859 sort_text,
860 sort_kind,
861 sort_label,
862 }
863 })
864 .collect();
865
866 Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
867
868 matches = sortable_items
869 .into_iter()
870 .map(|sortable| sortable.string_match)
871 .collect();
872 }
873
874 *self.entries.borrow_mut() = matches;
875 self.selected_item = 0;
876 // This keeps the display consistent when y_flipped.
877 self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
878
879 if let Some(provider) = provider {
880 cx.update(|window, cx| {
881 // Since this is async, it's possible the menu has been closed and possibly even
882 // another opened. `provider.selection_changed` should not be called in this case.
883 let this_menu_still_active = editor
884 .read_with(cx, |editor, _cx| {
885 if let Some(CodeContextMenu::Completions(completions_menu)) =
886 editor.context_menu.borrow().as_ref()
887 {
888 completions_menu.id == self.id
889 } else {
890 false
891 }
892 })
893 .unwrap_or(false);
894 if this_menu_still_active {
895 self.handle_selection_changed(&*provider, window, cx);
896 }
897 })
898 .ok();
899 }
900 }
901}
902
903#[derive(Debug)]
904pub struct SortableMatch<'a> {
905 pub string_match: StringMatch,
906 pub is_snippet: bool,
907 pub sort_text: Option<&'a str>,
908 pub sort_kind: usize,
909 pub sort_label: &'a str,
910}
911
912#[derive(Clone)]
913pub struct AvailableCodeAction {
914 pub excerpt_id: ExcerptId,
915 pub action: CodeAction,
916 pub provider: Rc<dyn CodeActionProvider>,
917}
918
919#[derive(Clone)]
920pub struct CodeActionContents {
921 tasks: Option<Rc<ResolvedTasks>>,
922 actions: Option<Rc<[AvailableCodeAction]>>,
923 debug_scenarios: Vec<DebugScenario>,
924 pub(crate) context: TaskContext,
925}
926
927impl CodeActionContents {
928 pub(crate) fn new(
929 tasks: Option<ResolvedTasks>,
930 actions: Option<Rc<[AvailableCodeAction]>>,
931 debug_scenarios: Vec<DebugScenario>,
932 context: TaskContext,
933 ) -> Self {
934 Self {
935 tasks: tasks.map(Rc::new),
936 actions,
937 debug_scenarios,
938 context,
939 }
940 }
941
942 pub fn tasks(&self) -> Option<&ResolvedTasks> {
943 self.tasks.as_deref()
944 }
945
946 fn len(&self) -> usize {
947 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
948 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
949 tasks_len + code_actions_len + self.debug_scenarios.len()
950 }
951
952 fn is_empty(&self) -> bool {
953 self.len() == 0
954 }
955
956 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
957 self.tasks
958 .iter()
959 .flat_map(|tasks| {
960 tasks
961 .templates
962 .iter()
963 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
964 })
965 .chain(self.actions.iter().flat_map(|actions| {
966 actions.iter().map(|available| CodeActionsItem::CodeAction {
967 excerpt_id: available.excerpt_id,
968 action: available.action.clone(),
969 provider: available.provider.clone(),
970 })
971 }))
972 .chain(
973 self.debug_scenarios
974 .iter()
975 .cloned()
976 .map(CodeActionsItem::DebugScenario),
977 )
978 }
979
980 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
981 if let Some(tasks) = &self.tasks {
982 if let Some((kind, task)) = tasks.templates.get(index) {
983 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
984 } else {
985 index -= tasks.templates.len();
986 }
987 }
988 if let Some(actions) = &self.actions {
989 if let Some(available) = actions.get(index) {
990 return Some(CodeActionsItem::CodeAction {
991 excerpt_id: available.excerpt_id,
992 action: available.action.clone(),
993 provider: available.provider.clone(),
994 });
995 } else {
996 index -= actions.len();
997 }
998 }
999
1000 self.debug_scenarios
1001 .get(index)
1002 .cloned()
1003 .map(CodeActionsItem::DebugScenario)
1004 }
1005}
1006
1007#[derive(Clone)]
1008pub enum CodeActionsItem {
1009 Task(TaskSourceKind, ResolvedTask),
1010 CodeAction {
1011 excerpt_id: ExcerptId,
1012 action: CodeAction,
1013 provider: Rc<dyn CodeActionProvider>,
1014 },
1015 DebugScenario(DebugScenario),
1016}
1017
1018impl CodeActionsItem {
1019 fn as_task(&self) -> Option<&ResolvedTask> {
1020 let Self::Task(_, task) = self else {
1021 return None;
1022 };
1023 Some(task)
1024 }
1025
1026 fn as_code_action(&self) -> Option<&CodeAction> {
1027 let Self::CodeAction { action, .. } = self else {
1028 return None;
1029 };
1030 Some(action)
1031 }
1032 fn as_debug_scenario(&self) -> Option<&DebugScenario> {
1033 let Self::DebugScenario(scenario) = self else {
1034 return None;
1035 };
1036 Some(scenario)
1037 }
1038
1039 pub fn label(&self) -> String {
1040 match self {
1041 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1042 Self::Task(_, task) => task.resolved_label.clone(),
1043 Self::DebugScenario(scenario) => scenario.label.to_string(),
1044 }
1045 }
1046}
1047
1048pub struct CodeActionsMenu {
1049 pub actions: CodeActionContents,
1050 pub buffer: Entity<Buffer>,
1051 pub selected_item: usize,
1052 pub scroll_handle: UniformListScrollHandle,
1053 pub deployed_from: Option<CodeActionSource>,
1054}
1055
1056impl CodeActionsMenu {
1057 fn select_first(&mut self, cx: &mut Context<Editor>) {
1058 self.selected_item = if self.scroll_handle.y_flipped() {
1059 self.actions.len() - 1
1060 } else {
1061 0
1062 };
1063 self.scroll_handle
1064 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1065 cx.notify()
1066 }
1067
1068 fn select_last(&mut self, cx: &mut Context<Editor>) {
1069 self.selected_item = if self.scroll_handle.y_flipped() {
1070 0
1071 } else {
1072 self.actions.len() - 1
1073 };
1074 self.scroll_handle
1075 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1076 cx.notify()
1077 }
1078
1079 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1080 self.selected_item = if self.scroll_handle.y_flipped() {
1081 self.next_match_index()
1082 } else {
1083 self.prev_match_index()
1084 };
1085 self.scroll_handle
1086 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1087 cx.notify();
1088 }
1089
1090 fn select_next(&mut self, cx: &mut Context<Editor>) {
1091 self.selected_item = if self.scroll_handle.y_flipped() {
1092 self.prev_match_index()
1093 } else {
1094 self.next_match_index()
1095 };
1096 self.scroll_handle
1097 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1098 cx.notify();
1099 }
1100
1101 fn prev_match_index(&self) -> usize {
1102 if self.selected_item > 0 {
1103 self.selected_item - 1
1104 } else {
1105 self.actions.len() - 1
1106 }
1107 }
1108
1109 fn next_match_index(&self) -> usize {
1110 if self.selected_item + 1 < self.actions.len() {
1111 self.selected_item + 1
1112 } else {
1113 0
1114 }
1115 }
1116
1117 fn visible(&self) -> bool {
1118 !self.actions.is_empty()
1119 }
1120
1121 fn origin(&self) -> ContextMenuOrigin {
1122 match &self.deployed_from {
1123 Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),
1124 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1125 None => ContextMenuOrigin::Cursor,
1126 }
1127 }
1128
1129 fn render(
1130 &self,
1131 _style: &EditorStyle,
1132 max_height_in_lines: u32,
1133 window: &mut Window,
1134 cx: &mut Context<Editor>,
1135 ) -> AnyElement {
1136 let actions = self.actions.clone();
1137 let selected_item = self.selected_item;
1138 let list = uniform_list(
1139 cx.entity().clone(),
1140 "code_actions_menu",
1141 self.actions.len(),
1142 move |_this, range, _, cx| {
1143 actions
1144 .iter()
1145 .skip(range.start)
1146 .take(range.end - range.start)
1147 .enumerate()
1148 .map(|(ix, action)| {
1149 let item_ix = range.start + ix;
1150 let selected = item_ix == selected_item;
1151 let colors = cx.theme().colors();
1152 div().min_w(px(220.)).max_w(px(540.)).child(
1153 ListItem::new(item_ix)
1154 .inset(true)
1155 .toggle_state(selected)
1156 .when_some(action.as_code_action(), |this, action| {
1157 this.child(
1158 h_flex()
1159 .overflow_hidden()
1160 .child(
1161 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1162 action.lsp_action.title().replace("\n", ""),
1163 )
1164 .when(selected, |this| {
1165 this.text_color(colors.text_accent)
1166 }),
1167 )
1168 })
1169 .when_some(action.as_task(), |this, task| {
1170 this.child(
1171 h_flex()
1172 .overflow_hidden()
1173 .child(task.resolved_label.replace("\n", ""))
1174 .when(selected, |this| {
1175 this.text_color(colors.text_accent)
1176 }),
1177 )
1178 })
1179 .when_some(action.as_debug_scenario(), |this, scenario| {
1180 this.child(
1181 h_flex()
1182 .overflow_hidden()
1183 .child("debug: ")
1184 .child(scenario.label.clone())
1185 .when(selected, |this| {
1186 this.text_color(colors.text_accent)
1187 }),
1188 )
1189 })
1190 .on_click(cx.listener(move |editor, _, window, cx| {
1191 cx.stop_propagation();
1192 if let Some(task) = editor.confirm_code_action(
1193 &ConfirmCodeAction {
1194 item_ix: Some(item_ix),
1195 },
1196 window,
1197 cx,
1198 ) {
1199 task.detach_and_log_err(cx)
1200 }
1201 })),
1202 )
1203 })
1204 .collect()
1205 },
1206 )
1207 .occlude()
1208 .max_h(max_height_in_lines as f32 * window.line_height())
1209 .track_scroll(self.scroll_handle.clone())
1210 .with_width_from_item(
1211 self.actions
1212 .iter()
1213 .enumerate()
1214 .max_by_key(|(_, action)| match action {
1215 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1216 CodeActionsItem::CodeAction { action, .. } => {
1217 action.lsp_action.title().chars().count()
1218 }
1219 CodeActionsItem::DebugScenario(scenario) => {
1220 format!("debug: {}", scenario.label).chars().count()
1221 }
1222 })
1223 .map(|(ix, _)| ix),
1224 )
1225 .with_sizing_behavior(ListSizingBehavior::Infer);
1226
1227 Popover::new().child(list).into_any_element()
1228 }
1229}