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