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