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 = match &self.entries[selected_item] {
405            CompletionEntry::Match(mat) if show_completion_documentation => {
406                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            }
425            CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
426                InlineCompletionText::Edit { text, highlights } => div()
427                    .my_1()
428                    .rounded_md()
429                    .bg(cx.theme().colors().editor_background)
430                    .child(
431                        gpui::StyledText::new(text.clone())
432                            .with_highlights(&style.text, highlights.clone()),
433                    ),
434                InlineCompletionText::Move(text) => div().child(text.clone()),
435            }),
436            _ => None,
437        };
438
439        let aside_contents = if let Some(multiline_docs) = multiline_docs {
440            Some(multiline_docs)
441        } else if show_completion_documentation && self.aside_was_displayed.get() {
442            Some(div().child("Fetching documentation..."))
443        } else {
444            None
445        };
446        self.aside_was_displayed.set(aside_contents.is_some());
447
448        let aside_contents = aside_contents.map(|div| {
449            div.id("multiline_docs")
450                .max_h(max_height)
451                .flex_1()
452                .px_1p5()
453                .py_1()
454                .min_w(px(260.))
455                .max_w(px(640.))
456                .w(px(500.))
457                .overflow_y_scroll()
458                .occlude()
459        });
460
461        drop(completions);
462        let completions = self.completions.clone();
463        let matches = self.entries.clone();
464        let list = uniform_list(
465            cx.view().clone(),
466            "completions",
467            matches.len(),
468            move |_editor, range, cx| {
469                let start_ix = range.start;
470                let completions_guard = completions.borrow_mut();
471
472                matches[range]
473                    .iter()
474                    .enumerate()
475                    .map(|(ix, mat)| {
476                        let item_ix = start_ix + ix;
477                        match mat {
478                            CompletionEntry::Match(mat) => {
479                                let candidate_id = mat.candidate_id;
480                                let completion = &completions_guard[candidate_id];
481
482                                let documentation = if show_completion_documentation {
483                                    &completion.documentation
484                                } else {
485                                    &None
486                                };
487
488                                let filter_start = completion.label.filter_range.start;
489                                let highlights = gpui::combine_highlights(
490                                    mat.ranges().map(|range| {
491                                        (
492                                            filter_start + range.start..filter_start + range.end,
493                                            FontWeight::BOLD.into(),
494                                        )
495                                    }),
496                                    styled_runs_for_code_label(&completion.label, &style.syntax)
497                                        .map(|(range, mut highlight)| {
498                                            // Ignore font weight for syntax highlighting, as we'll use it
499                                            // for fuzzy matches.
500                                            highlight.font_weight = None;
501
502                                            if completion.lsp_completion.deprecated.unwrap_or(false)
503                                            {
504                                                highlight.strikethrough =
505                                                    Some(StrikethroughStyle {
506                                                        thickness: 1.0.into(),
507                                                        ..Default::default()
508                                                    });
509                                                highlight.color =
510                                                    Some(cx.theme().colors().text_muted);
511                                            }
512
513                                            (range, highlight)
514                                        }),
515                                );
516                                let completion_label =
517                                    StyledText::new(completion.label.text.clone())
518                                        .with_highlights(&style.text, highlights);
519                                let documentation_label =
520                                    if let Some(Documentation::SingleLine(text)) = documentation {
521                                        if text.trim().is_empty() {
522                                            None
523                                        } else {
524                                            Some(
525                                                Label::new(text.clone())
526                                                    .ml_4()
527                                                    .size(LabelSize::Small)
528                                                    .color(Color::Muted),
529                                            )
530                                        }
531                                    } else {
532                                        None
533                                    };
534
535                                let color_swatch = completion
536                                    .color()
537                                    .map(|color| div().size_4().bg(color).rounded_sm());
538
539                                div().min_w(px(220.)).max_w(px(540.)).child(
540                                    ListItem::new(mat.candidate_id)
541                                        .inset(true)
542                                        .toggle_state(item_ix == selected_item)
543                                        .on_click(cx.listener(move |editor, _event, cx| {
544                                            cx.stop_propagation();
545                                            if let Some(task) = editor.confirm_completion(
546                                                &ConfirmCompletion {
547                                                    item_ix: Some(item_ix),
548                                                },
549                                                cx,
550                                            ) {
551                                                task.detach_and_log_err(cx)
552                                            }
553                                        }))
554                                        .start_slot::<Div>(color_swatch)
555                                        .child(h_flex().overflow_hidden().child(completion_label))
556                                        .end_slot::<Label>(documentation_label),
557                                )
558                            }
559                            CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
560                                provider_name,
561                                ..
562                            }) => div()
563                                .min_w(px(250.))
564                                .max_w(px(500.))
565                                .pb_1()
566                                .border_b_1()
567                                .border_color(cx.theme().colors().border_variant)
568                                .child(
569                                    ListItem::new("inline-completion")
570                                        .inset(true)
571                                        .toggle_state(item_ix == selected_item)
572                                        .on_click(cx.listener(move |editor, _event, cx| {
573                                            cx.stop_propagation();
574                                            editor.accept_inline_completion(
575                                                &AcceptInlineCompletion {},
576                                                cx,
577                                            );
578                                        }))
579                                        .child(Label::new(SharedString::new_static(provider_name))),
580                                ),
581                        }
582                    })
583                    .collect()
584            },
585        )
586        .occlude()
587        .max_h(max_height_in_lines as f32 * cx.line_height())
588        .track_scroll(self.scroll_handle.clone())
589        .with_width_from_item(widest_completion_ix)
590        .with_sizing_behavior(ListSizingBehavior::Infer);
591
592        Popover::new()
593            .child(list)
594            .when_some(aside_contents, |popover, aside_contents| {
595                popover.aside(aside_contents)
596            })
597            .into_any_element()
598    }
599
600    pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
601        let mut matches = if let Some(query) = query {
602            fuzzy::match_strings(
603                &self.match_candidates,
604                query,
605                query.chars().any(|c| c.is_uppercase()),
606                100,
607                &Default::default(),
608                executor,
609            )
610            .await
611        } else {
612            self.match_candidates
613                .iter()
614                .enumerate()
615                .map(|(candidate_id, candidate)| StringMatch {
616                    candidate_id,
617                    score: Default::default(),
618                    positions: Default::default(),
619                    string: candidate.string.clone(),
620                })
621                .collect()
622        };
623
624        // Remove all candidates where the query's start does not match the start of any word in the candidate
625        if let Some(query) = query {
626            if let Some(query_start) = query.chars().next() {
627                matches.retain(|string_match| {
628                    split_words(&string_match.string).any(|word| {
629                        // Check that the first codepoint of the word as lowercase matches the first
630                        // codepoint of the query as lowercase
631                        word.chars()
632                            .flat_map(|codepoint| codepoint.to_lowercase())
633                            .zip(query_start.to_lowercase())
634                            .all(|(word_cp, query_cp)| word_cp == query_cp)
635                    })
636                });
637            }
638        }
639
640        let completions = self.completions.borrow_mut();
641        if self.sort_completions {
642            matches.sort_unstable_by_key(|mat| {
643                // We do want to strike a balance here between what the language server tells us
644                // to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
645                // `Creat` and there is a local variable called `CreateComponent`).
646                // So what we do is: we bucket all matches into two buckets
647                // - Strong matches
648                // - Weak matches
649                // Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
650                // and the Weak matches are the rest.
651                //
652                // For the strong matches, we sort by our fuzzy-finder score first and for the weak
653                // matches, we prefer language-server sort_text first.
654                //
655                // The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
656                // Rest of the matches(weak) can be sorted as language-server expects.
657
658                #[derive(PartialEq, Eq, PartialOrd, Ord)]
659                enum MatchScore<'a> {
660                    Strong {
661                        score: Reverse<OrderedFloat<f64>>,
662                        sort_text: Option<&'a str>,
663                        sort_key: (usize, &'a str),
664                    },
665                    Weak {
666                        sort_text: Option<&'a str>,
667                        score: Reverse<OrderedFloat<f64>>,
668                        sort_key: (usize, &'a str),
669                    },
670                }
671
672                let completion = &completions[mat.candidate_id];
673                let sort_key = completion.sort_key();
674                let sort_text = completion.lsp_completion.sort_text.as_deref();
675                let score = Reverse(OrderedFloat(mat.score));
676
677                if mat.score >= 0.2 {
678                    MatchScore::Strong {
679                        score,
680                        sort_text,
681                        sort_key,
682                    }
683                } else {
684                    MatchScore::Weak {
685                        sort_text,
686                        score,
687                        sort_key,
688                    }
689                }
690            });
691        }
692        drop(completions);
693
694        let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
695        if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
696            new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
697        }
698
699        self.entries = new_entries.into();
700        self.selected_item = 0;
701    }
702}
703
704#[derive(Clone)]
705pub struct AvailableCodeAction {
706    pub excerpt_id: ExcerptId,
707    pub action: CodeAction,
708    pub provider: Rc<dyn CodeActionProvider>,
709}
710
711#[derive(Clone)]
712pub struct CodeActionContents {
713    pub tasks: Option<Rc<ResolvedTasks>>,
714    pub actions: Option<Rc<[AvailableCodeAction]>>,
715}
716
717impl CodeActionContents {
718    fn len(&self) -> usize {
719        match (&self.tasks, &self.actions) {
720            (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
721            (Some(tasks), None) => tasks.templates.len(),
722            (None, Some(actions)) => actions.len(),
723            (None, None) => 0,
724        }
725    }
726
727    fn is_empty(&self) -> bool {
728        match (&self.tasks, &self.actions) {
729            (Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
730            (Some(tasks), None) => tasks.templates.is_empty(),
731            (None, Some(actions)) => actions.is_empty(),
732            (None, None) => true,
733        }
734    }
735
736    fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
737        self.tasks
738            .iter()
739            .flat_map(|tasks| {
740                tasks
741                    .templates
742                    .iter()
743                    .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
744            })
745            .chain(self.actions.iter().flat_map(|actions| {
746                actions.iter().map(|available| CodeActionsItem::CodeAction {
747                    excerpt_id: available.excerpt_id,
748                    action: available.action.clone(),
749                    provider: available.provider.clone(),
750                })
751            }))
752    }
753
754    pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
755        match (&self.tasks, &self.actions) {
756            (Some(tasks), Some(actions)) => {
757                if index < tasks.templates.len() {
758                    tasks
759                        .templates
760                        .get(index)
761                        .cloned()
762                        .map(|(kind, task)| CodeActionsItem::Task(kind, task))
763                } else {
764                    actions.get(index - tasks.templates.len()).map(|available| {
765                        CodeActionsItem::CodeAction {
766                            excerpt_id: available.excerpt_id,
767                            action: available.action.clone(),
768                            provider: available.provider.clone(),
769                        }
770                    })
771                }
772            }
773            (Some(tasks), None) => tasks
774                .templates
775                .get(index)
776                .cloned()
777                .map(|(kind, task)| CodeActionsItem::Task(kind, task)),
778            (None, Some(actions)) => {
779                actions
780                    .get(index)
781                    .map(|available| CodeActionsItem::CodeAction {
782                        excerpt_id: available.excerpt_id,
783                        action: available.action.clone(),
784                        provider: available.provider.clone(),
785                    })
786            }
787            (None, None) => None,
788        }
789    }
790}
791
792#[allow(clippy::large_enum_variant)]
793#[derive(Clone)]
794pub enum CodeActionsItem {
795    Task(TaskSourceKind, ResolvedTask),
796    CodeAction {
797        excerpt_id: ExcerptId,
798        action: CodeAction,
799        provider: Rc<dyn CodeActionProvider>,
800    },
801}
802
803impl CodeActionsItem {
804    fn as_task(&self) -> Option<&ResolvedTask> {
805        let Self::Task(_, task) = self else {
806            return None;
807        };
808        Some(task)
809    }
810
811    fn as_code_action(&self) -> Option<&CodeAction> {
812        let Self::CodeAction { action, .. } = self else {
813            return None;
814        };
815        Some(action)
816    }
817
818    pub fn label(&self) -> String {
819        match self {
820            Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
821            Self::Task(_, task) => task.resolved_label.clone(),
822        }
823    }
824}
825
826pub struct CodeActionsMenu {
827    pub actions: CodeActionContents,
828    pub buffer: Model<Buffer>,
829    pub selected_item: usize,
830    pub scroll_handle: UniformListScrollHandle,
831    pub deployed_from_indicator: Option<DisplayRow>,
832}
833
834impl CodeActionsMenu {
835    fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
836        self.selected_item = 0;
837        self.scroll_handle
838            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
839        cx.notify()
840    }
841
842    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
843        if self.selected_item > 0 {
844            self.selected_item -= 1;
845        } else {
846            self.selected_item = self.actions.len() - 1;
847        }
848        self.scroll_handle
849            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
850        cx.notify();
851    }
852
853    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
854        if self.selected_item + 1 < self.actions.len() {
855            self.selected_item += 1;
856        } else {
857            self.selected_item = 0;
858        }
859        self.scroll_handle
860            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
861        cx.notify();
862    }
863
864    fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
865        self.selected_item = self.actions.len() - 1;
866        self.scroll_handle
867            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
868        cx.notify()
869    }
870
871    fn visible(&self) -> bool {
872        !self.actions.is_empty()
873    }
874
875    fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
876        if let Some(row) = self.deployed_from_indicator {
877            ContextMenuOrigin::GutterIndicator(row)
878        } else {
879            ContextMenuOrigin::EditorPoint(cursor_position)
880        }
881    }
882
883    fn render(
884        &self,
885        _style: &EditorStyle,
886        max_height_in_lines: u32,
887        cx: &mut ViewContext<Editor>,
888    ) -> AnyElement {
889        let actions = self.actions.clone();
890        let selected_item = self.selected_item;
891        let list = uniform_list(
892            cx.view().clone(),
893            "code_actions_menu",
894            self.actions.len(),
895            move |_this, range, cx| {
896                actions
897                    .iter()
898                    .skip(range.start)
899                    .take(range.end - range.start)
900                    .enumerate()
901                    .map(|(ix, action)| {
902                        let item_ix = range.start + ix;
903                        let selected = item_ix == selected_item;
904                        let colors = cx.theme().colors();
905                        div().min_w(px(220.)).max_w(px(540.)).child(
906                            ListItem::new(item_ix)
907                                .inset(true)
908                                .toggle_state(selected)
909                                .when_some(action.as_code_action(), |this, action| {
910                                    this.on_click(cx.listener(move |editor, _, cx| {
911                                        cx.stop_propagation();
912                                        if let Some(task) = editor.confirm_code_action(
913                                            &ConfirmCodeAction {
914                                                item_ix: Some(item_ix),
915                                            },
916                                            cx,
917                                        ) {
918                                            task.detach_and_log_err(cx)
919                                        }
920                                    }))
921                                    .child(
922                                        h_flex()
923                                            .overflow_hidden()
924                                            .child(
925                                                // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
926                                                action.lsp_action.title.replace("\n", ""),
927                                            )
928                                            .when(selected, |this| {
929                                                this.text_color(colors.text_accent)
930                                            }),
931                                    )
932                                })
933                                .when_some(action.as_task(), |this, task| {
934                                    this.on_click(cx.listener(move |editor, _, cx| {
935                                        cx.stop_propagation();
936                                        if let Some(task) = editor.confirm_code_action(
937                                            &ConfirmCodeAction {
938                                                item_ix: Some(item_ix),
939                                            },
940                                            cx,
941                                        ) {
942                                            task.detach_and_log_err(cx)
943                                        }
944                                    }))
945                                    .child(
946                                        h_flex()
947                                            .overflow_hidden()
948                                            .child(task.resolved_label.replace("\n", ""))
949                                            .when(selected, |this| {
950                                                this.text_color(colors.text_accent)
951                                            }),
952                                    )
953                                }),
954                        )
955                    })
956                    .collect()
957            },
958        )
959        .occlude()
960        .max_h(max_height_in_lines as f32 * cx.line_height())
961        .track_scroll(self.scroll_handle.clone())
962        .with_width_from_item(
963            self.actions
964                .iter()
965                .enumerate()
966                .max_by_key(|(_, action)| match action {
967                    CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
968                    CodeActionsItem::CodeAction { action, .. } => {
969                        action.lsp_action.title.chars().count()
970                    }
971                })
972                .map(|(ix, _)| ix),
973        )
974        .with_sizing_behavior(ListSizingBehavior::Infer);
975
976        Popover::new().child(list).into_any_element()
977    }
978}