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