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