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