Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:31:20.778Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

code_context_menus.rs

2094 lines · 77.3 KB · rust
1use crate::scroll::ScrollAmount;
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{
4    AnyElement, Entity, Focusable, FontWeight, HighlightStyle, ListSizingBehavior, ScrollHandle,
5    ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText, Task, TaskExt,
6    UniformListScrollHandle, div, px, uniform_list,
7};
8use itertools::Itertools;
9use language::CodeLabel;
10use language::{Buffer, LanguageName, LanguageRegistry};
11use lsp::{CompletionItemKind, CompletionItemTag};
12use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
13use multi_buffer::Anchor;
14use ordered_float::OrderedFloat;
15use project::lsp_store::CompletionDocumentation;
16use project::{CodeAction, Completion, CompletionGroup, TaskSourceKind};
17use project::{CompletionDisplayOptions, CompletionSource};
18use task::DebugScenario;
19use task::TaskContext;
20
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::{
32    Divider, ListItem, ListSubHeader, Popover, ScrollAxes, Scrollbars, Tooltip, WithScrollbar,
33    prelude::*,
34};
35use util::ResultExt;
36
37use crate::hover_popover::{hover_markdown_style, open_markdown_url};
38use crate::{
39    CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
40    ResolvedTasks,
41    actions::{ConfirmCodeAction, ConfirmCompletion},
42    split_words, styled_runs_for_code_label,
43};
44use crate::{CodeActionSource, EditorSettings};
45use collections::{HashSet, VecDeque};
46use settings::{CompletionDetailAlignment, CompletionMenuItemKind, Settings, SnippetSortOrder};
47
48pub const MENU_GAP: Pixels = px(4.);
49pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
50pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
51pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
52pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
53pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
54pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
55pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
56
57// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
58// documentation not yet being parsed.
59//
60// The size of the cache is set to 16, which is roughly 3 times more than the number of items
61// fetched around the current selection. This way documentation is more often ready for render when
62// revisiting previous entries, such as when pressing backspace.
63const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
64const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
65const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
66
67// Number of items beyond the visible items to resolve documentation.
68const RESOLVE_BEFORE_ITEMS: usize = 4;
69const RESOLVE_AFTER_ITEMS: usize = 4;
70
71#[derive(Clone, Debug)]
72pub enum CompletionMenuEntry {
73    Match(StringMatch),
74    Divider,
75    GroupHeader(SharedString),
76}
77
78impl CompletionMenuEntry {
79    pub fn as_match(&self) -> Option<&StringMatch> {
80        match self {
81            CompletionMenuEntry::Match(m) => Some(m),
82            CompletionMenuEntry::Divider | CompletionMenuEntry::GroupHeader(_) => None,
83        }
84    }
85
86    pub fn is_selectable(&self) -> bool {
87        matches!(self, CompletionMenuEntry::Match(_))
88    }
89}
90
91pub enum CodeContextMenu {
92    Completions(CompletionsMenu),
93    CodeActions(CodeActionsMenu),
94}
95
96impl CodeContextMenu {
97    pub fn select_first(
98        &mut self,
99        provider: Option<&dyn CompletionProvider>,
100        window: &mut Window,
101        cx: &mut Context<Editor>,
102    ) -> bool {
103        if self.visible() {
104            match self {
105                CodeContextMenu::Completions(menu) => menu.select_first(provider, window, cx),
106                CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
107            }
108            true
109        } else {
110            false
111        }
112    }
113
114    pub fn select_prev(
115        &mut self,
116        provider: Option<&dyn CompletionProvider>,
117        window: &mut Window,
118        cx: &mut Context<Editor>,
119    ) -> bool {
120        if self.visible() {
121            match self {
122                CodeContextMenu::Completions(menu) => menu.select_prev(provider, window, cx),
123                CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
124            }
125            true
126        } else {
127            false
128        }
129    }
130
131    pub fn select_next(
132        &mut self,
133        provider: Option<&dyn CompletionProvider>,
134        window: &mut Window,
135        cx: &mut Context<Editor>,
136    ) -> bool {
137        if self.visible() {
138            match self {
139                CodeContextMenu::Completions(menu) => menu.select_next(provider, window, cx),
140                CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
141            }
142            true
143        } else {
144            false
145        }
146    }
147
148    pub fn select_last(
149        &mut self,
150        provider: Option<&dyn CompletionProvider>,
151        window: &mut Window,
152        cx: &mut Context<Editor>,
153    ) -> bool {
154        if self.visible() {
155            match self {
156                CodeContextMenu::Completions(menu) => menu.select_last(provider, window, cx),
157                CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
158            }
159            true
160        } else {
161            false
162        }
163    }
164
165    pub fn visible(&self) -> bool {
166        match self {
167            CodeContextMenu::Completions(menu) => menu.visible(),
168            CodeContextMenu::CodeActions(menu) => menu.visible(),
169        }
170    }
171
172    pub fn origin(&self) -> ContextMenuOrigin {
173        match self {
174            CodeContextMenu::Completions(menu) => menu.origin(),
175            CodeContextMenu::CodeActions(menu) => menu.origin(),
176        }
177    }
178
179    pub fn render(
180        &self,
181        style: &EditorStyle,
182        max_height_in_lines: u32,
183        window: &mut Window,
184        cx: &mut Context<Editor>,
185    ) -> AnyElement {
186        match self {
187            CodeContextMenu::Completions(menu) => {
188                menu.render(style, max_height_in_lines, window, cx)
189            }
190            CodeContextMenu::CodeActions(menu) => {
191                menu.render(style, max_height_in_lines, window, cx)
192            }
193        }
194    }
195
196    pub fn render_aside(
197        &mut self,
198        max_size: Size<Pixels>,
199        window: &mut Window,
200        cx: &mut Context<Editor>,
201    ) -> Option<AnyElement> {
202        match self {
203            CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
204            CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
205        }
206    }
207
208    pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
209        match self {
210            CodeContextMenu::Completions(completions_menu) => completions_menu
211                .get_or_create_entry_markdown(completions_menu.selected_item, cx)
212                .as_ref()
213                .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),
214            CodeContextMenu::CodeActions(_) => false,
215        }
216    }
217
218    pub fn scroll_aside(
219        &mut self,
220        scroll_amount: ScrollAmount,
221        window: &mut Window,
222        cx: &mut Context<Editor>,
223    ) {
224        match self {
225            CodeContextMenu::Completions(completions_menu) => {
226                completions_menu.scroll_aside(scroll_amount, window, cx)
227            }
228            CodeContextMenu::CodeActions(_) => (),
229        }
230    }
231
232    pub fn primary_scroll_handle(&self) -> UniformListScrollHandle {
233        match self {
234            CodeContextMenu::Completions(menu) => menu.scroll_handle.clone(),
235            CodeContextMenu::CodeActions(menu) => menu.scroll_handle.clone(),
236        }
237    }
238}
239
240pub enum ContextMenuOrigin {
241    Cursor,
242    GutterIndicator(DisplayRow),
243    QuickActionBar,
244}
245
246pub struct CompletionsMenu {
247    pub id: CompletionId,
248    pub source: CompletionsMenuSource,
249    sort_completions: bool,
250    pub initial_position: Anchor,
251    pub initial_query: Option<Arc<String>>,
252    pub is_incomplete: bool,
253    pub buffer: Entity<Buffer>,
254    pub completions: Rc<RefCell<Box<[Completion]>>>,
255    /// String match candidate for each completion, grouped by `match_start`.
256    match_candidates: Arc<[(Option<text::Anchor>, Vec<StringMatchCandidate>)]>,
257    /// Entries displayed in the menu, which is a filtered and sorted subset of `match_candidates`.
258    pub entries: Rc<RefCell<Box<[CompletionMenuEntry]>>>,
259    pub selected_item: usize,
260    filter_task: Task<()>,
261    cancel_filter: Arc<AtomicBool>,
262    scroll_handle: UniformListScrollHandle,
263    // The `ScrollHandle` used on the Markdown documentation rendered on the
264    // side of the completions menu.
265    pub scroll_handle_aside: ScrollHandle,
266    resolve_completions: bool,
267    show_completion_documentation: bool,
268    last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
269    markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
270    language_registry: Option<Arc<LanguageRegistry>>,
271    language: Option<LanguageName>,
272    display_options: CompletionDisplayOptions,
273    snippet_sort_order: SnippetSortOrder,
274}
275
276#[derive(Clone, Debug, PartialEq)]
277enum MarkdownCacheKey {
278    ForCandidate {
279        candidate_id: usize,
280    },
281    ForCompletionMatch {
282        new_text: String,
283        markdown_source: SharedString,
284    },
285}
286
287#[derive(Copy, Clone, Debug, PartialEq, Eq)]
288pub enum CompletionsMenuSource {
289    /// Show all completions (words, snippets, LSP)
290    Normal,
291    /// Show only snippets (not words or LSP)
292    ///
293    /// Used after typing a non-word character
294    SnippetsOnly,
295    /// Tab stops within a snippet that have a predefined finite set of choices
296    SnippetChoices,
297    /// Show only words (not snippets or LSP)
298    ///
299    /// Used when word completions are explicitly triggered
300    Words { ignore_threshold: bool },
301}
302
303// TODO: There should really be a wrapper around fuzzy match tasks that does this.
304impl Drop for CompletionsMenu {
305    fn drop(&mut self) {
306        self.cancel_filter.store(true, Ordering::Relaxed);
307    }
308}
309
310#[derive(Default)]
311struct CompletionMenuScrollBarSetting;
312
313impl ui::scrollbars::ScrollbarVisibility for CompletionMenuScrollBarSetting {
314    fn visibility(&self, cx: &App) -> ui::scrollbars::ShowScrollbar {
315        EditorSettings::get_global(cx).completion_menu_scrollbar
316    }
317}
318
319impl CompletionsMenu {
320    pub fn new(
321        id: CompletionId,
322        source: CompletionsMenuSource,
323        sort_completions: bool,
324        show_completion_documentation: bool,
325        initial_position: Anchor,
326        initial_query: Option<Arc<String>>,
327        is_incomplete: bool,
328        buffer: Entity<Buffer>,
329        completions: Box<[Completion]>,
330        scroll_handle: Option<UniformListScrollHandle>,
331        display_options: CompletionDisplayOptions,
332        snippet_sort_order: SnippetSortOrder,
333        language_registry: Option<Arc<LanguageRegistry>>,
334        language: Option<LanguageName>,
335        cx: &mut Context<Editor>,
336    ) -> Self {
337        let match_candidates = completions
338            .iter()
339            .enumerate()
340            .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
341            .into_group_map_by(|candidate| completions[candidate.id].match_start)
342            .into_iter()
343            .collect();
344
345        let completions_menu = Self {
346            id,
347            source,
348            sort_completions,
349            initial_position,
350            initial_query,
351            is_incomplete,
352            buffer,
353            show_completion_documentation,
354            completions: RefCell::new(completions).into(),
355            match_candidates,
356            entries: Rc::new(RefCell::new(Box::new([]))),
357            selected_item: 0,
358            filter_task: Task::ready(()),
359            cancel_filter: Arc::new(AtomicBool::new(false)),
360            scroll_handle: scroll_handle.unwrap_or_else(UniformListScrollHandle::new),
361            scroll_handle_aside: ScrollHandle::new(),
362            resolve_completions: true,
363            last_rendered_range: RefCell::new(None).into(),
364            markdown_cache: RefCell::new(VecDeque::new()).into(),
365            language_registry,
366            language,
367            display_options,
368            snippet_sort_order,
369        };
370
371        completions_menu.start_markdown_parse_for_nearby_entries(cx);
372
373        completions_menu
374    }
375
376    pub fn new_snippet_choices(
377        id: CompletionId,
378        sort_completions: bool,
379        choices: &Vec<String>,
380        initial_position: Anchor,
381        selection: Range<text::Anchor>,
382        buffer: Entity<Buffer>,
383        scroll_handle: Option<UniformListScrollHandle>,
384        snippet_sort_order: SnippetSortOrder,
385    ) -> Self {
386        let completions = choices
387            .iter()
388            .map(|choice| Completion {
389                replace_range: selection.clone(),
390                new_text: choice.to_string(),
391                label: CodeLabel::plain(choice.to_string(), None),
392                match_start: None,
393                snippet_deduplication_key: None,
394                icon_path: None,
395                icon_color: None,
396                documentation: None,
397                confirm: None,
398                insert_text_mode: None,
399                source: CompletionSource::Custom,
400                group: None,
401            })
402            .collect();
403
404        let match_candidates = Arc::new([(
405            None,
406            choices
407                .iter()
408                .enumerate()
409                .map(|(id, completion)| StringMatchCandidate::new(id, completion))
410                .collect(),
411        )]);
412        let entries = choices
413            .iter()
414            .enumerate()
415            .map(|(id, completion)| {
416                CompletionMenuEntry::Match(StringMatch {
417                    candidate_id: id,
418                    score: 1.,
419                    positions: vec![],
420                    string: completion.clone(),
421                })
422            })
423            .collect();
424        Self {
425            id,
426            source: CompletionsMenuSource::SnippetChoices,
427            sort_completions,
428            initial_position,
429            initial_query: None,
430            is_incomplete: false,
431            buffer,
432            completions: RefCell::new(completions).into(),
433            match_candidates,
434            entries: RefCell::new(entries).into(),
435            selected_item: 0,
436            filter_task: Task::ready(()),
437            cancel_filter: Arc::new(AtomicBool::new(false)),
438            scroll_handle: scroll_handle.unwrap_or_else(UniformListScrollHandle::new),
439            scroll_handle_aside: ScrollHandle::new(),
440            resolve_completions: false,
441            show_completion_documentation: false,
442            last_rendered_range: RefCell::new(None).into(),
443            markdown_cache: RefCell::new(VecDeque::new()).into(),
444            language_registry: None,
445            language: None,
446            display_options: CompletionDisplayOptions::default(),
447            snippet_sort_order,
448        }
449    }
450
451    fn select_first(
452        &mut self,
453        provider: Option<&dyn CompletionProvider>,
454        window: &mut Window,
455        cx: &mut Context<Editor>,
456    ) {
457        let entries = self.entries.borrow();
458        if entries.is_empty() {
459            return;
460        }
461        let start = if self.scroll_handle.y_flipped() {
462            entries.len() - 1
463        } else {
464            0
465        };
466        drop(entries);
467        let index = self.find_selectable_entry(start, !self.scroll_handle.y_flipped());
468        if let Some(index) = index {
469            self.update_selection_index(index, provider, window, cx);
470        }
471    }
472
473    fn select_last(
474        &mut self,
475        provider: Option<&dyn CompletionProvider>,
476        window: &mut Window,
477        cx: &mut Context<Editor>,
478    ) {
479        let entries = self.entries.borrow();
480        if entries.is_empty() {
481            return;
482        }
483        let start = if self.scroll_handle.y_flipped() {
484            0
485        } else {
486            entries.len() - 1
487        };
488        drop(entries);
489        let index = self.find_selectable_entry(start, self.scroll_handle.y_flipped());
490        if let Some(index) = index {
491            self.update_selection_index(index, provider, window, cx);
492        }
493    }
494
495    fn select_prev(
496        &mut self,
497        provider: Option<&dyn CompletionProvider>,
498        window: &mut Window,
499        cx: &mut Context<Editor>,
500    ) {
501        let index = if self.scroll_handle.y_flipped() {
502            self.next_match_index()
503        } else {
504            self.prev_match_index()
505        };
506        self.update_selection_index(index, provider, window, cx);
507    }
508
509    fn select_next(
510        &mut self,
511        provider: Option<&dyn CompletionProvider>,
512        window: &mut Window,
513        cx: &mut Context<Editor>,
514    ) {
515        let index = if self.scroll_handle.y_flipped() {
516            self.prev_match_index()
517        } else {
518            self.next_match_index()
519        };
520        self.update_selection_index(index, provider, window, cx);
521    }
522
523    fn update_selection_index(
524        &mut self,
525        match_index: usize,
526        provider: Option<&dyn CompletionProvider>,
527        window: &mut Window,
528        cx: &mut Context<Editor>,
529    ) {
530        if self.selected_item != match_index {
531            self.selected_item = match_index;
532            self.handle_selection_changed(provider, window, cx);
533        }
534    }
535
536    fn prev_match_index(&self) -> usize {
537        let entries = self.entries.borrow();
538        let len = entries.len();
539        if len == 0 {
540            return 0;
541        }
542        let mut index = if self.selected_item > 0 {
543            self.selected_item - 1
544        } else {
545            len - 1
546        };
547        let start = index;
548        loop {
549            if entries[index].is_selectable() {
550                return index;
551            }
552            index = if index > 0 { index - 1 } else { len - 1 };
553            if index == start {
554                return self.selected_item;
555            }
556        }
557    }
558
559    fn next_match_index(&self) -> usize {
560        let entries = self.entries.borrow();
561        let len = entries.len();
562        if len == 0 {
563            return 0;
564        }
565        let mut index = if self.selected_item + 1 < len {
566            self.selected_item + 1
567        } else {
568            0
569        };
570        let start = index;
571        loop {
572            if entries[index].is_selectable() {
573                return index;
574            }
575            index = if index + 1 < len { index + 1 } else { 0 };
576            if index == start {
577                return self.selected_item;
578            }
579        }
580    }
581
582    fn find_selectable_entry(&self, start: usize, forward: bool) -> Option<usize> {
583        let entries = self.entries.borrow();
584        let len = entries.len();
585        if len == 0 {
586            return None;
587        }
588        let mut index = start;
589        loop {
590            if entries[index].is_selectable() {
591                return Some(index);
592            }
593            if forward {
594                index = if index + 1 < len { index + 1 } else { 0 };
595            } else {
596                index = if index > 0 { index - 1 } else { len - 1 };
597            }
598            if index == start {
599                return None;
600            }
601        }
602    }
603
604    fn handle_selection_changed(
605        &mut self,
606        provider: Option<&dyn CompletionProvider>,
607        window: &mut Window,
608        cx: &mut Context<Editor>,
609    ) {
610        self.scroll_handle
611            .scroll_to_item(self.selected_item, ScrollStrategy::Nearest);
612        if let Some(provider) = provider {
613            let entries = self.entries.borrow();
614            let entry = if self.selected_item < entries.len() {
615                entries[self.selected_item].as_match()
616            } else {
617                None
618            };
619            provider.selection_changed(entry, window, cx);
620        }
621        self.resolve_visible_completions(provider, cx);
622        self.start_markdown_parse_for_nearby_entries(cx);
623        cx.notify();
624    }
625
626    pub fn resolve_visible_completions(
627        &mut self,
628        provider: Option<&dyn CompletionProvider>,
629        cx: &mut Context<Editor>,
630    ) {
631        if !self.resolve_completions {
632            return;
633        }
634        let Some(provider) = provider else {
635            return;
636        };
637
638        let entries = self.entries.borrow();
639        if entries.is_empty() {
640            return;
641        }
642        if self.selected_item >= entries.len() {
643            log::error!(
644                "bug: completion selected_item >= entries.len(): {} >= {}",
645                self.selected_item,
646                entries.len()
647            );
648            self.selected_item = entries.len() - 1;
649        }
650
651        // Attempt to resolve completions for every item that will be displayed. This matters
652        // because single line documentation may be displayed inline with the completion.
653        //
654        // When navigating to the very beginning or end of completions, `last_rendered_range` may
655        // have no overlap with the completions that will be displayed, so instead use a range based
656        // on the last rendered count.
657        const APPROXIMATE_VISIBLE_COUNT: usize = 12;
658        let last_rendered_range = self.last_rendered_range.borrow().clone();
659        let visible_count = last_rendered_range
660            .clone()
661            .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
662        let entry_range = if self.selected_item == 0 {
663            0..min(visible_count, entries.len())
664        } else if self.selected_item == entries.len() - 1 {
665            entries.len().saturating_sub(visible_count)..entries.len()
666        } else {
667            last_rendered_range.map_or(0..0, |range| {
668                min(range.start, entries.len())..min(range.end, entries.len())
669            })
670        };
671
672        // Expand the range to resolve more completions than are predicted to be visible, to reduce
673        // jank on navigation.
674        let entry_indices = util::expanded_and_wrapped_usize_range(
675            entry_range,
676            RESOLVE_BEFORE_ITEMS,
677            RESOLVE_AFTER_ITEMS,
678            entries.len(),
679        );
680
681        // Avoid work by sometimes filtering out completions that already have documentation.
682        // This filtering doesn't happen if the completions are currently being updated.
683        let completions = self.completions.borrow();
684        let candidate_ids = entry_indices
685            .filter_map(|i| entries[i].as_match().map(|m| m.candidate_id))
686            .filter(|i| completions[*i].documentation.is_none());
687
688        // Current selection is always resolved even if it already has documentation, to handle
689        // out-of-spec language servers that return more results later.
690        let Some(selected_candidate_id) = entries[self.selected_item]
691            .as_match()
692            .map(|m| m.candidate_id)
693        else {
694            drop(entries);
695            drop(completions);
696            return;
697        };
698        let candidate_ids = iter::once(selected_candidate_id)
699            .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
700            .collect::<Vec<usize>>();
701        drop(entries);
702
703        if candidate_ids.is_empty() {
704            return;
705        }
706
707        let resolve_task = provider.resolve_completions(
708            self.buffer.clone(),
709            candidate_ids,
710            self.completions.clone(),
711            cx,
712        );
713
714        let completion_id = self.id;
715        cx.spawn(async move |editor, cx| {
716            if let Some(true) = resolve_task.await.log_err() {
717                editor
718                    .update(cx, |editor, cx| {
719                        // `resolve_completions` modified state affecting display.
720                        cx.notify();
721                        editor.with_completions_menu_matching_id(completion_id, |menu| {
722                            if let Some(menu) = menu {
723                                menu.start_markdown_parse_for_nearby_entries(cx)
724                            }
725                        });
726                    })
727                    .ok();
728            }
729        })
730        .detach();
731    }
732
733    fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
734        // Enqueue parse tasks of nearer items first.
735        //
736        // TODO: This means that the nearer items will actually be further back in the cache, which
737        // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
738        // selection to the front (when `is_render = true`).
739        let entry_indices = util::wrapped_usize_outward_from(
740            self.selected_item,
741            MARKDOWN_CACHE_BEFORE_ITEMS,
742            MARKDOWN_CACHE_AFTER_ITEMS,
743            self.entries.borrow().len(),
744        );
745
746        for index in entry_indices {
747            self.get_or_create_entry_markdown(index, cx);
748        }
749    }
750
751    fn get_or_create_entry_markdown(
752        &self,
753        index: usize,
754        cx: &mut Context<Editor>,
755    ) -> Option<Entity<Markdown>> {
756        let entries = self.entries.borrow();
757        if index >= entries.len() {
758            return None;
759        }
760        let candidate_id = entries[index].as_match()?.candidate_id;
761        let completions = self.completions.borrow();
762        match &completions[candidate_id].documentation {
763            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
764                .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
765                .map(|(_, markdown)| markdown),
766            Some(_) => None,
767            _ => None,
768        }
769    }
770
771    fn get_or_create_markdown(
772        &self,
773        candidate_id: usize,
774        source: Option<&SharedString>,
775        is_render: bool,
776        completions: &[Completion],
777        cx: &mut Context<Editor>,
778    ) -> Option<(bool, Entity<Markdown>)> {
779        let mut markdown_cache = self.markdown_cache.borrow_mut();
780
781        let mut has_completion_match_cache_entry = false;
782        let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
783            MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
784            MarkdownCacheKey::ForCompletionMatch { .. } => {
785                has_completion_match_cache_entry = true;
786                false
787            }
788        });
789
790        if has_completion_match_cache_entry && matching_entry.is_none() {
791            if let Some(source) = source {
792                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
793                    matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
794                                if markdown_source == source)
795                });
796            } else {
797                // Heuristic guess that documentation can be reused when new_text matches. This is
798                // to mitigate documentation flicker while typing. If this is wrong, then resolution
799                // should cause the correct documentation to be displayed soon.
800                let completion = &completions[candidate_id];
801                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
802                    matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
803                                if new_text == &completion.new_text)
804                });
805            }
806        }
807
808        if let Some((cache_index, (key, markdown))) = matching_entry {
809            let markdown = markdown.clone();
810
811            // Since the markdown source matches, the key can now be ForCandidate.
812            if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
813                markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
814            }
815
816            if is_render && cache_index != 0 {
817                // Move the current selection's cache entry to the front.
818                markdown_cache.rotate_right(1);
819                let cache_len = markdown_cache.len();
820                markdown_cache.swap(0, (cache_index + 1) % cache_len);
821            }
822
823            let is_parsing = markdown.update(cx, |markdown, cx| {
824                if let Some(source) = source {
825                    // `reset` is called as it's possible for documentation to change due to resolve
826                    // requests. It does nothing if `source` is unchanged.
827                    markdown.reset(source.clone(), cx);
828                }
829                markdown.is_parsing()
830            });
831            return Some((is_parsing, markdown));
832        }
833
834        let Some(source) = source else {
835            // Can't create markdown as there is no source.
836            return None;
837        };
838
839        if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
840            let markdown = cx.new(|cx| {
841                Markdown::new(
842                    source.clone(),
843                    self.language_registry.clone(),
844                    self.language.clone(),
845                    cx,
846                )
847            });
848            // Handles redraw when the markdown is done parsing. The current render is for a
849            // deferred draw, and so without this did not redraw when `markdown` notified.
850            cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
851            markdown_cache.push_front((
852                MarkdownCacheKey::ForCandidate { candidate_id },
853                markdown.clone(),
854            ));
855            Some((true, markdown))
856        } else {
857            debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
858            // Moves the last cache entry to the start. The ring buffer is full, so this does no
859            // copying and just shifts indexes.
860            markdown_cache.rotate_right(1);
861            markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
862            let markdown = &markdown_cache[0].1;
863            markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
864            Some((true, markdown.clone()))
865        }
866    }
867
868    pub fn visible(&self) -> bool {
869        self.entries.borrow().iter().any(|e| e.as_match().is_some())
870    }
871
872    fn origin(&self) -> ContextMenuOrigin {
873        ContextMenuOrigin::Cursor
874    }
875
876    fn render(
877        &self,
878        style: &EditorStyle,
879        max_height_in_lines: u32,
880        window: &mut Window,
881        cx: &mut Context<Editor>,
882    ) -> AnyElement {
883        let show_completion_documentation = self.show_completion_documentation;
884        let editor_settings = EditorSettings::get_global(cx);
885        let completion_detail_alignment = editor_settings.completion_detail_alignment;
886        let completion_menu_item_kind = editor_settings.completion_menu_item_kind;
887        let widest_completion_ix = if self.display_options.dynamic_width {
888            let completions = self.completions.borrow();
889            let widest_completion_ix = self
890                .entries
891                .borrow()
892                .iter()
893                .enumerate()
894                .filter_map(|(ix, entry)| entry.as_match().map(|m| (ix, m)))
895                .max_by_key(|(_, mat)| {
896                    let completion = &completions[mat.candidate_id];
897                    let documentation = &completion.documentation;
898
899                    let mut len = completion.label.text.chars().count();
900                    if show_completion_documentation {
901                        match documentation {
902                            Some(CompletionDocumentation::SingleLine(text)) => {
903                                len += text.chars().count();
904                            }
905                            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
906                                single_line,
907                                ..
908                            }) => {
909                                len += single_line.chars().count();
910                            }
911                            _ => {}
912                        }
913                    }
914
915                    len
916                })
917                .map(|(ix, _)| ix);
918            drop(completions);
919            widest_completion_ix
920        } else {
921            None
922        };
923
924        let selected_item = self.selected_item;
925        let completions = self.completions.clone();
926        let entries = self.entries.clone();
927        let last_rendered_range = self.last_rendered_range.clone();
928        let style = style.clone();
929        let list = uniform_list(
930            "completions",
931            self.entries.borrow().len(),
932            cx.processor(move |_editor, range: Range<usize>, _window, cx| {
933                last_rendered_range.borrow_mut().replace(range.clone());
934                let start_ix = range.start;
935                let completions_guard = completions.borrow_mut();
936
937                entries.borrow()[range]
938                    .iter()
939                    .enumerate()
940                    .map(|(ix, entry)| {
941                        let item_ix = start_ix + ix;
942
943                        let Some(mat) = entry.as_match() else {
944                            return match entry {
945                                CompletionMenuEntry::GroupHeader(label) => div()
946                                    .child(ListSubHeader::new(label.clone()).inset(true))
947                                    .into_any_element(),
948                                CompletionMenuEntry::Divider => h_flex()
949                                    .flex_1()
950                                    .size_full()
951                                    .child(Divider::horizontal())
952                                    .into_any_element(),
953                                CompletionMenuEntry::Match(_) => unreachable!(),
954                            };
955                        };
956
957                        let completion = &completions_guard[mat.candidate_id];
958                        let documentation = if show_completion_documentation {
959                            &completion.documentation
960                        } else {
961                            &None
962                        };
963
964                        let filter_start = completion.label.filter_range.start;
965
966                        let highlights = gpui::combine_highlights(
967                            mat.ranges().map(|range| {
968                                (
969                                    filter_start + range.start..filter_start + range.end,
970                                    FontWeight::BOLD.into(),
971                                )
972                            }),
973                            styled_runs_for_code_label(
974                                &completion.label,
975                                &style.syntax,
976                                &style.local_player,
977                            )
978                            .map(|(range, mut highlight)| {
979                                // Ignore font weight for syntax highlighting, as we'll use it
980                                // for fuzzy matches.
981                                highlight.font_weight = None;
982                                if completion
983                                    .source
984                                    .lsp_completion(false)
985                                    .and_then(|lsp_completion| {
986                                        match (lsp_completion.deprecated, &lsp_completion.tags) {
987                                            (Some(true), _) => Some(true),
988                                            (_, Some(tags)) => {
989                                                Some(tags.contains(&CompletionItemTag::DEPRECATED))
990                                            }
991                                            _ => None,
992                                        }
993                                    })
994                                    .unwrap_or(false)
995                                {
996                                    highlight.strikethrough = Some(StrikethroughStyle {
997                                        thickness: 1.0.into(),
998                                        ..Default::default()
999                                    });
1000                                    highlight.color = Some(cx.theme().colors().text_muted);
1001                                }
1002
1003                                (range, highlight)
1004                            }),
1005                        );
1006
1007                        let highlights: Vec<_> = highlights.collect();
1008
1009                        let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
1010                            split_completion_label(
1011                                &completion.label.text,
1012                                &completion.label.filter_range,
1013                                &highlights,
1014                            );
1015                        let main_label = StyledText::new(main_text.to_string())
1016                            .with_default_highlights(&style.text, main_highlights);
1017
1018                        let suffix_label = if !suffix_text.is_empty() {
1019                            Some(
1020                                StyledText::new(suffix_text.to_string())
1021                                    .with_default_highlights(&style.text, suffix_highlights),
1022                            )
1023                        } else {
1024                            None
1025                        };
1026
1027                        let left_aligned_suffix =
1028                            matches!(completion_detail_alignment, CompletionDetailAlignment::Left);
1029
1030                        let right_aligned_suffix = matches!(
1031                            completion_detail_alignment,
1032                            CompletionDetailAlignment::Right,
1033                        );
1034
1035                        let documentation_label = match documentation {
1036                            Some(CompletionDocumentation::SingleLine(text))
1037                            | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1038                                single_line: text,
1039                                ..
1040                            }) => {
1041                                if text.trim().is_empty() {
1042                                    None
1043                                } else {
1044                                    Some(
1045                                        Label::new(text.trim().to_string())
1046                                            .ml_4()
1047                                            .size(LabelSize::Small)
1048                                            .color(Color::Muted),
1049                                    )
1050                                }
1051                            }
1052                            _ => None,
1053                        };
1054
1055                        let icon_or_color_slot = completion
1056                            .color()
1057                            .map(|color| {
1058                                div()
1059                                    .flex_shrink_0()
1060                                    .size_3p5()
1061                                    .rounded_xs()
1062                                    .bg(color)
1063                                    .into_any_element()
1064                            })
1065                            .or_else(|| {
1066                                completion.icon_path.as_ref().map(|path| {
1067                                    Icon::from_path(path)
1068                                        .size(IconSize::XSmall)
1069                                        .color(
1070                                            completion
1071                                                .icon_color
1072                                                .map_or(Color::Muted, Color::Custom),
1073                                        )
1074                                        .into_any_element()
1075                                })
1076                            });
1077
1078                        let kind_letter_slot = match completion_menu_item_kind {
1079                            CompletionMenuItemKind::Off => None,
1080                            CompletionMenuItemKind::Symbol => Some(render_completion_kind_letter(
1081                                completion.kind(),
1082                                item_ix,
1083                                &style,
1084                            )),
1085                        };
1086
1087                        let start_slot = match (kind_letter_slot, icon_or_color_slot) {
1088                            (Some(letter), Some(icon_or_color)) => Some(
1089                                h_flex()
1090                                    .gap_0p5()
1091                                    .child(letter)
1092                                    .child(icon_or_color)
1093                                    .into_any_element(),
1094                            ),
1095                            (Some(letter), None) => Some(letter),
1096                            (None, slot) => slot,
1097                        };
1098
1099                        div()
1100                            .min_w(COMPLETION_MENU_MIN_WIDTH)
1101                            .max_w(COMPLETION_MENU_MAX_WIDTH)
1102                            .child(
1103                                ListItem::new(mat.candidate_id)
1104                                    .inset(true)
1105                                    .toggle_state(item_ix == selected_item)
1106                                    .on_click(cx.listener(move |editor, _event, window, cx| {
1107                                        cx.stop_propagation();
1108                                        if let Some(task) = editor.confirm_completion(
1109                                            &ConfirmCompletion {
1110                                                item_ix: Some(item_ix),
1111                                            },
1112                                            window,
1113                                            cx,
1114                                        ) {
1115                                            task.detach_and_log_err(cx)
1116                                        }
1117                                    }))
1118                                    .start_slot::<AnyElement>(start_slot)
1119                                    .child(
1120                                        h_flex()
1121                                            .min_w_0()
1122                                            .w_full()
1123                                            .when(left_aligned_suffix, |this| this.justify_start())
1124                                            .when(right_aligned_suffix, |this| {
1125                                                this.justify_between()
1126                                            })
1127                                            .child(
1128                                                div()
1129                                                    .flex_none()
1130                                                    .whitespace_nowrap()
1131                                                    .child(main_label),
1132                                            )
1133                                            .when_some(suffix_label, |this, suffix| {
1134                                                this.child(div().truncate().child(suffix))
1135                                            }),
1136                                    )
1137                                    .end_slot::<Label>(documentation_label),
1138                            )
1139                            .into_any_element()
1140                    })
1141                    .collect()
1142            }),
1143        )
1144        .occlude()
1145        .max_h(max_height_in_lines as f32 * window.line_height())
1146        .track_scroll(&self.scroll_handle)
1147        .with_sizing_behavior(ListSizingBehavior::Infer)
1148        .map(|this| {
1149            if self.display_options.dynamic_width {
1150                this.with_width_from_item(widest_completion_ix)
1151            } else {
1152                this.w(rems(34.))
1153            }
1154        });
1155
1156        Popover::new()
1157            .child(
1158                div().child(list).custom_scrollbars(
1159                    Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
1160                        .show_along(ScrollAxes::Vertical)
1161                        .tracked_scroll_handle(&self.scroll_handle),
1162                    window,
1163                    cx,
1164                ),
1165            )
1166            .into_any_element()
1167    }
1168
1169    fn render_aside(
1170        &mut self,
1171        max_size: Size<Pixels>,
1172        window: &mut Window,
1173        cx: &mut Context<Editor>,
1174    ) -> Option<AnyElement> {
1175        if !self.show_completion_documentation {
1176            return None;
1177        }
1178
1179        let entries = self.entries.borrow();
1180        let Some(mat) = entries[self.selected_item].as_match() else {
1181            return None;
1182        };
1183        let completions = self.completions.borrow();
1184        let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
1185            Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
1186            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1187                plain_text: Some(text),
1188                ..
1189            }) => div().child(text.clone()),
1190            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
1191                let Some((false, markdown)) = self.get_or_create_markdown(
1192                    mat.candidate_id,
1193                    Some(source),
1194                    true,
1195                    &completions,
1196                    cx,
1197                ) else {
1198                    return None;
1199                };
1200                Self::render_markdown(markdown, window, cx)
1201            }
1202            None => {
1203                // Handle the case where documentation hasn't yet been resolved but there's a
1204                // `new_text` match in the cache.
1205                //
1206                // TODO: It's inconsistent that documentation caching based on matching `new_text`
1207                // only works for markdown. Consider generally caching the results of resolving
1208                // completions.
1209                let Some((false, markdown)) =
1210                    self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
1211                else {
1212                    return None;
1213                };
1214                Self::render_markdown(markdown, window, cx)
1215            }
1216            Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
1217            Some(CompletionDocumentation::SingleLine(_)) => return None,
1218            Some(CompletionDocumentation::Undocumented) => return None,
1219            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1220                plain_text: None,
1221                ..
1222            }) => {
1223                return None;
1224            }
1225        };
1226
1227        Some(
1228            Popover::new()
1229                .child(
1230                    multiline_docs
1231                        .id("multiline_docs")
1232                        .px(MENU_ASIDE_X_PADDING / 2.)
1233                        .max_w(max_size.width)
1234                        .max_h(max_size.height)
1235                        .overflow_y_scroll()
1236                        .track_scroll(&self.scroll_handle_aside)
1237                        .occlude(),
1238                )
1239                .into_any_element(),
1240        )
1241    }
1242
1243    fn render_markdown(
1244        markdown: Entity<Markdown>,
1245        window: &mut Window,
1246        cx: &mut Context<Editor>,
1247    ) -> Div {
1248        let editor = cx.weak_entity();
1249        div().child(
1250            MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1251                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1252                    copy_button_visibility: CopyButtonVisibility::Hidden,
1253                    wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
1254                    border: false,
1255                })
1256                .on_url_click(move |link, window, cx| {
1257                    open_markdown_url(
1258                        editor
1259                            .read_with(cx, |editor, _| editor.workspace())
1260                            .ok()
1261                            .flatten(),
1262                        link,
1263                        window,
1264                        cx,
1265                    )
1266                }),
1267        )
1268    }
1269
1270    pub fn filter(
1271        &mut self,
1272        query: Arc<String>,
1273        query_end: text::Anchor,
1274        buffer: &Entity<Buffer>,
1275        provider: Option<Rc<dyn CompletionProvider>>,
1276        window: &mut Window,
1277        cx: &mut Context<Editor>,
1278    ) {
1279        self.cancel_filter.store(true, Ordering::Relaxed);
1280        self.cancel_filter = Arc::new(AtomicBool::new(false));
1281        let matches = self.do_async_filtering(query, query_end, buffer, cx);
1282        let id = self.id;
1283        self.filter_task = cx.spawn_in(window, async move |editor, cx| {
1284            let matches = matches.await;
1285            editor
1286                .update_in(cx, |editor, window, cx| {
1287                    editor.with_completions_menu_matching_id(id, |this| {
1288                        if let Some(this) = this {
1289                            this.set_filter_results(matches, provider, window, cx);
1290                        }
1291                    });
1292                })
1293                .ok();
1294        });
1295    }
1296
1297    pub fn do_async_filtering(
1298        &self,
1299        query: Arc<String>,
1300        query_end: text::Anchor,
1301        buffer: &Entity<Buffer>,
1302        cx: &Context<Editor>,
1303    ) -> Task<Vec<StringMatch>> {
1304        let buffer_snapshot = buffer.read(cx).snapshot();
1305        let background_executor = cx.background_executor().clone();
1306        let match_candidates = self.match_candidates.clone();
1307        let cancel_filter = self.cancel_filter.clone();
1308        let default_query = query.clone();
1309
1310        let matches_task = cx.background_spawn(async move {
1311            let queries_and_candidates = match_candidates
1312                .iter()
1313                .map(|(query_start, candidates)| {
1314                    let query_for_batch = match query_start {
1315                        Some(start) => {
1316                            Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
1317                        }
1318                        None => default_query.clone(),
1319                    };
1320                    (query_for_batch, candidates)
1321                })
1322                .collect_vec();
1323
1324            let mut results = vec![];
1325            for (query, match_candidates) in queries_and_candidates {
1326                results.extend(
1327                    fuzzy::match_strings(
1328                        &match_candidates,
1329                        &query,
1330                        query.chars().any(|c| c.is_uppercase()),
1331                        false,
1332                        1000,
1333                        &cancel_filter,
1334                        background_executor.clone(),
1335                    )
1336                    .await,
1337                );
1338            }
1339            results
1340        });
1341
1342        let completions = self.completions.clone();
1343        let sort_completions = self.sort_completions;
1344        let snippet_sort_order = self.snippet_sort_order;
1345        cx.foreground_executor().spawn(async move {
1346            let mut matches = matches_task.await;
1347
1348            let completions_ref = completions.borrow();
1349
1350            if sort_completions {
1351                matches = Self::sort_string_matches(
1352                    matches,
1353                    Some(&query), // used for non-snippets only
1354                    snippet_sort_order,
1355                    &completions_ref,
1356                );
1357            }
1358
1359            // Remove duplicate snippet prefixes (e.g., "cool code" will match
1360            // the text "c c" in two places; we should only show the longer one)
1361            let mut snippets_seen = HashSet::<(usize, usize)>::default();
1362            matches.retain(|result| {
1363                match completions_ref[result.candidate_id].snippet_deduplication_key {
1364                    Some(key) => snippets_seen.insert(key),
1365                    None => true,
1366                }
1367            });
1368
1369            matches
1370        })
1371    }
1372
1373    pub fn set_filter_results(
1374        &mut self,
1375        matches: Vec<StringMatch>,
1376        provider: Option<Rc<dyn CompletionProvider>>,
1377        window: &mut Window,
1378        cx: &mut Context<Editor>,
1379    ) {
1380        let completions = self.completions.borrow();
1381        let mut entries: Vec<CompletionMenuEntry> = Vec::with_capacity(matches.len());
1382        let mut last_group: Option<&CompletionGroup> = None;
1383        for mat in matches {
1384            let group = completions[mat.candidate_id].group.as_ref();
1385            if group != last_group {
1386                if group.is_some() || last_group.is_some() {
1387                    if !entries.is_empty() {
1388                        entries.push(CompletionMenuEntry::Divider);
1389                    }
1390                    if let Some(label) = group.and_then(|g| g.label.as_ref()) {
1391                        entries.push(CompletionMenuEntry::GroupHeader(label.clone()));
1392                    }
1393                }
1394                last_group = group;
1395            }
1396            entries.push(CompletionMenuEntry::Match(mat));
1397        }
1398        drop(completions);
1399        *self.entries.borrow_mut() = entries.into_boxed_slice();
1400        self.selected_item = self.find_selectable_entry(0, true).unwrap_or(0);
1401        self.handle_selection_changed(provider.as_deref(), window, cx);
1402    }
1403
1404    pub fn sort_string_matches(
1405        matches: Vec<StringMatch>,
1406        query: Option<&str>,
1407        snippet_sort_order: SnippetSortOrder,
1408        completions: &[Completion],
1409    ) -> Vec<StringMatch> {
1410        let mut matches = matches;
1411
1412        #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1413        enum MatchTier<'a> {
1414            WordStartMatch {
1415                sort_exact: Reverse<i32>,
1416                sort_snippet: Reverse<i32>,
1417                sort_score: Reverse<OrderedFloat<f64>>,
1418                sort_positions: Vec<usize>,
1419                sort_exact_case_matches: Reverse<usize>,
1420                sort_text: Option<&'a str>,
1421                sort_kind: usize,
1422                sort_label: &'a str,
1423            },
1424            OtherMatch {
1425                sort_score: Reverse<OrderedFloat<f64>>,
1426            },
1427        }
1428
1429        let query_start_lower = query
1430            .as_ref()
1431            .and_then(|q| q.chars().next())
1432            .and_then(|c| c.to_lowercase().next());
1433
1434        if snippet_sort_order == SnippetSortOrder::None {
1435            matches
1436                .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
1437        }
1438
1439        matches.sort_unstable_by_key(|string_match| {
1440            let completion = &completions[string_match.candidate_id];
1441
1442            let sort_text = match &completion.source {
1443                CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1444                CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1445                _ => None,
1446            };
1447
1448            let (sort_kind, sort_label) = completion.sort_key();
1449
1450            let score = string_match.score;
1451            let sort_score = Reverse(OrderedFloat(score));
1452
1453            // Snippets do their own first-letter matching logic elsewhere.
1454            let is_snippet = completion.is_snippet_kind();
1455            let query_start_doesnt_match_split_words = !is_snippet
1456                && query_start_lower
1457                    .map(|query_char| {
1458                        !split_words(&string_match.string).any(|word| {
1459                            word.chars().next().and_then(|c| c.to_lowercase().next())
1460                                == Some(query_char)
1461                        })
1462                    })
1463                    .unwrap_or(false);
1464
1465            if query_start_doesnt_match_split_words {
1466                MatchTier::OtherMatch { sort_score }
1467            } else {
1468                let sort_snippet = match snippet_sort_order {
1469                    SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1470                    SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1471                    SnippetSortOrder::Inline => Reverse(0),
1472                    SnippetSortOrder::None => Reverse(0),
1473                };
1474                let sort_positions = string_match.positions.clone();
1475                let sort_exact_case_matches = Reverse(exact_case_match_count(
1476                    query.unwrap_or_default(),
1477                    string_match,
1478                ));
1479                // This exact matching won't work for multi-word snippets, but it's fine
1480                let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1481                    1
1482                } else {
1483                    0
1484                });
1485
1486                MatchTier::WordStartMatch {
1487                    sort_exact,
1488                    sort_snippet,
1489                    sort_score,
1490                    sort_positions,
1491                    sort_exact_case_matches,
1492                    sort_text,
1493                    sort_kind,
1494                    sort_label,
1495                }
1496            }
1497        });
1498
1499        matches
1500    }
1501
1502    pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1503        self.markdown_cache = prev_menu.markdown_cache.clone();
1504
1505        // Convert ForCandidate cache keys to ForCompletionMatch keys.
1506        let prev_completions = prev_menu.completions.borrow();
1507        self.markdown_cache
1508            .borrow_mut()
1509            .retain_mut(|(key, _markdown)| match key {
1510                MarkdownCacheKey::ForCompletionMatch { .. } => true,
1511                MarkdownCacheKey::ForCandidate { candidate_id } => {
1512                    if let Some(completion) = prev_completions.get(*candidate_id) {
1513                        match &completion.documentation {
1514                            Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1515                                *key = MarkdownCacheKey::ForCompletionMatch {
1516                                    new_text: completion.new_text.clone(),
1517                                    markdown_source: source.clone(),
1518                                };
1519                                true
1520                            }
1521                            _ => false,
1522                        }
1523                    } else {
1524                        false
1525                    }
1526                }
1527            });
1528    }
1529
1530    pub fn scroll_aside(
1531        &mut self,
1532        amount: ScrollAmount,
1533        window: &mut Window,
1534        cx: &mut Context<Editor>,
1535    ) {
1536        let mut offset = self.scroll_handle_aside.offset();
1537
1538        offset.y -= amount.pixels(
1539            window.line_height(),
1540            self.scroll_handle_aside.bounds().size.height - px(16.),
1541        ) / 2.0;
1542
1543        cx.notify();
1544        self.scroll_handle_aside.set_offset(offset);
1545    }
1546}
1547
1548fn render_completion_kind_letter(
1549    kind: Option<CompletionItemKind>,
1550    item_ix: usize,
1551    style: &EditorStyle,
1552) -> AnyElement {
1553    let badge = div()
1554        .flex_none()
1555        .w(IconSize::XSmall.rems())
1556        .text_center()
1557        .text_size(rems_from_px(11.))
1558        .line_height(rems_from_px(14.));
1559
1560    let Some(kind) = kind else {
1561        return badge.into_any_element();
1562    };
1563    let Some(letter) = completion_kind_letter(kind) else {
1564        return badge.into_any_element();
1565    };
1566
1567    let color = completion_kind_highlight_name(kind)
1568        .and_then(|name| {
1569            style.syntax.style_for_name(name).or_else(|| {
1570                let (parent, _) = name.rsplit_once('.')?;
1571                style.syntax.style_for_name(parent)
1572            })
1573        })
1574        .and_then(|hl| hl.color);
1575
1576    badge
1577        .id(("completion-kind", item_ix))
1578        .tooltip(Tooltip::text(completion_kind_name(kind)))
1579        .child(letter)
1580        .when_some(color, |element, color| element.text_color(color))
1581        .into_any_element()
1582}
1583
1584fn completion_kind_name(kind: CompletionItemKind) -> &'static str {
1585    match kind {
1586        CompletionItemKind::TEXT => "Text",
1587        CompletionItemKind::METHOD => "Method",
1588        CompletionItemKind::FUNCTION => "Function",
1589        CompletionItemKind::CONSTRUCTOR => "Constructor",
1590        CompletionItemKind::FIELD => "Field",
1591        CompletionItemKind::VARIABLE => "Variable",
1592        CompletionItemKind::CLASS => "Class",
1593        CompletionItemKind::INTERFACE => "Interface",
1594        CompletionItemKind::MODULE => "Module",
1595        CompletionItemKind::PROPERTY => "Property",
1596        CompletionItemKind::UNIT => "Unit",
1597        CompletionItemKind::VALUE => "Value",
1598        CompletionItemKind::ENUM => "Enum",
1599        CompletionItemKind::KEYWORD => "Keyword",
1600        CompletionItemKind::SNIPPET => "Snippet",
1601        CompletionItemKind::COLOR => "Color",
1602        CompletionItemKind::FILE => "File",
1603        CompletionItemKind::REFERENCE => "Reference",
1604        CompletionItemKind::FOLDER => "Folder",
1605        CompletionItemKind::ENUM_MEMBER => "Enum Member",
1606        CompletionItemKind::CONSTANT => "Constant",
1607        CompletionItemKind::STRUCT => "Struct",
1608        CompletionItemKind::EVENT => "Event",
1609        CompletionItemKind::OPERATOR => "Operator",
1610        CompletionItemKind::TYPE_PARAMETER => "Type Parameter",
1611        _ => "Unknown",
1612    }
1613}
1614
1615fn completion_kind_letter(kind: CompletionItemKind) -> Option<&'static str> {
1616    Some(match kind {
1617        CompletionItemKind::TEXT => "t",
1618        CompletionItemKind::METHOD => "m",
1619        CompletionItemKind::FUNCTION => "f",
1620        CompletionItemKind::CONSTRUCTOR => "C",
1621        CompletionItemKind::FIELD => "f",
1622        CompletionItemKind::VARIABLE => "v",
1623        CompletionItemKind::CLASS => "c",
1624        CompletionItemKind::INTERFACE => "i",
1625        CompletionItemKind::MODULE => "M",
1626        CompletionItemKind::PROPERTY => "p",
1627        CompletionItemKind::UNIT => "u",
1628        CompletionItemKind::VALUE => "v",
1629        CompletionItemKind::ENUM => "e",
1630        CompletionItemKind::KEYWORD => "k",
1631        CompletionItemKind::SNIPPET => "s",
1632        CompletionItemKind::COLOR => "c",
1633        CompletionItemKind::FILE => "F",
1634        CompletionItemKind::REFERENCE => "r",
1635        CompletionItemKind::FOLDER => "D",
1636        CompletionItemKind::ENUM_MEMBER => "e",
1637        CompletionItemKind::CONSTANT => "c",
1638        CompletionItemKind::STRUCT => "S",
1639        CompletionItemKind::EVENT => "E",
1640        CompletionItemKind::OPERATOR => "o",
1641        CompletionItemKind::TYPE_PARAMETER => "T",
1642        _ => return None,
1643    })
1644}
1645
1646fn completion_kind_highlight_name(kind: CompletionItemKind) -> Option<&'static str> {
1647    Some(match kind {
1648        CompletionItemKind::CLASS => "type",
1649        CompletionItemKind::CONSTANT => "constant",
1650        CompletionItemKind::CONSTRUCTOR => "constructor",
1651        CompletionItemKind::ENUM => "enum",
1652        CompletionItemKind::ENUM_MEMBER => "variant",
1653        CompletionItemKind::FIELD => "property",
1654        CompletionItemKind::FUNCTION => "function",
1655        CompletionItemKind::INTERFACE => "type",
1656        CompletionItemKind::METHOD => "function.method",
1657        CompletionItemKind::MODULE => "namespace",
1658        CompletionItemKind::OPERATOR => "operator",
1659        CompletionItemKind::PROPERTY => "property",
1660        CompletionItemKind::STRUCT => "type",
1661        CompletionItemKind::TYPE_PARAMETER => "type",
1662        CompletionItemKind::VARIABLE => "variable",
1663        CompletionItemKind::KEYWORD => "keyword",
1664        CompletionItemKind::SNIPPET => "string",
1665        _ => return None,
1666    })
1667}
1668
1669fn split_completion_label<'a>(
1670    text: &'a str,
1671    filter_range: &Range<usize>,
1672    highlights: &[(Range<usize>, HighlightStyle)],
1673) -> (
1674    (&'a str, Vec<(Range<usize>, HighlightStyle)>),
1675    (&'a str, Vec<(Range<usize>, HighlightStyle)>),
1676) {
1677    let (main_text, suffix_text) = text.split_at(filter_range.end);
1678    let main_highlights = highlights
1679        .iter()
1680        .filter_map(|(range, highlight)| {
1681            if range.start >= filter_range.end {
1682                return None;
1683            }
1684            let clamped_end = range.end.min(filter_range.end);
1685            Some((range.start..clamped_end, *highlight))
1686        })
1687        .collect();
1688    let suffix_highlights = highlights
1689        .iter()
1690        .filter_map(|(range, highlight)| {
1691            if range.end <= filter_range.end {
1692                return None;
1693            }
1694            let shifted_start = range.start.saturating_sub(filter_range.end);
1695            let shifted_end = range.end - filter_range.end;
1696            Some((shifted_start..shifted_end, *highlight))
1697        })
1698        .collect();
1699    (
1700        (main_text, main_highlights),
1701        (suffix_text, suffix_highlights),
1702    )
1703}
1704
1705fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize {
1706    let mut exact_matches = 0;
1707    let mut query_chars = query.chars();
1708    let mut next_query_char = query_chars.next();
1709    let mut matched_positions = string_match.positions.iter().copied().peekable();
1710
1711    for (index, candidate_char) in string_match.string.char_indices() {
1712        if matched_positions.peek() == Some(&index) {
1713            let Some(query_char) = next_query_char else {
1714                break;
1715            };
1716
1717            if query_char == candidate_char {
1718                exact_matches += 1;
1719            }
1720
1721            matched_positions.next();
1722            next_query_char = query_chars.next();
1723        }
1724    }
1725
1726    exact_matches
1727}
1728
1729#[derive(Clone)]
1730pub struct AvailableCodeAction {
1731    pub action: CodeAction,
1732    pub provider: Rc<dyn CodeActionProvider>,
1733}
1734
1735#[derive(Clone)]
1736pub struct CodeActionContents {
1737    tasks: Option<Rc<ResolvedTasks>>,
1738    actions: Option<Rc<[AvailableCodeAction]>>,
1739    debug_scenarios: Vec<DebugScenario>,
1740    pub(crate) context: TaskContext,
1741}
1742
1743impl CodeActionContents {
1744    pub(crate) fn new(
1745        tasks: Option<ResolvedTasks>,
1746        actions: Option<Rc<[AvailableCodeAction]>>,
1747        debug_scenarios: Vec<DebugScenario>,
1748        context: TaskContext,
1749    ) -> Self {
1750        Self {
1751            tasks: tasks.map(Rc::new),
1752            actions,
1753            debug_scenarios,
1754            context,
1755        }
1756    }
1757
1758    pub fn tasks(&self) -> Option<&ResolvedTasks> {
1759        self.tasks.as_deref()
1760    }
1761
1762    fn len(&self) -> usize {
1763        let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1764        let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1765        tasks_len + code_actions_len + self.debug_scenarios.len()
1766    }
1767
1768    pub fn is_empty(&self) -> bool {
1769        self.len() == 0
1770    }
1771
1772    fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1773        self.tasks
1774            .iter()
1775            .flat_map(|tasks| {
1776                tasks
1777                    .templates
1778                    .iter()
1779                    .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1780            })
1781            .chain(self.actions.iter().flat_map(|actions| {
1782                actions.iter().map(|available| CodeActionsItem::CodeAction {
1783                    action: available.action.clone(),
1784                    provider: available.provider.clone(),
1785                })
1786            }))
1787            .chain(
1788                self.debug_scenarios
1789                    .iter()
1790                    .cloned()
1791                    .map(CodeActionsItem::DebugScenario),
1792            )
1793    }
1794
1795    pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1796        if let Some(tasks) = &self.tasks {
1797            if let Some((kind, task)) = tasks.templates.get(index) {
1798                return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1799            } else {
1800                index -= tasks.templates.len();
1801            }
1802        }
1803        if let Some(actions) = &self.actions {
1804            if let Some(available) = actions.get(index) {
1805                return Some(CodeActionsItem::CodeAction {
1806                    action: available.action.clone(),
1807                    provider: available.provider.clone(),
1808                });
1809            } else {
1810                index -= actions.len();
1811            }
1812        }
1813
1814        self.debug_scenarios
1815            .get(index)
1816            .cloned()
1817            .map(CodeActionsItem::DebugScenario)
1818    }
1819}
1820
1821#[derive(Clone)]
1822pub enum CodeActionsItem {
1823    Task(TaskSourceKind, ResolvedTask),
1824    CodeAction {
1825        action: CodeAction,
1826        provider: Rc<dyn CodeActionProvider>,
1827    },
1828    DebugScenario(DebugScenario),
1829}
1830
1831impl CodeActionsItem {
1832    pub fn label(&self) -> String {
1833        match self {
1834            Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1835            Self::Task(_, task) => task.resolved_label.clone(),
1836            Self::DebugScenario(scenario) => scenario.label.to_string(),
1837        }
1838    }
1839
1840    pub fn menu_label(&self) -> String {
1841        match self {
1842            Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1843            Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1844            Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1845        }
1846    }
1847}
1848
1849pub struct CodeActionsMenu {
1850    pub actions: CodeActionContents,
1851    pub buffer: Entity<Buffer>,
1852    pub selected_item: usize,
1853    pub scroll_handle: UniformListScrollHandle,
1854    pub deployed_from: Option<CodeActionSource>,
1855}
1856
1857impl CodeActionsMenu {
1858    fn select_first(&mut self, cx: &mut Context<Editor>) {
1859        self.selected_item = if self.scroll_handle.y_flipped() {
1860            self.actions.len() - 1
1861        } else {
1862            0
1863        };
1864        self.scroll_handle
1865            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1866        cx.notify()
1867    }
1868
1869    fn select_last(&mut self, cx: &mut Context<Editor>) {
1870        self.selected_item = if self.scroll_handle.y_flipped() {
1871            0
1872        } else {
1873            self.actions.len() - 1
1874        };
1875        self.scroll_handle
1876            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1877        cx.notify()
1878    }
1879
1880    fn select_prev(&mut self, cx: &mut Context<Editor>) {
1881        self.selected_item = if self.scroll_handle.y_flipped() {
1882            self.next_match_index()
1883        } else {
1884            self.prev_match_index()
1885        };
1886        self.scroll_handle
1887            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1888        cx.notify();
1889    }
1890
1891    fn select_next(&mut self, cx: &mut Context<Editor>) {
1892        self.selected_item = if self.scroll_handle.y_flipped() {
1893            self.prev_match_index()
1894        } else {
1895            self.next_match_index()
1896        };
1897        self.scroll_handle
1898            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1899        cx.notify();
1900    }
1901
1902    fn prev_match_index(&self) -> usize {
1903        if self.selected_item > 0 {
1904            self.selected_item - 1
1905        } else {
1906            self.actions.len() - 1
1907        }
1908    }
1909
1910    fn next_match_index(&self) -> usize {
1911        if self.selected_item + 1 < self.actions.len() {
1912            self.selected_item + 1
1913        } else {
1914            0
1915        }
1916    }
1917
1918    pub fn visible(&self) -> bool {
1919        !self.actions.is_empty()
1920    }
1921
1922    fn origin(&self) -> ContextMenuOrigin {
1923        match &self.deployed_from {
1924            Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1925                ContextMenuOrigin::GutterIndicator(*row)
1926            }
1927            Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1928            None => ContextMenuOrigin::Cursor,
1929        }
1930    }
1931
1932    fn render(
1933        &self,
1934        _style: &EditorStyle,
1935        max_height_in_lines: u32,
1936        window: &mut Window,
1937        cx: &mut Context<Editor>,
1938    ) -> AnyElement {
1939        let actions = self.actions.clone();
1940        let selected_item = self.selected_item;
1941        let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1942
1943        let list = uniform_list(
1944            "code_actions_menu",
1945            self.actions.len(),
1946            cx.processor(move |_this, range: Range<usize>, _, cx| {
1947                actions
1948                    .iter()
1949                    .skip(range.start)
1950                    .take(range.end - range.start)
1951                    .enumerate()
1952                    .map(|(ix, action)| {
1953                        let item_ix = range.start + ix;
1954                        let selected = item_ix == selected_item;
1955                        let colors = cx.theme().colors();
1956
1957                        ListItem::new(item_ix)
1958                            .inset(true)
1959                            .toggle_state(selected)
1960                            .overflow_x()
1961                            .child(
1962                                div()
1963                                    .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1964                                    .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1965                                    .overflow_hidden()
1966                                    .text_ellipsis()
1967                                    .when(is_quick_action_bar, |this| this.text_ui(cx))
1968                                    .when(selected, |this| this.text_color(colors.text_accent))
1969                                    .child(action.menu_label()),
1970                            )
1971                            .on_click(cx.listener(move |editor, _, window, cx| {
1972                                cx.stop_propagation();
1973                                if let Some(task) = editor.confirm_code_action(
1974                                    &ConfirmCodeAction {
1975                                        item_ix: Some(item_ix),
1976                                    },
1977                                    window,
1978                                    cx,
1979                                ) {
1980                                    task.detach_and_log_err(cx)
1981                                }
1982                            }))
1983                    })
1984                    .collect()
1985            }),
1986        )
1987        .occlude()
1988        .max_h(max_height_in_lines as f32 * window.line_height())
1989        .track_scroll(&self.scroll_handle)
1990        .with_width_from_item(
1991            self.actions
1992                .iter()
1993                .enumerate()
1994                .max_by_key(|(_, action)| match action {
1995                    CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1996                    CodeActionsItem::CodeAction { action, .. } => {
1997                        action.lsp_action.title().chars().count()
1998                    }
1999                    CodeActionsItem::DebugScenario(scenario) => {
2000                        format!("debug: {}", scenario.label).chars().count()
2001                    }
2002                })
2003                .map(|(ix, _)| ix),
2004        )
2005        .with_sizing_behavior(ListSizingBehavior::Infer);
2006
2007        Popover::new().child(list).into_any_element()
2008    }
2009
2010    fn render_aside(
2011        &mut self,
2012        max_size: Size<Pixels>,
2013        window: &mut Window,
2014        _cx: &mut Context<Editor>,
2015    ) -> Option<AnyElement> {
2016        let Some(action) = self.actions.get(self.selected_item) else {
2017            return None;
2018        };
2019
2020        let label = action.menu_label();
2021        let text_system = window.text_system();
2022        let mut line_wrapper = text_system.line_wrapper(
2023            window.text_style().font(),
2024            window.text_style().font_size.to_pixels(window.rem_size()),
2025        );
2026        let is_truncated = line_wrapper.should_truncate_line(
2027            &label,
2028            CODE_ACTION_MENU_MAX_WIDTH,
2029            "…",
2030            gpui::TruncateFrom::End,
2031        );
2032
2033        if is_truncated.is_none() {
2034            return None;
2035        }
2036
2037        Some(
2038            Popover::new()
2039                .child(
2040                    div()
2041                        .child(label)
2042                        .id("code_actions_menu_extended")
2043                        .px(MENU_ASIDE_X_PADDING / 2.)
2044                        .max_w(max_size.width)
2045                        .max_h(max_size.height)
2046                        .occlude(),
2047                )
2048                .into_any_element(),
2049        )
2050    }
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055    use super::*;
2056
2057    fn bold() -> HighlightStyle {
2058        FontWeight::BOLD.into()
2059    }
2060
2061    fn colored() -> HighlightStyle {
2062        HighlightStyle {
2063            fade_out: Some(0.5),
2064            ..Default::default()
2065        }
2066    }
2067
2068    #[test]
2069    fn test_split_completion_label_keeps_prefix_before_filter_range() {
2070        let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
2071            split_completion_label("&some_str: String", &(1..9), &[(11..17, colored())]);
2072
2073        assert_eq!(main_text, "&some_str");
2074        assert_eq!(suffix_text, ": String");
2075        assert_eq!(main_highlights, vec![]);
2076        assert_eq!(suffix_highlights, vec![(2..8, colored())]);
2077    }
2078
2079    #[test]
2080    fn test_split_completion_label_splits_boundary_spanning_highlight() {
2081        let ((main_text, main_highlights), (suffix_text, suffix_highlights)) =
2082            split_completion_label(
2083                "await.as_deref_mut(&mut self)",
2084                &(6..18),
2085                &[(0..29, bold())],
2086            );
2087
2088        assert_eq!(main_text, "await.as_deref_mut");
2089        assert_eq!(suffix_text, "(&mut self)");
2090        assert_eq!(main_highlights, vec![(0..18, bold())]);
2091        assert_eq!(suffix_highlights, vec![(0..11, bold())]);
2092    }
2093}
2094
Served at tenant.openagents/omega Member data and write actions are omitted.