code_context_menus.rs

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