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}