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