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