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