code_context_menus.rs

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