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, CompletionDocumentation};
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(CompletionDocumentation::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.entity().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(CompletionDocumentation::SingleLine(text)) =
562 documentation
563 {
564 if text.trim().is_empty() {
565 None
566 } else {
567 Some(
568 Label::new(text.clone())
569 .ml_4()
570 .size(LabelSize::Small)
571 .color(Color::Muted),
572 )
573 }
574 } else {
575 None
576 };
577
578 let color_swatch = completion
579 .color()
580 .map(|color| div().size_4().bg(color).rounded_sm());
581
582 div().min_w(px(220.)).max_w(px(540.)).child(
583 ListItem::new(mat.candidate_id)
584 .inset(true)
585 .toggle_state(item_ix == selected_item)
586 .on_click(cx.listener(move |editor, _event, window, cx| {
587 cx.stop_propagation();
588 if let Some(task) = editor.confirm_completion(
589 &ConfirmCompletion {
590 item_ix: Some(item_ix),
591 },
592 window,
593 cx,
594 ) {
595 task.detach_and_log_err(cx)
596 }
597 }))
598 .start_slot::<Div>(color_swatch)
599 .child(h_flex().overflow_hidden().child(completion_label))
600 .end_slot::<Label>(documentation_label),
601 )
602 }
603 CompletionEntry::InlineCompletionHint(
604 hint @ InlineCompletionMenuHint::None,
605 ) => div().min_w(px(250.)).max_w(px(500.)).child(
606 ListItem::new("inline-completion")
607 .inset(true)
608 .toggle_state(item_ix == selected_item)
609 .start_slot(Icon::new(IconName::ZedPredict))
610 .child(
611 base_label.child(
612 StyledText::new(hint.label())
613 .with_highlights(&style.text, None),
614 ),
615 ),
616 ),
617 CompletionEntry::InlineCompletionHint(
618 hint @ InlineCompletionMenuHint::Loading,
619 ) => div().min_w(px(250.)).max_w(px(500.)).child(
620 ListItem::new("inline-completion")
621 .inset(true)
622 .toggle_state(item_ix == selected_item)
623 .start_slot(Icon::new(IconName::ZedPredict))
624 .child(base_label.child({
625 let text_style = style.text.clone();
626 StyledText::new(hint.label())
627 .with_highlights(&text_style, None)
628 .with_animation(
629 "pulsating-label",
630 Animation::new(Duration::from_secs(1))
631 .repeat()
632 .with_easing(pulsating_between(0.4, 0.8)),
633 move |text, delta| {
634 let mut text_style = text_style.clone();
635 text_style.color =
636 text_style.color.opacity(delta);
637 text.with_highlights(&text_style, None)
638 },
639 )
640 })),
641 ),
642 CompletionEntry::InlineCompletionHint(
643 hint @ InlineCompletionMenuHint::PendingTermsAcceptance,
644 ) => div().min_w(px(250.)).max_w(px(500.)).child(
645 ListItem::new("inline-completion")
646 .inset(true)
647 .toggle_state(item_ix == selected_item)
648 .start_slot(Icon::new(IconName::ZedPredict))
649 .child(
650 base_label.child(
651 StyledText::new(hint.label())
652 .with_highlights(&style.text, None),
653 ),
654 )
655 .on_click(cx.listener(move |editor, _event, window, cx| {
656 cx.stop_propagation();
657 editor.toggle_zed_predict_onboarding(window, cx);
658 })),
659 ),
660
661 CompletionEntry::InlineCompletionHint(
662 hint @ InlineCompletionMenuHint::Loaded { .. },
663 ) => div().min_w(px(250.)).max_w(px(500.)).child(
664 ListItem::new("inline-completion")
665 .inset(true)
666 .toggle_state(item_ix == selected_item)
667 .start_slot(Icon::new(IconName::ZedPredict))
668 .child(
669 base_label.child(
670 StyledText::new(hint.label())
671 .with_highlights(&style.text, None),
672 ),
673 )
674 .on_click(cx.listener(move |editor, _event, window, cx| {
675 cx.stop_propagation();
676 editor.accept_inline_completion(
677 &AcceptInlineCompletion {},
678 window,
679 cx,
680 );
681 })),
682 ),
683 }
684 })
685 .collect()
686 },
687 )
688 .occlude()
689 .max_h(max_height_in_lines as f32 * window.line_height())
690 .track_scroll(self.scroll_handle.clone())
691 .y_flipped(y_flipped)
692 .with_width_from_item(widest_completion_ix)
693 .with_sizing_behavior(ListSizingBehavior::Infer);
694
695 Popover::new().child(list).into_any_element()
696 }
697
698 fn render_aside(
699 &self,
700 style: &EditorStyle,
701 max_size: Size<Pixels>,
702 workspace: Option<WeakEntity<Workspace>>,
703 cx: &mut Context<Editor>,
704 ) -> Option<AnyElement> {
705 if !self.show_completion_documentation {
706 return None;
707 }
708
709 let multiline_docs = match &self.entries.borrow()[self.selected_item] {
710 CompletionEntry::Match(mat) => {
711 match self.completions.borrow_mut()[mat.candidate_id]
712 .documentation
713 .as_ref()?
714 {
715 CompletionDocumentation::MultiLinePlainText(text) => {
716 div().child(SharedString::from(text.clone()))
717 }
718 CompletionDocumentation::MultiLineMarkdown(parsed)
719 if !parsed.text.is_empty() =>
720 {
721 div().child(render_parsed_markdown(
722 "completions_markdown",
723 parsed,
724 &style,
725 workspace,
726 cx,
727 ))
728 }
729 CompletionDocumentation::MultiLineMarkdown(_) => return None,
730 CompletionDocumentation::SingleLine(_) => return None,
731 CompletionDocumentation::Undocumented => return None,
732 }
733 }
734 CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
735 match text {
736 InlineCompletionText::Edit(highlighted_edits) => div()
737 .mx_1()
738 .rounded_md()
739 .bg(cx.theme().colors().editor_background)
740 .child(
741 gpui::StyledText::new(highlighted_edits.text.clone())
742 .with_highlights(&style.text, highlighted_edits.highlights.clone()),
743 ),
744 InlineCompletionText::Move(text) => div().child(text.clone()),
745 }
746 }
747 CompletionEntry::InlineCompletionHint(_) => return None,
748 };
749
750 Some(
751 Popover::new()
752 .child(
753 multiline_docs
754 .id("multiline_docs")
755 .px(MENU_ASIDE_X_PADDING / 2.)
756 .max_w(max_size.width)
757 .max_h(max_size.height)
758 .overflow_y_scroll()
759 .occlude(),
760 )
761 .into_any_element(),
762 )
763 }
764
765 pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
766 let inline_completion_was_selected = self.selected_item == 0
767 && self.entries.borrow().first().map_or(false, |entry| {
768 matches!(entry, CompletionEntry::InlineCompletionHint(_))
769 });
770
771 let mut matches = if let Some(query) = query {
772 fuzzy::match_strings(
773 &self.match_candidates,
774 query,
775 query.chars().any(|c| c.is_uppercase()),
776 100,
777 &Default::default(),
778 executor,
779 )
780 .await
781 } else {
782 self.match_candidates
783 .iter()
784 .enumerate()
785 .map(|(candidate_id, candidate)| StringMatch {
786 candidate_id,
787 score: Default::default(),
788 positions: Default::default(),
789 string: candidate.string.clone(),
790 })
791 .collect()
792 };
793
794 // Remove all candidates where the query's start does not match the start of any word in the candidate
795 if let Some(query) = query {
796 if let Some(query_start) = query.chars().next() {
797 matches.retain(|string_match| {
798 split_words(&string_match.string).any(|word| {
799 // Check that the first codepoint of the word as lowercase matches the first
800 // codepoint of the query as lowercase
801 word.chars()
802 .flat_map(|codepoint| codepoint.to_lowercase())
803 .zip(query_start.to_lowercase())
804 .all(|(word_cp, query_cp)| word_cp == query_cp)
805 })
806 });
807 }
808 }
809
810 let completions = self.completions.borrow_mut();
811 if self.sort_completions {
812 matches.sort_unstable_by_key(|mat| {
813 // We do want to strike a balance here between what the language server tells us
814 // to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
815 // `Creat` and there is a local variable called `CreateComponent`).
816 // So what we do is: we bucket all matches into two buckets
817 // - Strong matches
818 // - Weak matches
819 // Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
820 // and the Weak matches are the rest.
821 //
822 // For the strong matches, we sort by our fuzzy-finder score first and for the weak
823 // matches, we prefer language-server sort_text first.
824 //
825 // The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
826 // Rest of the matches(weak) can be sorted as language-server expects.
827
828 #[derive(PartialEq, Eq, PartialOrd, Ord)]
829 enum MatchScore<'a> {
830 Strong {
831 score: Reverse<OrderedFloat<f64>>,
832 sort_text: Option<&'a str>,
833 sort_key: (usize, &'a str),
834 },
835 Weak {
836 sort_text: Option<&'a str>,
837 score: Reverse<OrderedFloat<f64>>,
838 sort_key: (usize, &'a str),
839 },
840 }
841
842 let completion = &completions[mat.candidate_id];
843 let sort_key = completion.sort_key();
844 let sort_text = completion.lsp_completion.sort_text.as_deref();
845 let score = Reverse(OrderedFloat(mat.score));
846
847 if mat.score >= 0.2 {
848 MatchScore::Strong {
849 score,
850 sort_text,
851 sort_key,
852 }
853 } else {
854 MatchScore::Weak {
855 sort_text,
856 score,
857 sort_key,
858 }
859 }
860 });
861 }
862 drop(completions);
863
864 let mut entries = self.entries.borrow_mut();
865 let new_selection = if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first()
866 {
867 entries.truncate(1);
868 if inline_completion_was_selected || matches.is_empty() {
869 0
870 } else {
871 1
872 }
873 } else {
874 entries.truncate(0);
875 0
876 };
877 entries.extend(matches.into_iter().map(CompletionEntry::Match));
878 self.selected_item = new_selection;
879 // Scroll to 0 even if the LSP completion is the only one selected. This keeps the display
880 // consistent when y_flipped.
881 self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
882 }
883}
884
885#[derive(Clone)]
886pub struct AvailableCodeAction {
887 pub excerpt_id: ExcerptId,
888 pub action: CodeAction,
889 pub provider: Rc<dyn CodeActionProvider>,
890}
891
892#[derive(Clone)]
893pub struct CodeActionContents {
894 pub tasks: Option<Rc<ResolvedTasks>>,
895 pub actions: Option<Rc<[AvailableCodeAction]>>,
896}
897
898impl CodeActionContents {
899 fn len(&self) -> usize {
900 match (&self.tasks, &self.actions) {
901 (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
902 (Some(tasks), None) => tasks.templates.len(),
903 (None, Some(actions)) => actions.len(),
904 (None, None) => 0,
905 }
906 }
907
908 fn is_empty(&self) -> bool {
909 match (&self.tasks, &self.actions) {
910 (Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
911 (Some(tasks), None) => tasks.templates.is_empty(),
912 (None, Some(actions)) => actions.is_empty(),
913 (None, None) => true,
914 }
915 }
916
917 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
918 self.tasks
919 .iter()
920 .flat_map(|tasks| {
921 tasks
922 .templates
923 .iter()
924 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
925 })
926 .chain(self.actions.iter().flat_map(|actions| {
927 actions.iter().map(|available| CodeActionsItem::CodeAction {
928 excerpt_id: available.excerpt_id,
929 action: available.action.clone(),
930 provider: available.provider.clone(),
931 })
932 }))
933 }
934
935 pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
936 match (&self.tasks, &self.actions) {
937 (Some(tasks), Some(actions)) => {
938 if index < tasks.templates.len() {
939 tasks
940 .templates
941 .get(index)
942 .cloned()
943 .map(|(kind, task)| CodeActionsItem::Task(kind, task))
944 } else {
945 actions.get(index - tasks.templates.len()).map(|available| {
946 CodeActionsItem::CodeAction {
947 excerpt_id: available.excerpt_id,
948 action: available.action.clone(),
949 provider: available.provider.clone(),
950 }
951 })
952 }
953 }
954 (Some(tasks), None) => tasks
955 .templates
956 .get(index)
957 .cloned()
958 .map(|(kind, task)| CodeActionsItem::Task(kind, task)),
959 (None, Some(actions)) => {
960 actions
961 .get(index)
962 .map(|available| CodeActionsItem::CodeAction {
963 excerpt_id: available.excerpt_id,
964 action: available.action.clone(),
965 provider: available.provider.clone(),
966 })
967 }
968 (None, None) => None,
969 }
970 }
971}
972
973#[allow(clippy::large_enum_variant)]
974#[derive(Clone)]
975pub enum CodeActionsItem {
976 Task(TaskSourceKind, ResolvedTask),
977 CodeAction {
978 excerpt_id: ExcerptId,
979 action: CodeAction,
980 provider: Rc<dyn CodeActionProvider>,
981 },
982}
983
984impl CodeActionsItem {
985 fn as_task(&self) -> Option<&ResolvedTask> {
986 let Self::Task(_, task) = self else {
987 return None;
988 };
989 Some(task)
990 }
991
992 fn as_code_action(&self) -> Option<&CodeAction> {
993 let Self::CodeAction { action, .. } = self else {
994 return None;
995 };
996 Some(action)
997 }
998
999 pub fn label(&self) -> String {
1000 match self {
1001 Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
1002 Self::Task(_, task) => task.resolved_label.clone(),
1003 }
1004 }
1005}
1006
1007pub struct CodeActionsMenu {
1008 pub actions: CodeActionContents,
1009 pub buffer: Entity<Buffer>,
1010 pub selected_item: usize,
1011 pub scroll_handle: UniformListScrollHandle,
1012 pub deployed_from_indicator: Option<DisplayRow>,
1013}
1014
1015impl CodeActionsMenu {
1016 fn select_first(&mut self, cx: &mut Context<Editor>) {
1017 self.selected_item = if self.scroll_handle.y_flipped() {
1018 self.actions.len() - 1
1019 } else {
1020 0
1021 };
1022 self.scroll_handle
1023 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1024 cx.notify()
1025 }
1026
1027 fn select_last(&mut self, cx: &mut Context<Editor>) {
1028 self.selected_item = if self.scroll_handle.y_flipped() {
1029 0
1030 } else {
1031 self.actions.len() - 1
1032 };
1033 self.scroll_handle
1034 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1035 cx.notify()
1036 }
1037
1038 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1039 self.selected_item = if self.scroll_handle.y_flipped() {
1040 self.next_match_index()
1041 } else {
1042 self.prev_match_index()
1043 };
1044 self.scroll_handle
1045 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1046 cx.notify();
1047 }
1048
1049 fn select_next(&mut self, cx: &mut Context<Editor>) {
1050 self.selected_item = if self.scroll_handle.y_flipped() {
1051 self.prev_match_index()
1052 } else {
1053 self.next_match_index()
1054 };
1055 self.scroll_handle
1056 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1057 cx.notify();
1058 }
1059
1060 fn prev_match_index(&self) -> usize {
1061 if self.selected_item > 0 {
1062 self.selected_item - 1
1063 } else {
1064 self.actions.len() - 1
1065 }
1066 }
1067
1068 fn next_match_index(&self) -> usize {
1069 if self.selected_item + 1 < self.actions.len() {
1070 self.selected_item + 1
1071 } else {
1072 0
1073 }
1074 }
1075
1076 fn visible(&self) -> bool {
1077 !self.actions.is_empty()
1078 }
1079
1080 fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
1081 if let Some(row) = self.deployed_from_indicator {
1082 ContextMenuOrigin::GutterIndicator(row)
1083 } else {
1084 ContextMenuOrigin::EditorPoint(cursor_position)
1085 }
1086 }
1087
1088 fn render(
1089 &self,
1090 _style: &EditorStyle,
1091 max_height_in_lines: u32,
1092 y_flipped: bool,
1093 window: &mut Window,
1094 cx: &mut Context<Editor>,
1095 ) -> AnyElement {
1096 let actions = self.actions.clone();
1097 let selected_item = self.selected_item;
1098 let list = uniform_list(
1099 cx.entity().clone(),
1100 "code_actions_menu",
1101 self.actions.len(),
1102 move |_this, range, _, cx| {
1103 actions
1104 .iter()
1105 .skip(range.start)
1106 .take(range.end - range.start)
1107 .enumerate()
1108 .map(|(ix, action)| {
1109 let item_ix = range.start + ix;
1110 let selected = item_ix == selected_item;
1111 let colors = cx.theme().colors();
1112 div().min_w(px(220.)).max_w(px(540.)).child(
1113 ListItem::new(item_ix)
1114 .inset(true)
1115 .toggle_state(selected)
1116 .when_some(action.as_code_action(), |this, action| {
1117 this.on_click(cx.listener(move |editor, _, window, cx| {
1118 cx.stop_propagation();
1119 if let Some(task) = editor.confirm_code_action(
1120 &ConfirmCodeAction {
1121 item_ix: Some(item_ix),
1122 },
1123 window,
1124 cx,
1125 ) {
1126 task.detach_and_log_err(cx)
1127 }
1128 }))
1129 .child(
1130 h_flex()
1131 .overflow_hidden()
1132 .child(
1133 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1134 action.lsp_action.title.replace("\n", ""),
1135 )
1136 .when(selected, |this| {
1137 this.text_color(colors.text_accent)
1138 }),
1139 )
1140 })
1141 .when_some(action.as_task(), |this, task| {
1142 this.on_click(cx.listener(move |editor, _, window, cx| {
1143 cx.stop_propagation();
1144 if let Some(task) = editor.confirm_code_action(
1145 &ConfirmCodeAction {
1146 item_ix: Some(item_ix),
1147 },
1148 window,
1149 cx,
1150 ) {
1151 task.detach_and_log_err(cx)
1152 }
1153 }))
1154 .child(
1155 h_flex()
1156 .overflow_hidden()
1157 .child(task.resolved_label.replace("\n", ""))
1158 .when(selected, |this| {
1159 this.text_color(colors.text_accent)
1160 }),
1161 )
1162 }),
1163 )
1164 })
1165 .collect()
1166 },
1167 )
1168 .occlude()
1169 .max_h(max_height_in_lines as f32 * window.line_height())
1170 .track_scroll(self.scroll_handle.clone())
1171 .y_flipped(y_flipped)
1172 .with_width_from_item(
1173 self.actions
1174 .iter()
1175 .enumerate()
1176 .max_by_key(|(_, action)| match action {
1177 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1178 CodeActionsItem::CodeAction { action, .. } => {
1179 action.lsp_action.title.chars().count()
1180 }
1181 })
1182 .map(|(ix, _)| ix),
1183 )
1184 .with_sizing_behavior(ListSizingBehavior::Infer);
1185
1186 Popover::new().child(list).into_any_element()
1187 }
1188}