1use crate::scroll::ScrollAmount;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{
4 AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
5 SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
6 uniform_list,
7};
8use itertools::Itertools;
9use language::CodeLabel;
10use language::{Buffer, LanguageName, LanguageRegistry};
11use markdown::{Markdown, MarkdownElement};
12use multi_buffer::{Anchor, ExcerptId};
13use ordered_float::OrderedFloat;
14use project::lsp_store::CompletionDocumentation;
15use project::{CodeAction, Completion, TaskSourceKind};
16use project::{CompletionDisplayOptions, CompletionSource};
17use task::DebugScenario;
18use task::TaskContext;
19
20use std::collections::VecDeque;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::{
24 cell::RefCell,
25 cmp::{Reverse, min},
26 iter,
27 ops::Range,
28 rc::Rc,
29};
30use task::ResolvedTask;
31use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
32use util::ResultExt;
33
34use crate::CodeActionSource;
35use crate::hover_popover::{hover_markdown_style, open_markdown_url};
36use crate::{
37 CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
38 ResolvedTasks,
39 actions::{ConfirmCodeAction, ConfirmCompletion},
40 split_words, styled_runs_for_code_label,
41};
42use settings::SnippetSortOrder;
43
44pub const MENU_GAP: Pixels = px(4.);
45pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
46pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
47pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
48
49// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
50// documentation not yet being parsed.
51//
52// The size of the cache is set to 16, which is roughly 3 times more than the number of items
53// fetched around the current selection. This way documentation is more often ready for render when
54// revisiting previous entries, such as when pressing backspace.
55const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
56const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
57const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
58
59// Number of items beyond the visible items to resolve documentation.
60const RESOLVE_BEFORE_ITEMS: usize = 4;
61const RESOLVE_AFTER_ITEMS: usize = 4;
62
63pub enum CodeContextMenu {
64 Completions(CompletionsMenu),
65 CodeActions(CodeActionsMenu),
66}
67
68impl CodeContextMenu {
69 pub fn select_first(
70 &mut self,
71 provider: Option<&dyn CompletionProvider>,
72 window: &mut Window,
73 cx: &mut Context<Editor>,
74 ) -> bool {
75 if self.visible() {
76 match self {
77 CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
78 CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
79 }
80 true
81 } else {
82 false
83 }
84 }
85
86 pub fn select_prev(
87 &mut self,
88 provider: Option<&dyn CompletionProvider>,
89 window: &mut Window,
90 cx: &mut Context<Editor>,
91 ) -> bool {
92 if self.visible() {
93 match self {
94 CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
95 CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
96 }
97 true
98 } else {
99 false
100 }
101 }
102
103 pub fn select_next(
104 &mut self,
105 provider: Option<&dyn CompletionProvider>,
106 window: &mut Window,
107 cx: &mut Context<Editor>,
108 ) -> bool {
109 if self.visible() {
110 match self {
111 CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
112 CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
113 }
114 true
115 } else {
116 false
117 }
118 }
119
120 pub fn select_last(
121 &mut self,
122 provider: Option<&dyn CompletionProvider>,
123 window: &mut Window,
124 cx: &mut Context<Editor>,
125 ) -> bool {
126 if self.visible() {
127 match self {
128 CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
129 CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
130 }
131 true
132 } else {
133 false
134 }
135 }
136
137 pub fn visible(&self) -> bool {
138 match self {
139 CodeContextMenu::Completions(menu) => menu.visible(),
140 CodeContextMenu::CodeActions(menu) => menu.visible(),
141 }
142 }
143
144 pub fn origin(&self) -> ContextMenuOrigin {
145 match self {
146 CodeContextMenu::Completions(menu) => menu.origin(),
147 CodeContextMenu::CodeActions(menu) => menu.origin(),
148 }
149 }
150
151 pub fn render(
152 &self,
153 style: &EditorStyle,
154 max_height_in_lines: u32,
155 window: &mut Window,
156 cx: &mut Context<Editor>,
157 ) -> AnyElement {
158 match self {
159 CodeContextMenu::Completions(menu) => {
160 menu.render(style, max_height_in_lines, window, cx)
161 }
162 CodeContextMenu::CodeActions(menu) => {
163 menu.render(style, max_height_in_lines, window, cx)
164 }
165 }
166 }
167
168 pub fn render_aside(
169 &mut self,
170 max_size: Size<Pixels>,
171 window: &mut Window,
172 cx: &mut Context<Editor>,
173 ) -> Option<AnyElement> {
174 match self {
175 CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
176 CodeContextMenu::CodeActions(_) => None,
177 }
178 }
179
180 pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
181 match self {
182 CodeContextMenu::Completions(completions_menu) => completions_menu
183 .get_or_create_entry_markdown(completions_menu.selected_item, cx)
184 .as_ref()
185 .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
186 CodeContextMenu::CodeActions(_) => false,
187 }
188 }
189
190 pub fn scroll_aside(
191 &mut self,
192 scroll_amount: ScrollAmount,
193 window: &mut Window,
194 cx: &mut Context<Editor>,
195 ) {
196 match self {
197 CodeContextMenu::Completions(completions_menu) => {
198 completions_menu.scroll_aside(scroll_amount, window, cx)
199 }
200 CodeContextMenu::CodeActions(_) => (),
201 }
202 }
203}
204
205pub enum ContextMenuOrigin {
206 Cursor,
207 GutterIndicator(DisplayRow),
208 QuickActionBar,
209}
210
211pub struct CompletionsMenu {
212 pub id: CompletionId,
213 pub source: CompletionsMenuSource,
214 sort_completions: bool,
215 pub initial_position: Anchor,
216 pub initial_query: Option<Arc<String>>,
217 pub is_incomplete: bool,
218 pub buffer: Entity<Buffer>,
219 pub completions: Rc<RefCell<Box<[Completion]>>>,
220 /// Match candidates for completions that have `buffer_match = None`
221 match_candidates: Arc<[StringMatchCandidate]>,
222 /// Precomputed `buffer_match` for candidates that have it
223 precomputed_entries: Arc<[StringMatch]>,
224 pub entries: Rc<RefCell<Box<[StringMatch]>>>,
225 pub selected_item: usize,
226 filter_task: Task<()>,
227 cancel_filter: Arc<AtomicBool>,
228 scroll_handle: UniformListScrollHandle,
229 // The `ScrollHandle` used on the Markdown documentation rendered on the
230 // side of the completions menu.
231 pub scroll_handle_aside: ScrollHandle,
232 resolve_completions: bool,
233 show_completion_documentation: bool,
234 last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
235 markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
236 language_registry: Option<Arc<LanguageRegistry>>,
237 language: Option<LanguageName>,
238 display_options: CompletionDisplayOptions,
239 snippet_sort_order: SnippetSortOrder,
240}
241
242#[derive(Clone, Debug, PartialEq)]
243enum MarkdownCacheKey {
244 ForCandidate {
245 candidate_id: usize,
246 },
247 ForCompletionMatch {
248 new_text: String,
249 markdown_source: SharedString,
250 },
251}
252
253#[derive(Copy, Clone, Debug, PartialEq, Eq)]
254pub enum CompletionsMenuSource {
255 Normal,
256 SnippetChoices,
257 Words { ignore_threshold: bool },
258}
259
260// TODO: There should really be a wrapper around fuzzy match tasks that does this.
261impl Drop for CompletionsMenu {
262 fn drop(&mut self) {
263 self.cancel_filter.store(true, Ordering::Relaxed);
264 }
265}
266
267impl CompletionsMenu {
268 pub fn new(
269 id: CompletionId,
270 source: CompletionsMenuSource,
271 sort_completions: bool,
272 show_completion_documentation: bool,
273 initial_position: Anchor,
274 initial_query: Option<Arc<String>>,
275 is_incomplete: bool,
276 buffer: Entity<Buffer>,
277 completions: Box<[Completion]>,
278 display_options: CompletionDisplayOptions,
279 snippet_sort_order: SnippetSortOrder,
280 language_registry: Option<Arc<LanguageRegistry>>,
281 language: Option<LanguageName>,
282 cx: &mut Context<Editor>,
283 ) -> Self {
284 let match_candidates = completions
285 .iter()
286 .enumerate()
287 .filter(|(_id, completion)| completion.buffer_match.is_none())
288 .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
289 .collect();
290 let precomputed_entries = completions
291 .iter()
292 .enumerate()
293 .filter_map(|(id, completion)| {
294 let mut m = completion.buffer_match.clone()?;
295 m.candidate_id = id;
296 Some(m)
297 })
298 .collect();
299
300 let completions_menu = Self {
301 id,
302 source,
303 sort_completions,
304 initial_position,
305 initial_query,
306 is_incomplete,
307 buffer,
308 show_completion_documentation,
309 completions: RefCell::new(completions).into(),
310 match_candidates,
311 precomputed_entries,
312 entries: Rc::new(RefCell::new(Box::new([]))),
313 selected_item: 0,
314 filter_task: Task::ready(()),
315 cancel_filter: Arc::new(AtomicBool::new(false)),
316 scroll_handle: UniformListScrollHandle::new(),
317 scroll_handle_aside: ScrollHandle::new(),
318 resolve_completions: true,
319 last_rendered_range: RefCell::new(None).into(),
320 markdown_cache: RefCell::new(VecDeque::new()).into(),
321 language_registry,
322 language,
323 display_options,
324 snippet_sort_order,
325 };
326
327 completions_menu.start_markdown_parse_for_nearby_entries(cx);
328
329 completions_menu
330 }
331
332 pub fn new_snippet_choices(
333 id: CompletionId,
334 sort_completions: bool,
335 choices: &Vec<String>,
336 selection: Range<Anchor>,
337 buffer: Entity<Buffer>,
338 snippet_sort_order: SnippetSortOrder,
339 ) -> Self {
340 let completions = choices
341 .iter()
342 .map(|choice| Completion {
343 replace_range: selection.start.text_anchor..selection.end.text_anchor,
344 new_text: choice.to_string(),
345 label: CodeLabel::plain(choice.to_string(), None),
346 buffer_match: None,
347 icon_path: None,
348 documentation: None,
349 confirm: None,
350 insert_text_mode: None,
351 source: CompletionSource::Custom,
352 })
353 .collect();
354
355 let match_candidates = choices
356 .iter()
357 .enumerate()
358 .map(|(id, completion)| StringMatchCandidate::new(id, completion))
359 .collect();
360 let entries = choices
361 .iter()
362 .enumerate()
363 .map(|(id, completion)| StringMatch {
364 candidate_id: id,
365 score: 1.,
366 positions: vec![],
367 string: completion.clone(),
368 })
369 .collect();
370 Self {
371 id,
372 source: CompletionsMenuSource::SnippetChoices,
373 sort_completions,
374 initial_position: selection.start,
375 initial_query: None,
376 is_incomplete: false,
377 buffer,
378 completions: RefCell::new(completions).into(),
379 precomputed_entries: Arc::new([]),
380 match_candidates,
381 entries: RefCell::new(entries).into(),
382 selected_item: 0,
383 filter_task: Task::ready(()),
384 cancel_filter: Arc::new(AtomicBool::new(false)),
385 scroll_handle: UniformListScrollHandle::new(),
386 scroll_handle_aside: ScrollHandle::new(),
387 resolve_completions: false,
388 show_completion_documentation: false,
389 last_rendered_range: RefCell::new(None).into(),
390 markdown_cache: RefCell::new(VecDeque::new()).into(),
391 language_registry: None,
392 language: None,
393 display_options: CompletionDisplayOptions::default(),
394 snippet_sort_order,
395 }
396 }
397
398 fn select_first(
399 &mut self,
400 provider: Option<&dyn CompletionProvider>,
401 window: &mut Window,
402 cx: &mut Context<Editor>,
403 ) {
404 let index = if self.scroll_handle.y_flipped() {
405 self.entries.borrow().len() - 1
406 } else {
407 0
408 };
409 self.update_selection_index(index, provider, window, cx);
410 }
411
412 fn select_last(
413 &mut self,
414 provider: Option<&dyn CompletionProvider>,
415 window: &mut Window,
416 cx: &mut Context<Editor>,
417 ) {
418 let index = if self.scroll_handle.y_flipped() {
419 0
420 } else {
421 self.entries.borrow().len() - 1
422 };
423 self.update_selection_index(index, provider, window, cx);
424 }
425
426 fn select_prev(
427 &mut self,
428 provider: Option<&dyn CompletionProvider>,
429 window: &mut Window,
430 cx: &mut Context<Editor>,
431 ) {
432 let index = if self.scroll_handle.y_flipped() {
433 self.next_match_index()
434 } else {
435 self.prev_match_index()
436 };
437 self.update_selection_index(index, provider, window, cx);
438 }
439
440 fn select_next(
441 &mut self,
442 provider: Option<&dyn CompletionProvider>,
443 window: &mut Window,
444 cx: &mut Context<Editor>,
445 ) {
446 let index = if self.scroll_handle.y_flipped() {
447 self.prev_match_index()
448 } else {
449 self.next_match_index()
450 };
451 self.update_selection_index(index, provider, window, cx);
452 }
453
454 fn update_selection_index(
455 &mut self,
456 match_index: usize,
457 provider: Option<&dyn CompletionProvider>,
458 window: &mut Window,
459 cx: &mut Context<Editor>,
460 ) {
461 if self.selected_item != match_index {
462 self.selected_item = match_index;
463 self.handle_selection_changed(provider, window, cx);
464 }
465 }
466
467 fn prev_match_index(&self) -> usize {
468 if self.selected_item > 0 {
469 self.selected_item - 1
470 } else {
471 self.entries.borrow().len() - 1
472 }
473 }
474
475 fn next_match_index(&self) -> usize {
476 if self.selected_item + 1 < self.entries.borrow().len() {
477 self.selected_item + 1
478 } else {
479 0
480 }
481 }
482
483 fn handle_selection_changed(
484 &mut self,
485 provider: Option<&dyn CompletionProvider>,
486 window: &mut Window,
487 cx: &mut Context<Editor>,
488 ) {
489 self.scroll_handle
490 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
491 if let Some(provider) = provider {
492 let entries = self.entries.borrow();
493 let entry = if self.selected_item < entries.len() {
494 Some(&entries[self.selected_item])
495 } else {
496 None
497 };
498 provider.selection_changed(entry, window, cx);
499 }
500 self.resolve_visible_completions(provider, cx);
501 self.start_markdown_parse_for_nearby_entries(cx);
502 cx.notify();
503 }
504
505 pub fn resolve_visible_completions(
506 &mut self,
507 provider: Option<&dyn CompletionProvider>,
508 cx: &mut Context<Editor>,
509 ) {
510 if !self.resolve_completions {
511 return;
512 }
513 let Some(provider) = provider else {
514 return;
515 };
516
517 let entries = self.entries.borrow();
518 if entries.is_empty() {
519 return;
520 }
521 if self.selected_item >= entries.len() {
522 log::error!(
523 "bug: completion selected_item >= entries.len(): {} >= {}",
524 self.selected_item,
525 entries.len()
526 );
527 self.selected_item = entries.len() - 1;
528 }
529
530 // Attempt to resolve completions for every item that will be displayed. This matters
531 // because single line documentation may be displayed inline with the completion.
532 //
533 // When navigating to the very beginning or end of completions, `last_rendered_range` may
534 // have no overlap with the completions that will be displayed, so instead use a range based
535 // on the last rendered count.
536 const APPROXIMATE_VISIBLE_COUNT: usize = 12;
537 let last_rendered_range = self.last_rendered_range.borrow().clone();
538 let visible_count = last_rendered_range
539 .clone()
540 .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
541 let entry_range = if self.selected_item == 0 {
542 0..min(visible_count, entries.len())
543 } else if self.selected_item == entries.len() - 1 {
544 entries.len().saturating_sub(visible_count)..entries.len()
545 } else {
546 last_rendered_range.map_or(0..0, |range| {
547 min(range.start, entries.len())..min(range.end, entries.len())
548 })
549 };
550
551 // Expand the range to resolve more completions than are predicted to be visible, to reduce
552 // jank on navigation.
553 let entry_indices = util::expanded_and_wrapped_usize_range(
554 entry_range,
555 RESOLVE_BEFORE_ITEMS,
556 RESOLVE_AFTER_ITEMS,
557 entries.len(),
558 );
559
560 // Avoid work by sometimes filtering out completions that already have documentation.
561 // This filtering doesn't happen if the completions are currently being updated.
562 let completions = self.completions.borrow();
563 let candidate_ids = entry_indices
564 .map(|i| entries[i].candidate_id)
565 .filter(|i| completions[*i].documentation.is_none());
566
567 // Current selection is always resolved even if it already has documentation, to handle
568 // out-of-spec language servers that return more results later.
569 let selected_candidate_id = entries[self.selected_item].candidate_id;
570 let candidate_ids = iter::once(selected_candidate_id)
571 .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
572 .collect::<Vec<usize>>();
573 drop(entries);
574
575 if candidate_ids.is_empty() {
576 return;
577 }
578
579 let resolve_task = provider.resolve_completions(
580 self.buffer.clone(),
581 candidate_ids,
582 self.completions.clone(),
583 cx,
584 );
585
586 let completion_id = self.id;
587 cx.spawn(async move |editor, cx| {
588 if let Some(true) = resolve_task.await.log_err() {
589 editor
590 .update(cx, |editor, cx| {
591 // `resolve_completions` modified state affecting display.
592 cx.notify();
593 editor.with_completions_menu_matching_id(completion_id, |menu| {
594 if let Some(menu) = menu {
595 menu.start_markdown_parse_for_nearby_entries(cx)
596 }
597 });
598 })
599 .ok();
600 }
601 })
602 .detach();
603 }
604
605 fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
606 // Enqueue parse tasks of nearer items first.
607 //
608 // TODO: This means that the nearer items will actually be further back in the cache, which
609 // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
610 // selection to the front (when `is_render = true`).
611 let entry_indices = util::wrapped_usize_outward_from(
612 self.selected_item,
613 MARKDOWN_CACHE_BEFORE_ITEMS,
614 MARKDOWN_CACHE_AFTER_ITEMS,
615 self.entries.borrow().len(),
616 );
617
618 for index in entry_indices {
619 self.get_or_create_entry_markdown(index, cx);
620 }
621 }
622
623 fn get_or_create_entry_markdown(
624 &self,
625 index: usize,
626 cx: &mut Context<Editor>,
627 ) -> Option<Entity<Markdown>> {
628 let entries = self.entries.borrow();
629 if index >= entries.len() {
630 return None;
631 }
632 let candidate_id = entries[index].candidate_id;
633 let completions = self.completions.borrow();
634 match &completions[candidate_id].documentation {
635 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
636 .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
637 .map(|(_, markdown)| markdown),
638 Some(_) => None,
639 _ => None,
640 }
641 }
642
643 fn get_or_create_markdown(
644 &self,
645 candidate_id: usize,
646 source: Option<&SharedString>,
647 is_render: bool,
648 completions: &[Completion],
649 cx: &mut Context<Editor>,
650 ) -> Option<(bool, Entity<Markdown>)> {
651 let mut markdown_cache = self.markdown_cache.borrow_mut();
652
653 let mut has_completion_match_cache_entry = false;
654 let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
655 MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
656 MarkdownCacheKey::ForCompletionMatch { .. } => {
657 has_completion_match_cache_entry = true;
658 false
659 }
660 });
661
662 if has_completion_match_cache_entry && matching_entry.is_none() {
663 if let Some(source) = source {
664 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
665 matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
666 if markdown_source == source)
667 });
668 } else {
669 // Heuristic guess that documentation can be reused when new_text matches. This is
670 // to mitigate documentation flicker while typing. If this is wrong, then resolution
671 // should cause the correct documentation to be displayed soon.
672 let completion = &completions[candidate_id];
673 matching_entry = markdown_cache.iter().find_position(|(key, _)| {
674 matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
675 if new_text == &completion.new_text)
676 });
677 }
678 }
679
680 if let Some((cache_index, (key, markdown))) = matching_entry {
681 let markdown = markdown.clone();
682
683 // Since the markdown source matches, the key can now be ForCandidate.
684 if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
685 markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
686 }
687
688 if is_render && cache_index != 0 {
689 // Move the current selection's cache entry to the front.
690 markdown_cache.rotate_right(1);
691 let cache_len = markdown_cache.len();
692 markdown_cache.swap(0, (cache_index + 1) % cache_len);
693 }
694
695 let is_parsing = markdown.update(cx, |markdown, cx| {
696 if let Some(source) = source {
697 // `reset` is called as it's possible for documentation to change due to resolve
698 // requests. It does nothing if `source` is unchanged.
699 markdown.reset(source.clone(), cx);
700 }
701 markdown.is_parsing()
702 });
703 return Some((is_parsing, markdown));
704 }
705
706 let Some(source) = source else {
707 // Can't create markdown as there is no source.
708 return None;
709 };
710
711 if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
712 let markdown = cx.new(|cx| {
713 Markdown::new(
714 source.clone(),
715 self.language_registry.clone(),
716 self.language.clone(),
717 cx,
718 )
719 });
720 // Handles redraw when the markdown is done parsing. The current render is for a
721 // deferred draw, and so without this did not redraw when `markdown` notified.
722 cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
723 markdown_cache.push_front((
724 MarkdownCacheKey::ForCandidate { candidate_id },
725 markdown.clone(),
726 ));
727 Some((true, markdown))
728 } else {
729 debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
730 // Moves the last cache entry to the start. The ring buffer is full, so this does no
731 // copying and just shifts indexes.
732 markdown_cache.rotate_right(1);
733 markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
734 let markdown = &markdown_cache[0].1;
735 markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
736 Some((true, markdown.clone()))
737 }
738 }
739
740 pub fn visible(&self) -> bool {
741 !self.entries.borrow().is_empty()
742 }
743
744 fn origin(&self) -> ContextMenuOrigin {
745 ContextMenuOrigin::Cursor
746 }
747
748 fn render(
749 &self,
750 style: &EditorStyle,
751 max_height_in_lines: u32,
752 window: &mut Window,
753 cx: &mut Context<Editor>,
754 ) -> AnyElement {
755 let show_completion_documentation = self.show_completion_documentation;
756 let widest_completion_ix = if self.display_options.dynamic_width {
757 let completions = self.completions.borrow();
758 let widest_completion_ix = self
759 .entries
760 .borrow()
761 .iter()
762 .enumerate()
763 .max_by_key(|(_, mat)| {
764 let completion = &completions[mat.candidate_id];
765 let documentation = &completion.documentation;
766
767 let mut len = completion.label.text.chars().count();
768 if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
769 if show_completion_documentation {
770 len += text.chars().count();
771 }
772 }
773
774 len
775 })
776 .map(|(ix, _)| ix);
777 drop(completions);
778 widest_completion_ix
779 } else {
780 None
781 };
782
783 let selected_item = self.selected_item;
784 let completions = self.completions.clone();
785 let entries = self.entries.clone();
786 let last_rendered_range = self.last_rendered_range.clone();
787 let style = style.clone();
788 let list = uniform_list(
789 "completions",
790 self.entries.borrow().len(),
791 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
792 last_rendered_range.borrow_mut().replace(range.clone());
793 let start_ix = range.start;
794 let completions_guard = completions.borrow_mut();
795
796 entries.borrow()[range]
797 .iter()
798 .enumerate()
799 .map(|(ix, mat)| {
800 let item_ix = start_ix + ix;
801 let completion = &completions_guard[mat.candidate_id];
802 let documentation = if show_completion_documentation {
803 &completion.documentation
804 } else {
805 &None
806 };
807
808 let filter_start = completion.label.filter_range.start;
809 let highlights = gpui::combine_highlights(
810 mat.ranges().map(|range| {
811 (
812 filter_start + range.start..filter_start + range.end,
813 FontWeight::BOLD.into(),
814 )
815 }),
816 styled_runs_for_code_label(&completion.label, &style.syntax).map(
817 |(range, mut highlight)| {
818 // Ignore font weight for syntax highlighting, as we'll use it
819 // for fuzzy matches.
820 highlight.font_weight = None;
821 if completion
822 .source
823 .lsp_completion(false)
824 .and_then(|lsp_completion| lsp_completion.deprecated)
825 .unwrap_or(false)
826 {
827 highlight.strikethrough = Some(StrikethroughStyle {
828 thickness: 1.0.into(),
829 ..Default::default()
830 });
831 highlight.color = Some(cx.theme().colors().text_muted);
832 }
833
834 (range, highlight)
835 },
836 ),
837 );
838
839 let completion_label = StyledText::new(completion.label.text.clone())
840 .with_default_highlights(&style.text, highlights);
841
842 let documentation_label = match documentation {
843 Some(CompletionDocumentation::SingleLine(text))
844 | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
845 single_line: text,
846 ..
847 }) => {
848 if text.trim().is_empty() {
849 None
850 } else {
851 Some(
852 Label::new(text.clone())
853 .ml_4()
854 .size(LabelSize::Small)
855 .color(Color::Muted),
856 )
857 }
858 }
859 _ => None,
860 };
861
862 let start_slot = completion
863 .color()
864 .map(|color| {
865 div()
866 .flex_shrink_0()
867 .size_3p5()
868 .rounded_xs()
869 .bg(color)
870 .into_any_element()
871 })
872 .or_else(|| {
873 completion.icon_path.as_ref().map(|path| {
874 Icon::from_path(path)
875 .size(IconSize::XSmall)
876 .color(Color::Muted)
877 .into_any_element()
878 })
879 });
880
881 div().min_w(px(280.)).max_w(px(540.)).child(
882 ListItem::new(mat.candidate_id)
883 .inset(true)
884 .toggle_state(item_ix == selected_item)
885 .on_click(cx.listener(move |editor, _event, window, cx| {
886 cx.stop_propagation();
887 if let Some(task) = editor.confirm_completion(
888 &ConfirmCompletion {
889 item_ix: Some(item_ix),
890 },
891 window,
892 cx,
893 ) {
894 task.detach_and_log_err(cx)
895 }
896 }))
897 .start_slot::<AnyElement>(start_slot)
898 .child(h_flex().overflow_hidden().child(completion_label))
899 .end_slot::<Label>(documentation_label),
900 )
901 })
902 .collect()
903 }),
904 )
905 .occlude()
906 .max_h(max_height_in_lines as f32 * window.line_height())
907 .track_scroll(self.scroll_handle.clone())
908 .with_sizing_behavior(ListSizingBehavior::Infer)
909 .map(|this| {
910 if self.display_options.dynamic_width {
911 this.with_width_from_item(widest_completion_ix)
912 } else {
913 this.w(rems(34.))
914 }
915 });
916
917 Popover::new().child(list).into_any_element()
918 }
919
920 fn render_aside(
921 &mut self,
922 max_size: Size<Pixels>,
923 window: &mut Window,
924 cx: &mut Context<Editor>,
925 ) -> Option<AnyElement> {
926 if !self.show_completion_documentation {
927 return None;
928 }
929
930 let mat = &self.entries.borrow()[self.selected_item];
931 let completions = self.completions.borrow();
932 let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
933 Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
934 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
935 plain_text: Some(text),
936 ..
937 }) => div().child(text.clone()),
938 Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
939 let Some((false, markdown)) = self.get_or_create_markdown(
940 mat.candidate_id,
941 Some(source),
942 true,
943 &completions,
944 cx,
945 ) else {
946 return None;
947 };
948 Self::render_markdown(markdown, window, cx)
949 }
950 None => {
951 // Handle the case where documentation hasn't yet been resolved but there's a
952 // `new_text` match in the cache.
953 //
954 // TODO: It's inconsistent that documentation caching based on matching `new_text`
955 // only works for markdown. Consider generally caching the results of resolving
956 // completions.
957 let Some((false, markdown)) =
958 self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
959 else {
960 return None;
961 };
962 Self::render_markdown(markdown, window, cx)
963 }
964 Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
965 Some(CompletionDocumentation::SingleLine(_)) => return None,
966 Some(CompletionDocumentation::Undocumented) => return None,
967 Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
968 plain_text: None,
969 ..
970 }) => {
971 return None;
972 }
973 };
974
975 Some(
976 Popover::new()
977 .child(
978 multiline_docs
979 .id("multiline_docs")
980 .px(MENU_ASIDE_X_PADDING / 2.)
981 .max_w(max_size.width)
982 .max_h(max_size.height)
983 .overflow_y_scroll()
984 .track_scroll(&self.scroll_handle_aside)
985 .occlude(),
986 )
987 .into_any_element(),
988 )
989 }
990
991 fn render_markdown(
992 markdown: Entity<Markdown>,
993 window: &mut Window,
994 cx: &mut Context<Editor>,
995 ) -> Div {
996 div().child(
997 MarkdownElement::new(markdown, hover_markdown_style(window, cx))
998 .code_block_renderer(markdown::CodeBlockRenderer::Default {
999 copy_button: false,
1000 copy_button_on_hover: false,
1001 border: false,
1002 })
1003 .on_url_click(open_markdown_url),
1004 )
1005 }
1006
1007 pub fn filter(
1008 &mut self,
1009 query: Option<Arc<String>>,
1010 provider: Option<Rc<dyn CompletionProvider>>,
1011 window: &mut Window,
1012 cx: &mut Context<Editor>,
1013 ) {
1014 self.cancel_filter.store(true, Ordering::Relaxed);
1015 if let Some(query) = query {
1016 self.cancel_filter = Arc::new(AtomicBool::new(false));
1017 let matches = self.do_async_filtering(query, cx);
1018 let id = self.id;
1019 self.filter_task = cx.spawn_in(window, async move |editor, cx| {
1020 let matches = matches.await;
1021 editor
1022 .update_in(cx, |editor, window, cx| {
1023 editor.with_completions_menu_matching_id(id, |this| {
1024 if let Some(this) = this {
1025 this.set_filter_results(matches, provider, window, cx);
1026 }
1027 });
1028 })
1029 .ok();
1030 });
1031 } else {
1032 self.filter_task = Task::ready(());
1033 let matches = self.unfiltered_matches();
1034 self.set_filter_results(matches, provider, window, cx);
1035 }
1036 }
1037
1038 pub fn do_async_filtering(
1039 &self,
1040 query: Arc<String>,
1041 cx: &Context<Editor>,
1042 ) -> Task<Vec<StringMatch>> {
1043 let matches_task = cx.background_spawn({
1044 let query = query.clone();
1045 let match_candidates = self.match_candidates.clone();
1046 let precomputed_entries = self.precomputed_entries.clone();
1047 let cancel_filter = self.cancel_filter.clone();
1048 let background_executor = cx.background_executor().clone();
1049 async move {
1050 let mut matches = fuzzy::match_strings(
1051 &match_candidates,
1052 &query,
1053 query.chars().any(|c| c.is_uppercase()),
1054 false,
1055 1000,
1056 &cancel_filter,
1057 background_executor,
1058 )
1059 .await;
1060 matches.extend(precomputed_entries.iter().cloned());
1061 matches
1062 }
1063 });
1064
1065 let completions = self.completions.clone();
1066 let sort_completions = self.sort_completions;
1067 let snippet_sort_order = self.snippet_sort_order;
1068 cx.foreground_executor().spawn(async move {
1069 let mut matches = matches_task.await;
1070
1071 if sort_completions {
1072 matches = Self::sort_string_matches(
1073 matches,
1074 Some(&query),
1075 snippet_sort_order,
1076 completions.borrow().as_ref(),
1077 );
1078 }
1079
1080 matches
1081 })
1082 }
1083
1084 /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
1085 pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
1086 let mut matches = self
1087 .match_candidates
1088 .iter()
1089 .enumerate()
1090 .map(|(candidate_id, candidate)| StringMatch {
1091 candidate_id,
1092 score: Default::default(),
1093 positions: Default::default(),
1094 string: candidate.string.clone(),
1095 })
1096 .chain(self.precomputed_entries.iter().cloned())
1097 .collect();
1098
1099 if self.sort_completions {
1100 matches = Self::sort_string_matches(
1101 matches,
1102 None,
1103 self.snippet_sort_order,
1104 self.completions.borrow().as_ref(),
1105 );
1106 }
1107
1108 matches
1109 }
1110
1111 pub fn set_filter_results(
1112 &mut self,
1113 matches: Vec<StringMatch>,
1114 provider: Option<Rc<dyn CompletionProvider>>,
1115 window: &mut Window,
1116 cx: &mut Context<Editor>,
1117 ) {
1118 *self.entries.borrow_mut() = matches.into_boxed_slice();
1119 self.selected_item = 0;
1120 self.handle_selection_changed(provider.as_deref(), window, cx);
1121 }
1122
1123 pub fn sort_string_matches(
1124 matches: Vec<StringMatch>,
1125 query: Option<&str>,
1126 snippet_sort_order: SnippetSortOrder,
1127 completions: &[Completion],
1128 ) -> Vec<StringMatch> {
1129 let mut matches = matches;
1130
1131 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1132 enum MatchTier<'a> {
1133 WordStartMatch {
1134 sort_exact: Reverse<i32>,
1135 sort_snippet: Reverse<i32>,
1136 sort_score: Reverse<OrderedFloat<f64>>,
1137 sort_positions: Vec<usize>,
1138 sort_text: Option<&'a str>,
1139 sort_kind: usize,
1140 sort_label: &'a str,
1141 },
1142 OtherMatch {
1143 sort_score: Reverse<OrderedFloat<f64>>,
1144 },
1145 }
1146
1147 let query_start_lower = query
1148 .as_ref()
1149 .and_then(|q| q.chars().next())
1150 .and_then(|c| c.to_lowercase().next());
1151
1152 if snippet_sort_order == SnippetSortOrder::None {
1153 matches.retain(|string_match| !completions[string_match.candidate_id].is_snippet());
1154 }
1155
1156 matches.sort_unstable_by_key(|string_match| {
1157 let completion = &completions[string_match.candidate_id];
1158
1159 let sort_text = match &completion.source {
1160 CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1161 CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1162 _ => None,
1163 };
1164
1165 let (sort_kind, sort_label) = completion.sort_key();
1166
1167 let score = string_match.score;
1168 let sort_score = Reverse(OrderedFloat(score));
1169
1170 // Snippets do their own first-letter matching logic elsewhere.
1171 let is_snippet = completion.is_snippet();
1172 let query_start_doesnt_match_split_words = !is_snippet
1173 && query_start_lower
1174 .map(|query_char| {
1175 !split_words(&string_match.string).any(|word| {
1176 word.chars().next().and_then(|c| c.to_lowercase().next())
1177 == Some(query_char)
1178 })
1179 })
1180 .unwrap_or(false);
1181
1182 if query_start_doesnt_match_split_words {
1183 MatchTier::OtherMatch { sort_score }
1184 } else {
1185 let sort_snippet = match snippet_sort_order {
1186 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1187 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1188 SnippetSortOrder::Inline => Reverse(0),
1189 SnippetSortOrder::None => Reverse(0),
1190 };
1191 let sort_positions = string_match.positions.clone();
1192 let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1193 1
1194 } else {
1195 0
1196 });
1197
1198 MatchTier::WordStartMatch {
1199 sort_exact,
1200 sort_snippet,
1201 sort_score,
1202 sort_positions,
1203 sort_text,
1204 sort_kind,
1205 sort_label,
1206 }
1207 }
1208 });
1209
1210 matches
1211 }
1212
1213 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1214 self.markdown_cache = prev_menu.markdown_cache.clone();
1215
1216 // Convert ForCandidate cache keys to ForCompletionMatch keys.
1217 let prev_completions = prev_menu.completions.borrow();
1218 self.markdown_cache
1219 .borrow_mut()
1220 .retain_mut(|(key, _markdown)| match key {
1221 MarkdownCacheKey::ForCompletionMatch { .. } => true,
1222 MarkdownCacheKey::ForCandidate { candidate_id } => {
1223 if let Some(completion) = prev_completions.get(*candidate_id) {
1224 match &completion.documentation {
1225 Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1226 *key = MarkdownCacheKey::ForCompletionMatch {
1227 new_text: completion.new_text.clone(),
1228 markdown_source: source.clone(),
1229 };
1230 true
1231 }
1232 _ => false,
1233 }
1234 } else {
1235 false
1236 }
1237 }
1238 });
1239 }
1240
1241 pub fn scroll_aside(
1242 &mut self,
1243 amount: ScrollAmount,
1244 window: &mut Window,
1245 cx: &mut Context<Editor>,
1246 ) {
1247 let mut offset = self.scroll_handle_aside.offset();
1248
1249 offset.y -= amount.pixels(
1250 window.line_height(),
1251 self.scroll_handle_aside.bounds().size.height - px(16.),
1252 ) / 2.0;
1253
1254 cx.notify();
1255 self.scroll_handle_aside.set_offset(offset);
1256 }
1257}
1258
1259#[derive(Clone)]
1260pub struct AvailableCodeAction {
1261 pub excerpt_id: ExcerptId,
1262 pub action: CodeAction,
1263 pub provider: Rc<dyn CodeActionProvider>,
1264}
1265
1266#[derive(Clone)]
1267pub struct CodeActionContents {
1268 tasks: Option<Rc<ResolvedTasks>>,
1269 actions: Option<Rc<[AvailableCodeAction]>>,
1270 debug_scenarios: Vec<DebugScenario>,
1271 pub(crate) context: TaskContext,
1272}
1273
1274impl CodeActionContents {
1275 pub(crate) fn new(
1276 tasks: Option<ResolvedTasks>,
1277 actions: Option<Rc<[AvailableCodeAction]>>,
1278 debug_scenarios: Vec<DebugScenario>,
1279 context: TaskContext,
1280 ) -> Self {
1281 Self {
1282 tasks: tasks.map(Rc::new),
1283 actions,
1284 debug_scenarios,
1285 context,
1286 }
1287 }
1288
1289 pub fn tasks(&self) -> Option<&ResolvedTasks> {
1290 self.tasks.as_deref()
1291 }
1292
1293 fn len(&self) -> usize {
1294 let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1295 let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1296 tasks_len + code_actions_len + self.debug_scenarios.len()
1297 }
1298
1299 pub fn is_empty(&self) -> bool {
1300 self.len() == 0
1301 }
1302
1303 fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1304 self.tasks
1305 .iter()
1306 .flat_map(|tasks| {
1307 tasks
1308 .templates
1309 .iter()
1310 .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1311 })
1312 .chain(self.actions.iter().flat_map(|actions| {
1313 actions.iter().map(|available| CodeActionsItem::CodeAction {
1314 excerpt_id: available.excerpt_id,
1315 action: available.action.clone(),
1316 provider: available.provider.clone(),
1317 })
1318 }))
1319 .chain(
1320 self.debug_scenarios
1321 .iter()
1322 .cloned()
1323 .map(CodeActionsItem::DebugScenario),
1324 )
1325 }
1326
1327 pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1328 if let Some(tasks) = &self.tasks {
1329 if let Some((kind, task)) = tasks.templates.get(index) {
1330 return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1331 } else {
1332 index -= tasks.templates.len();
1333 }
1334 }
1335 if let Some(actions) = &self.actions {
1336 if let Some(available) = actions.get(index) {
1337 return Some(CodeActionsItem::CodeAction {
1338 excerpt_id: available.excerpt_id,
1339 action: available.action.clone(),
1340 provider: available.provider.clone(),
1341 });
1342 } else {
1343 index -= actions.len();
1344 }
1345 }
1346
1347 self.debug_scenarios
1348 .get(index)
1349 .cloned()
1350 .map(CodeActionsItem::DebugScenario)
1351 }
1352}
1353
1354#[derive(Clone)]
1355pub enum CodeActionsItem {
1356 Task(TaskSourceKind, ResolvedTask),
1357 CodeAction {
1358 excerpt_id: ExcerptId,
1359 action: CodeAction,
1360 provider: Rc<dyn CodeActionProvider>,
1361 },
1362 DebugScenario(DebugScenario),
1363}
1364
1365impl CodeActionsItem {
1366 fn as_task(&self) -> Option<&ResolvedTask> {
1367 let Self::Task(_, task) = self else {
1368 return None;
1369 };
1370 Some(task)
1371 }
1372
1373 fn as_code_action(&self) -> Option<&CodeAction> {
1374 let Self::CodeAction { action, .. } = self else {
1375 return None;
1376 };
1377 Some(action)
1378 }
1379 fn as_debug_scenario(&self) -> Option<&DebugScenario> {
1380 let Self::DebugScenario(scenario) = self else {
1381 return None;
1382 };
1383 Some(scenario)
1384 }
1385
1386 pub fn label(&self) -> String {
1387 match self {
1388 Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1389 Self::Task(_, task) => task.resolved_label.clone(),
1390 Self::DebugScenario(scenario) => scenario.label.to_string(),
1391 }
1392 }
1393}
1394
1395pub struct CodeActionsMenu {
1396 pub actions: CodeActionContents,
1397 pub buffer: Entity<Buffer>,
1398 pub selected_item: usize,
1399 pub scroll_handle: UniformListScrollHandle,
1400 pub deployed_from: Option<CodeActionSource>,
1401}
1402
1403impl CodeActionsMenu {
1404 fn select_first(&mut self, cx: &mut Context<Editor>) {
1405 self.selected_item = if self.scroll_handle.y_flipped() {
1406 self.actions.len() - 1
1407 } else {
1408 0
1409 };
1410 self.scroll_handle
1411 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1412 cx.notify()
1413 }
1414
1415 fn select_last(&mut self, cx: &mut Context<Editor>) {
1416 self.selected_item = if self.scroll_handle.y_flipped() {
1417 0
1418 } else {
1419 self.actions.len() - 1
1420 };
1421 self.scroll_handle
1422 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1423 cx.notify()
1424 }
1425
1426 fn select_prev(&mut self, cx: &mut Context<Editor>) {
1427 self.selected_item = if self.scroll_handle.y_flipped() {
1428 self.next_match_index()
1429 } else {
1430 self.prev_match_index()
1431 };
1432 self.scroll_handle
1433 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1434 cx.notify();
1435 }
1436
1437 fn select_next(&mut self, cx: &mut Context<Editor>) {
1438 self.selected_item = if self.scroll_handle.y_flipped() {
1439 self.prev_match_index()
1440 } else {
1441 self.next_match_index()
1442 };
1443 self.scroll_handle
1444 .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1445 cx.notify();
1446 }
1447
1448 fn prev_match_index(&self) -> usize {
1449 if self.selected_item > 0 {
1450 self.selected_item - 1
1451 } else {
1452 self.actions.len() - 1
1453 }
1454 }
1455
1456 fn next_match_index(&self) -> usize {
1457 if self.selected_item + 1 < self.actions.len() {
1458 self.selected_item + 1
1459 } else {
1460 0
1461 }
1462 }
1463
1464 pub fn visible(&self) -> bool {
1465 !self.actions.is_empty()
1466 }
1467
1468 fn origin(&self) -> ContextMenuOrigin {
1469 match &self.deployed_from {
1470 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1471 ContextMenuOrigin::GutterIndicator(*row)
1472 }
1473 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1474 None => ContextMenuOrigin::Cursor,
1475 }
1476 }
1477
1478 fn render(
1479 &self,
1480 _style: &EditorStyle,
1481 max_height_in_lines: u32,
1482 window: &mut Window,
1483 cx: &mut Context<Editor>,
1484 ) -> AnyElement {
1485 let actions = self.actions.clone();
1486 let selected_item = self.selected_item;
1487 let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1488
1489 let list = uniform_list(
1490 "code_actions_menu",
1491 self.actions.len(),
1492 cx.processor(move |_this, range: Range<usize>, _, cx| {
1493 actions
1494 .iter()
1495 .skip(range.start)
1496 .take(range.end - range.start)
1497 .enumerate()
1498 .map(|(ix, action)| {
1499 let item_ix = range.start + ix;
1500 let selected = item_ix == selected_item;
1501 let colors = cx.theme().colors();
1502 div().min_w(px(220.)).max_w(px(540.)).child(
1503 ListItem::new(item_ix)
1504 .inset(true)
1505 .toggle_state(selected)
1506 .when_some(action.as_code_action(), |this, action| {
1507 this.child(
1508 h_flex()
1509 .overflow_hidden()
1510 .when(is_quick_action_bar, |this| this.text_ui(cx))
1511 .child(
1512 // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
1513 action.lsp_action.title().replace("\n", ""),
1514 )
1515 .when(selected, |this| {
1516 this.text_color(colors.text_accent)
1517 }),
1518 )
1519 })
1520 .when_some(action.as_task(), |this, task| {
1521 this.child(
1522 h_flex()
1523 .overflow_hidden()
1524 .when(is_quick_action_bar, |this| this.text_ui(cx))
1525 .child(task.resolved_label.replace("\n", ""))
1526 .when(selected, |this| {
1527 this.text_color(colors.text_accent)
1528 }),
1529 )
1530 })
1531 .when_some(action.as_debug_scenario(), |this, scenario| {
1532 this.child(
1533 h_flex()
1534 .overflow_hidden()
1535 .when(is_quick_action_bar, |this| this.text_ui(cx))
1536 .child("debug: ")
1537 .child(scenario.label.clone())
1538 .when(selected, |this| {
1539 this.text_color(colors.text_accent)
1540 }),
1541 )
1542 })
1543 .on_click(cx.listener(move |editor, _, window, cx| {
1544 cx.stop_propagation();
1545 if let Some(task) = editor.confirm_code_action(
1546 &ConfirmCodeAction {
1547 item_ix: Some(item_ix),
1548 },
1549 window,
1550 cx,
1551 ) {
1552 task.detach_and_log_err(cx)
1553 }
1554 })),
1555 )
1556 })
1557 .collect()
1558 }),
1559 )
1560 .occlude()
1561 .max_h(max_height_in_lines as f32 * window.line_height())
1562 .track_scroll(self.scroll_handle.clone())
1563 .with_width_from_item(
1564 self.actions
1565 .iter()
1566 .enumerate()
1567 .max_by_key(|(_, action)| match action {
1568 CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1569 CodeActionsItem::CodeAction { action, .. } => {
1570 action.lsp_action.title().chars().count()
1571 }
1572 CodeActionsItem::DebugScenario(scenario) => {
1573 format!("debug: {}", scenario.label).chars().count()
1574 }
1575 })
1576 .map(|(ix, _)| ix),
1577 )
1578 .with_sizing_behavior(ListSizingBehavior::Infer);
1579
1580 Popover::new().child(list).into_any_element()
1581 }
1582}