code_context_menus.rs

  1use std::cell::RefCell;
  2use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
  3
  4use fuzzy::{StringMatch, StringMatchCandidate};
  5use gpui::{
  6    div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
  7    Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
  8    ViewContext, WeakView,
  9};
 10use language::Buffer;
 11use language::{CodeLabel, Documentation};
 12use lsp::LanguageServerId;
 13use multi_buffer::{Anchor, ExcerptId};
 14use ordered_float::OrderedFloat;
 15use project::{CodeAction, Completion, TaskSourceKind};
 16use task::ResolvedTask;
 17use ui::{
 18    h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
 19    Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
 20    StatefulInteractiveElement as _, Styled, Toggleable as _,
 21};
 22use util::ResultExt as _;
 23use workspace::Workspace;
 24
 25use crate::{
 26    actions::{ConfirmCodeAction, ConfirmCompletion},
 27    display_map::DisplayPoint,
 28    render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
 29    CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
 30};
 31use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
 32
 33pub enum CodeContextMenu {
 34    Completions(CompletionsMenu),
 35    CodeActions(CodeActionsMenu),
 36}
 37
 38impl CodeContextMenu {
 39    pub fn select_first(
 40        &mut self,
 41        provider: Option<&dyn CompletionProvider>,
 42        cx: &mut ViewContext<Editor>,
 43    ) -> bool {
 44        if self.visible() {
 45            match self {
 46                CodeContextMenu::Completions(menu) => menu.select_first(provider, cx),
 47                CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
 48            }
 49            true
 50        } else {
 51            false
 52        }
 53    }
 54
 55    pub fn select_prev(
 56        &mut self,
 57        provider: Option<&dyn CompletionProvider>,
 58        cx: &mut ViewContext<Editor>,
 59    ) -> bool {
 60        if self.visible() {
 61            match self {
 62                CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx),
 63                CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
 64            }
 65            true
 66        } else {
 67            false
 68        }
 69    }
 70
 71    pub fn select_next(
 72        &mut self,
 73        provider: Option<&dyn CompletionProvider>,
 74        cx: &mut ViewContext<Editor>,
 75    ) -> bool {
 76        if self.visible() {
 77            match self {
 78                CodeContextMenu::Completions(menu) => menu.select_next(provider, cx),
 79                CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
 80            }
 81            true
 82        } else {
 83            false
 84        }
 85    }
 86
 87    pub fn select_last(
 88        &mut self,
 89        provider: Option<&dyn CompletionProvider>,
 90        cx: &mut ViewContext<Editor>,
 91    ) -> bool {
 92        if self.visible() {
 93            match self {
 94                CodeContextMenu::Completions(menu) => menu.select_last(provider, cx),
 95                CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
 96            }
 97            true
 98        } else {
 99            false
100        }
101    }
102
103    pub fn visible(&self) -> bool {
104        match self {
105            CodeContextMenu::Completions(menu) => menu.visible(),
106            CodeContextMenu::CodeActions(menu) => menu.visible(),
107        }
108    }
109
110    pub fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
111        match self {
112            CodeContextMenu::Completions(menu) => menu.origin(cursor_position),
113            CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
114        }
115    }
116    pub fn render(
117        &self,
118        style: &EditorStyle,
119        max_height_in_lines: u32,
120        workspace: Option<WeakView<Workspace>>,
121        cx: &mut ViewContext<Editor>,
122    ) -> AnyElement {
123        match self {
124            CodeContextMenu::Completions(menu) => {
125                menu.render(style, max_height_in_lines, workspace, cx)
126            }
127            CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
128        }
129    }
130}
131
132pub enum ContextMenuOrigin {
133    EditorPoint(DisplayPoint),
134    GutterIndicator(DisplayRow),
135}
136
137#[derive(Clone, Debug)]
138pub struct CompletionsMenu {
139    pub id: CompletionId,
140    sort_completions: bool,
141    pub initial_position: Anchor,
142    pub buffer: Model<Buffer>,
143    pub completions: Rc<RefCell<Box<[Completion]>>>,
144    match_candidates: Rc<[StringMatchCandidate]>,
145    pub entries: Rc<[CompletionEntry]>,
146    pub selected_item: usize,
147    scroll_handle: UniformListScrollHandle,
148    resolve_completions: bool,
149    pub aside_was_displayed: Cell<bool>,
150    show_completion_documentation: bool,
151}
152
153#[derive(Clone, Debug)]
154pub(crate) enum CompletionEntry {
155    Match(StringMatch),
156    InlineCompletionHint(InlineCompletionMenuHint),
157}
158
159impl CompletionsMenu {
160    pub fn new(
161        id: CompletionId,
162        sort_completions: bool,
163        show_completion_documentation: bool,
164        initial_position: Anchor,
165        buffer: Model<Buffer>,
166        completions: Box<[Completion]>,
167        aside_was_displayed: bool,
168    ) -> Self {
169        let match_candidates = completions
170            .iter()
171            .enumerate()
172            .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
173            .collect();
174
175        Self {
176            id,
177            sort_completions,
178            initial_position,
179            buffer,
180            show_completion_documentation,
181            completions: RefCell::new(completions).into(),
182            match_candidates,
183            entries: Vec::new().into(),
184            selected_item: 0,
185            scroll_handle: UniformListScrollHandle::new(),
186            resolve_completions: true,
187            aside_was_displayed: Cell::new(aside_was_displayed),
188        }
189    }
190
191    pub fn new_snippet_choices(
192        id: CompletionId,
193        sort_completions: bool,
194        choices: &Vec<String>,
195        selection: Range<Anchor>,
196        buffer: Model<Buffer>,
197    ) -> Self {
198        let completions = choices
199            .iter()
200            .map(|choice| Completion {
201                old_range: selection.start.text_anchor..selection.end.text_anchor,
202                new_text: choice.to_string(),
203                label: CodeLabel {
204                    text: choice.to_string(),
205                    runs: Default::default(),
206                    filter_range: Default::default(),
207                },
208                server_id: LanguageServerId(usize::MAX),
209                documentation: None,
210                lsp_completion: Default::default(),
211                confirm: None,
212            })
213            .collect();
214
215        let match_candidates = choices
216            .iter()
217            .enumerate()
218            .map(|(id, completion)| StringMatchCandidate::new(id, &completion))
219            .collect();
220        let entries = choices
221            .iter()
222            .enumerate()
223            .map(|(id, completion)| {
224                CompletionEntry::Match(StringMatch {
225                    candidate_id: id,
226                    score: 1.,
227                    positions: vec![],
228                    string: completion.clone(),
229                })
230            })
231            .collect();
232        Self {
233            id,
234            sort_completions,
235            initial_position: selection.start,
236            buffer,
237            completions: RefCell::new(completions).into(),
238            match_candidates,
239            entries,
240            selected_item: 0,
241            scroll_handle: UniformListScrollHandle::new(),
242            resolve_completions: false,
243            aside_was_displayed: Cell::new(false),
244            show_completion_documentation: false,
245        }
246    }
247
248    fn select_first(
249        &mut self,
250        provider: Option<&dyn CompletionProvider>,
251        cx: &mut ViewContext<Editor>,
252    ) {
253        self.selected_item = 0;
254        self.scroll_handle
255            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
256        self.resolve_selected_completion(provider, cx);
257        cx.notify();
258    }
259
260    fn select_prev(
261        &mut self,
262        provider: Option<&dyn CompletionProvider>,
263        cx: &mut ViewContext<Editor>,
264    ) {
265        if self.selected_item > 0 {
266            self.selected_item -= 1;
267        } else {
268            self.selected_item = self.entries.len() - 1;
269        }
270        self.scroll_handle
271            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
272        self.resolve_selected_completion(provider, cx);
273        cx.notify();
274    }
275
276    fn select_next(
277        &mut self,
278        provider: Option<&dyn CompletionProvider>,
279        cx: &mut ViewContext<Editor>,
280    ) {
281        if self.selected_item + 1 < self.entries.len() {
282            self.selected_item += 1;
283        } else {
284            self.selected_item = 0;
285        }
286        self.scroll_handle
287            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
288        self.resolve_selected_completion(provider, cx);
289        cx.notify();
290    }
291
292    fn select_last(
293        &mut self,
294        provider: Option<&dyn CompletionProvider>,
295        cx: &mut ViewContext<Editor>,
296    ) {
297        self.selected_item = self.entries.len() - 1;
298        self.scroll_handle
299            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
300        self.resolve_selected_completion(provider, cx);
301        cx.notify();
302    }
303
304    pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
305        let hint = CompletionEntry::InlineCompletionHint(hint);
306
307        self.entries = match self.entries.first() {
308            Some(CompletionEntry::InlineCompletionHint { .. }) => {
309                let mut entries = Vec::from(&*self.entries);
310                entries[0] = hint;
311                entries
312            }
313            _ => {
314                let mut entries = Vec::with_capacity(self.entries.len() + 1);
315                entries.push(hint);
316                entries.extend_from_slice(&self.entries);
317                entries
318            }
319        }
320        .into();
321        self.selected_item = 0;
322    }
323
324    pub fn resolve_selected_completion(
325        &mut self,
326        provider: Option<&dyn CompletionProvider>,
327        cx: &mut ViewContext<Editor>,
328    ) {
329        if !self.resolve_completions {
330            return;
331        }
332        let Some(provider) = provider else {
333            return;
334        };
335
336        match &self.entries[self.selected_item] {
337            CompletionEntry::Match(entry) => {
338                let completion_index = entry.candidate_id;
339                let resolve_task = provider.resolve_completions(
340                    self.buffer.clone(),
341                    vec![completion_index],
342                    self.completions.clone(),
343                    cx,
344                );
345
346                cx.spawn(move |editor, mut cx| async move {
347                    if let Some(true) = resolve_task.await.log_err() {
348                        editor.update(&mut cx, |_, cx| cx.notify()).ok();
349                    }
350                })
351                .detach();
352            }
353            CompletionEntry::InlineCompletionHint { .. } => {}
354        }
355    }
356
357    pub fn visible(&self) -> bool {
358        !self.entries.is_empty()
359    }
360
361    fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
362        ContextMenuOrigin::EditorPoint(cursor_position)
363    }
364
365    fn render(
366        &self,
367        style: &EditorStyle,
368        max_height_in_lines: u32,
369        workspace: Option<WeakView<Workspace>>,
370        cx: &mut ViewContext<Editor>,
371    ) -> AnyElement {
372        let max_height = max_height_in_lines as f32 * cx.line_height();
373
374        let completions = self.completions.borrow_mut();
375        let show_completion_documentation = self.show_completion_documentation;
376        let widest_completion_ix = self
377            .entries
378            .iter()
379            .enumerate()
380            .max_by_key(|(_, mat)| match mat {
381                CompletionEntry::Match(mat) => {
382                    let completion = &completions[mat.candidate_id];
383                    let documentation = &completion.documentation;
384
385                    let mut len = completion.label.text.chars().count();
386                    if let Some(Documentation::SingleLine(text)) = documentation {
387                        if show_completion_documentation {
388                            len += text.chars().count();
389                        }
390                    }
391
392                    len
393                }
394                CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
395                    provider_name,
396                    ..
397                }) => provider_name.len(),
398            })
399            .map(|(ix, _)| ix);
400
401        let selected_item = self.selected_item;
402        let style = style.clone();
403
404        let multiline_docs = if show_completion_documentation {
405            match &self.entries[selected_item] {
406                CompletionEntry::Match(mat) => match &completions[mat.candidate_id].documentation {
407                    Some(Documentation::MultiLinePlainText(text)) => {
408                        Some(div().child(SharedString::from(text.clone())))
409                    }
410                    Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
411                        Some(div().child(render_parsed_markdown(
412                            "completions_markdown",
413                            parsed,
414                            &style,
415                            workspace,
416                            cx,
417                        )))
418                    }
419                    Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
420                        Some(div().child("No documentation"))
421                    }
422                    _ => None,
423                },
424                CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
425                    InlineCompletionText::Edit { text, highlights } => div()
426                        .my_1()
427                        .rounded_md()
428                        .bg(cx.theme().colors().editor_background)
429                        .child(
430                            gpui::StyledText::new(text.clone())
431                                .with_highlights(&style.text, highlights.clone()),
432                        ),
433                    InlineCompletionText::Move(text) => div().child(text.clone()),
434                }),
435            }
436        } else {
437            None
438        };
439
440        let aside_contents = if let Some(multiline_docs) = multiline_docs {
441            Some(multiline_docs)
442        } else if self.aside_was_displayed.get() {
443            Some(div().child("Fetching documentation..."))
444        } else {
445            None
446        };
447        self.aside_was_displayed.set(aside_contents.is_some());
448
449        let aside_contents = aside_contents.map(|div| {
450            div.id("multiline_docs")
451                .max_h(max_height)
452                .flex_1()
453                .px_1p5()
454                .py_1()
455                .min_w(px(260.))
456                .max_w(px(640.))
457                .w(px(500.))
458                .overflow_y_scroll()
459                .occlude()
460        });
461
462        drop(completions);
463        let completions = self.completions.clone();
464        let matches = self.entries.clone();
465        let list = uniform_list(
466            cx.view().clone(),
467            "completions",
468            matches.len(),
469            move |_editor, range, cx| {
470                let start_ix = range.start;
471                let completions_guard = completions.borrow_mut();
472
473                matches[range]
474                    .iter()
475                    .enumerate()
476                    .map(|(ix, mat)| {
477                        let item_ix = start_ix + ix;
478                        match mat {
479                            CompletionEntry::Match(mat) => {
480                                let candidate_id = mat.candidate_id;
481                                let completion = &completions_guard[candidate_id];
482
483                                let documentation = if show_completion_documentation {
484                                    &completion.documentation
485                                } else {
486                                    &None
487                                };
488
489                                let filter_start = completion.label.filter_range.start;
490                                let highlights = gpui::combine_highlights(
491                                    mat.ranges().map(|range| {
492                                        (
493                                            filter_start + range.start..filter_start + range.end,
494                                            FontWeight::BOLD.into(),
495                                        )
496                                    }),
497                                    styled_runs_for_code_label(&completion.label, &style.syntax)
498                                        .map(|(range, mut highlight)| {
499                                            // Ignore font weight for syntax highlighting, as we'll use it
500                                            // for fuzzy matches.
501                                            highlight.font_weight = None;
502
503                                            if completion.lsp_completion.deprecated.unwrap_or(false)
504                                            {
505                                                highlight.strikethrough =
506                                                    Some(StrikethroughStyle {
507                                                        thickness: 1.0.into(),
508                                                        ..Default::default()
509                                                    });
510                                                highlight.color =
511                                                    Some(cx.theme().colors().text_muted);
512                                            }
513
514                                            (range, highlight)
515                                        }),
516                                );
517                                let completion_label =
518                                    StyledText::new(completion.label.text.clone())
519                                        .with_highlights(&style.text, highlights);
520                                let documentation_label =
521                                    if let Some(Documentation::SingleLine(text)) = documentation {
522                                        if text.trim().is_empty() {
523                                            None
524                                        } else {
525                                            Some(
526                                                Label::new(text.clone())
527                                                    .ml_4()
528                                                    .size(LabelSize::Small)
529                                                    .color(Color::Muted),
530                                            )
531                                        }
532                                    } else {
533                                        None
534                                    };
535
536                                let color_swatch = completion
537                                    .color()
538                                    .map(|color| div().size_4().bg(color).rounded_sm());
539
540                                div().min_w(px(220.)).max_w(px(540.)).child(
541                                    ListItem::new(mat.candidate_id)
542                                        .inset(true)
543                                        .toggle_state(item_ix == selected_item)
544                                        .on_click(cx.listener(move |editor, _event, cx| {
545                                            cx.stop_propagation();
546                                            if let Some(task) = editor.confirm_completion(
547                                                &ConfirmCompletion {
548                                                    item_ix: Some(item_ix),
549                                                },
550                                                cx,
551                                            ) {
552                                                task.detach_and_log_err(cx)
553                                            }
554                                        }))
555                                        .start_slot::<Div>(color_swatch)
556                                        .child(h_flex().overflow_hidden().child(completion_label))
557                                        .end_slot::<Label>(documentation_label),
558                                )
559                            }
560                            CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
561                                provider_name,
562                                ..
563                            }) => div()
564                                .min_w(px(250.))
565                                .max_w(px(500.))
566                                .pb_1()
567                                .border_b_1()
568                                .border_color(cx.theme().colors().border_variant)
569                                .child(
570                                    ListItem::new("inline-completion")
571                                        .inset(true)
572                                        .toggle_state(item_ix == selected_item)
573                                        .on_click(cx.listener(move |editor, _event, cx| {
574                                            cx.stop_propagation();
575                                            editor.accept_inline_completion(
576                                                &AcceptInlineCompletion {},
577                                                cx,
578                                            );
579                                        }))
580                                        .child(Label::new(SharedString::new_static(provider_name))),
581                                ),
582                        }
583                    })
584                    .collect()
585            },
586        )
587        .occlude()
588        .max_h(max_height_in_lines as f32 * cx.line_height())
589        .track_scroll(self.scroll_handle.clone())
590        .with_width_from_item(widest_completion_ix)
591        .with_sizing_behavior(ListSizingBehavior::Infer);
592
593        Popover::new()
594            .child(list)
595            .when_some(aside_contents, |popover, aside_contents| {
596                popover.aside(aside_contents)
597            })
598            .into_any_element()
599    }
600
601    pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
602        let mut matches = if let Some(query) = query {
603            fuzzy::match_strings(
604                &self.match_candidates,
605                query,
606                query.chars().any(|c| c.is_uppercase()),
607                100,
608                &Default::default(),
609                executor,
610            )
611            .await
612        } else {
613            self.match_candidates
614                .iter()
615                .enumerate()
616                .map(|(candidate_id, candidate)| StringMatch {
617                    candidate_id,
618                    score: Default::default(),
619                    positions: Default::default(),
620                    string: candidate.string.clone(),
621                })
622                .collect()
623        };
624
625        // Remove all candidates where the query's start does not match the start of any word in the candidate
626        if let Some(query) = query {
627            if let Some(query_start) = query.chars().next() {
628                matches.retain(|string_match| {
629                    split_words(&string_match.string).any(|word| {
630                        // Check that the first codepoint of the word as lowercase matches the first
631                        // codepoint of the query as lowercase
632                        word.chars()
633                            .flat_map(|codepoint| codepoint.to_lowercase())
634                            .zip(query_start.to_lowercase())
635                            .all(|(word_cp, query_cp)| word_cp == query_cp)
636                    })
637                });
638            }
639        }
640
641        let completions = self.completions.borrow_mut();
642        if self.sort_completions {
643            matches.sort_unstable_by_key(|mat| {
644                // We do want to strike a balance here between what the language server tells us
645                // to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
646                // `Creat` and there is a local variable called `CreateComponent`).
647                // So what we do is: we bucket all matches into two buckets
648                // - Strong matches
649                // - Weak matches
650                // Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
651                // and the Weak matches are the rest.
652                //
653                // For the strong matches, we sort by our fuzzy-finder score first and for the weak
654                // matches, we prefer language-server sort_text first.
655                //
656                // The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
657                // Rest of the matches(weak) can be sorted as language-server expects.
658
659                #[derive(PartialEq, Eq, PartialOrd, Ord)]
660                enum MatchScore<'a> {
661                    Strong {
662                        score: Reverse<OrderedFloat<f64>>,
663                        sort_text: Option<&'a str>,
664                        sort_key: (usize, &'a str),
665                    },
666                    Weak {
667                        sort_text: Option<&'a str>,
668                        score: Reverse<OrderedFloat<f64>>,
669                        sort_key: (usize, &'a str),
670                    },
671                }
672
673                let completion = &completions[mat.candidate_id];
674                let sort_key = completion.sort_key();
675                let sort_text = completion.lsp_completion.sort_text.as_deref();
676                let score = Reverse(OrderedFloat(mat.score));
677
678                if mat.score >= 0.2 {
679                    MatchScore::Strong {
680                        score,
681                        sort_text,
682                        sort_key,
683                    }
684                } else {
685                    MatchScore::Weak {
686                        sort_text,
687                        score,
688                        sort_key,
689                    }
690                }
691            });
692        }
693        drop(completions);
694
695        let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
696        if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
697            new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
698        }
699
700        self.entries = new_entries.into();
701        self.selected_item = 0;
702    }
703}
704
705#[derive(Clone)]
706pub struct AvailableCodeAction {
707    pub excerpt_id: ExcerptId,
708    pub action: CodeAction,
709    pub provider: Rc<dyn CodeActionProvider>,
710}
711
712#[derive(Clone)]
713pub struct CodeActionContents {
714    pub tasks: Option<Rc<ResolvedTasks>>,
715    pub actions: Option<Rc<[AvailableCodeAction]>>,
716}
717
718impl CodeActionContents {
719    fn len(&self) -> usize {
720        match (&self.tasks, &self.actions) {
721            (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
722            (Some(tasks), None) => tasks.templates.len(),
723            (None, Some(actions)) => actions.len(),
724            (None, None) => 0,
725        }
726    }
727
728    fn is_empty(&self) -> bool {
729        match (&self.tasks, &self.actions) {
730            (Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
731            (Some(tasks), None) => tasks.templates.is_empty(),
732            (None, Some(actions)) => actions.is_empty(),
733            (None, None) => true,
734        }
735    }
736
737    fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
738        self.tasks
739            .iter()
740            .flat_map(|tasks| {
741                tasks
742                    .templates
743                    .iter()
744                    .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
745            })
746            .chain(self.actions.iter().flat_map(|actions| {
747                actions.iter().map(|available| CodeActionsItem::CodeAction {
748                    excerpt_id: available.excerpt_id,
749                    action: available.action.clone(),
750                    provider: available.provider.clone(),
751                })
752            }))
753    }
754
755    pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
756        match (&self.tasks, &self.actions) {
757            (Some(tasks), Some(actions)) => {
758                if index < tasks.templates.len() {
759                    tasks
760                        .templates
761                        .get(index)
762                        .cloned()
763                        .map(|(kind, task)| CodeActionsItem::Task(kind, task))
764                } else {
765                    actions.get(index - tasks.templates.len()).map(|available| {
766                        CodeActionsItem::CodeAction {
767                            excerpt_id: available.excerpt_id,
768                            action: available.action.clone(),
769                            provider: available.provider.clone(),
770                        }
771                    })
772                }
773            }
774            (Some(tasks), None) => tasks
775                .templates
776                .get(index)
777                .cloned()
778                .map(|(kind, task)| CodeActionsItem::Task(kind, task)),
779            (None, Some(actions)) => {
780                actions
781                    .get(index)
782                    .map(|available| CodeActionsItem::CodeAction {
783                        excerpt_id: available.excerpt_id,
784                        action: available.action.clone(),
785                        provider: available.provider.clone(),
786                    })
787            }
788            (None, None) => None,
789        }
790    }
791}
792
793#[allow(clippy::large_enum_variant)]
794#[derive(Clone)]
795pub enum CodeActionsItem {
796    Task(TaskSourceKind, ResolvedTask),
797    CodeAction {
798        excerpt_id: ExcerptId,
799        action: CodeAction,
800        provider: Rc<dyn CodeActionProvider>,
801    },
802}
803
804impl CodeActionsItem {
805    fn as_task(&self) -> Option<&ResolvedTask> {
806        let Self::Task(_, task) = self else {
807            return None;
808        };
809        Some(task)
810    }
811
812    fn as_code_action(&self) -> Option<&CodeAction> {
813        let Self::CodeAction { action, .. } = self else {
814            return None;
815        };
816        Some(action)
817    }
818
819    pub fn label(&self) -> String {
820        match self {
821            Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
822            Self::Task(_, task) => task.resolved_label.clone(),
823        }
824    }
825}
826
827pub struct CodeActionsMenu {
828    pub actions: CodeActionContents,
829    pub buffer: Model<Buffer>,
830    pub selected_item: usize,
831    pub scroll_handle: UniformListScrollHandle,
832    pub deployed_from_indicator: Option<DisplayRow>,
833}
834
835impl CodeActionsMenu {
836    fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
837        self.selected_item = 0;
838        self.scroll_handle
839            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
840        cx.notify()
841    }
842
843    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
844        if self.selected_item > 0 {
845            self.selected_item -= 1;
846        } else {
847            self.selected_item = self.actions.len() - 1;
848        }
849        self.scroll_handle
850            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
851        cx.notify();
852    }
853
854    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
855        if self.selected_item + 1 < self.actions.len() {
856            self.selected_item += 1;
857        } else {
858            self.selected_item = 0;
859        }
860        self.scroll_handle
861            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
862        cx.notify();
863    }
864
865    fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
866        self.selected_item = self.actions.len() - 1;
867        self.scroll_handle
868            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
869        cx.notify()
870    }
871
872    fn visible(&self) -> bool {
873        !self.actions.is_empty()
874    }
875
876    fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
877        if let Some(row) = self.deployed_from_indicator {
878            ContextMenuOrigin::GutterIndicator(row)
879        } else {
880            ContextMenuOrigin::EditorPoint(cursor_position)
881        }
882    }
883
884    fn render(
885        &self,
886        _style: &EditorStyle,
887        max_height_in_lines: u32,
888        cx: &mut ViewContext<Editor>,
889    ) -> AnyElement {
890        let actions = self.actions.clone();
891        let selected_item = self.selected_item;
892        let list = uniform_list(
893            cx.view().clone(),
894            "code_actions_menu",
895            self.actions.len(),
896            move |_this, range, cx| {
897                actions
898                    .iter()
899                    .skip(range.start)
900                    .take(range.end - range.start)
901                    .enumerate()
902                    .map(|(ix, action)| {
903                        let item_ix = range.start + ix;
904                        let selected = item_ix == selected_item;
905                        let colors = cx.theme().colors();
906                        div().min_w(px(220.)).max_w(px(540.)).child(
907                            ListItem::new(item_ix)
908                                .inset(true)
909                                .toggle_state(selected)
910                                .when_some(action.as_code_action(), |this, action| {
911                                    this.on_click(cx.listener(move |editor, _, cx| {
912                                        cx.stop_propagation();
913                                        if let Some(task) = editor.confirm_code_action(
914                                            &ConfirmCodeAction {
915                                                item_ix: Some(item_ix),
916                                            },
917                                            cx,
918                                        ) {
919                                            task.detach_and_log_err(cx)
920                                        }
921                                    }))
922                                    .child(
923                                        h_flex()
924                                            .overflow_hidden()
925                                            .child(
926                                                // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
927                                                action.lsp_action.title.replace("\n", ""),
928                                            )
929                                            .when(selected, |this| {
930                                                this.text_color(colors.text_accent)
931                                            }),
932                                    )
933                                })
934                                .when_some(action.as_task(), |this, task| {
935                                    this.on_click(cx.listener(move |editor, _, cx| {
936                                        cx.stop_propagation();
937                                        if let Some(task) = editor.confirm_code_action(
938                                            &ConfirmCodeAction {
939                                                item_ix: Some(item_ix),
940                                            },
941                                            cx,
942                                        ) {
943                                            task.detach_and_log_err(cx)
944                                        }
945                                    }))
946                                    .child(
947                                        h_flex()
948                                            .overflow_hidden()
949                                            .child(task.resolved_label.replace("\n", ""))
950                                            .when(selected, |this| {
951                                                this.text_color(colors.text_accent)
952                                            }),
953                                    )
954                                }),
955                        )
956                    })
957                    .collect()
958            },
959        )
960        .occlude()
961        .max_h(max_height_in_lines as f32 * cx.line_height())
962        .track_scroll(self.scroll_handle.clone())
963        .with_width_from_item(
964            self.actions
965                .iter()
966                .enumerate()
967                .max_by_key(|(_, action)| match action {
968                    CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
969                    CodeActionsItem::CodeAction { action, .. } => {
970                        action.lsp_action.title.chars().count()
971                    }
972                })
973                .map(|(ix, _)| ix),
974        )
975        .with_sizing_behavior(ListSizingBehavior::Infer);
976
977        Popover::new().child(list).into_any_element()
978    }
979}