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::{CopyButtonVisibility, Markdown, MarkdownElement};
  13use multi_buffer::Anchor;
  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        initial_position: Anchor,
 361        selection: Range<text::Anchor>,
 362        buffer: Entity<Buffer>,
 363        scroll_handle: Option<UniformListScrollHandle>,
 364        snippet_sort_order: SnippetSortOrder,
 365    ) -> Self {
 366        let completions = choices
 367            .iter()
 368            .map(|choice| Completion {
 369                replace_range: selection.clone(),
 370                new_text: choice.to_string(),
 371                label: CodeLabel::plain(choice.to_string(), None),
 372                match_start: None,
 373                snippet_deduplication_key: None,
 374                icon_path: None,
 375                documentation: None,
 376                confirm: None,
 377                insert_text_mode: None,
 378                source: CompletionSource::Custom,
 379            })
 380            .collect();
 381
 382        let match_candidates = Arc::new([(
 383            None,
 384            choices
 385                .iter()
 386                .enumerate()
 387                .map(|(id, completion)| StringMatchCandidate::new(id, completion))
 388                .collect(),
 389        )]);
 390        let entries = choices
 391            .iter()
 392            .enumerate()
 393            .map(|(id, completion)| StringMatch {
 394                candidate_id: id,
 395                score: 1.,
 396                positions: vec![],
 397                string: completion.clone(),
 398            })
 399            .collect();
 400        Self {
 401            id,
 402            source: CompletionsMenuSource::SnippetChoices,
 403            sort_completions,
 404            initial_position,
 405            initial_query: None,
 406            is_incomplete: false,
 407            buffer,
 408            completions: RefCell::new(completions).into(),
 409            match_candidates,
 410            entries: RefCell::new(entries).into(),
 411            selected_item: 0,
 412            filter_task: Task::ready(()),
 413            cancel_filter: Arc::new(AtomicBool::new(false)),
 414            scroll_handle: scroll_handle.unwrap_or_else(UniformListScrollHandle::new),
 415            scroll_handle_aside: ScrollHandle::new(),
 416            resolve_completions: false,
 417            show_completion_documentation: false,
 418            last_rendered_range: RefCell::new(None).into(),
 419            markdown_cache: RefCell::new(VecDeque::new()).into(),
 420            language_registry: None,
 421            language: None,
 422            display_options: CompletionDisplayOptions::default(),
 423            snippet_sort_order,
 424        }
 425    }
 426
 427    fn select_first(
 428        &mut self,
 429        provider: Option<&dyn CompletionProvider>,
 430        window: &mut Window,
 431        cx: &mut Context<Editor>,
 432    ) {
 433        let index = if self.scroll_handle.y_flipped() {
 434            self.entries.borrow().len() - 1
 435        } else {
 436            0
 437        };
 438        self.update_selection_index(index, provider, window, cx);
 439    }
 440
 441    fn select_last(
 442        &mut self,
 443        provider: Option<&dyn CompletionProvider>,
 444        window: &mut Window,
 445        cx: &mut Context<Editor>,
 446    ) {
 447        let index = if self.scroll_handle.y_flipped() {
 448            0
 449        } else {
 450            self.entries.borrow().len() - 1
 451        };
 452        self.update_selection_index(index, provider, window, cx);
 453    }
 454
 455    fn select_prev(
 456        &mut self,
 457        provider: Option<&dyn CompletionProvider>,
 458        window: &mut Window,
 459        cx: &mut Context<Editor>,
 460    ) {
 461        let index = if self.scroll_handle.y_flipped() {
 462            self.next_match_index()
 463        } else {
 464            self.prev_match_index()
 465        };
 466        self.update_selection_index(index, provider, window, cx);
 467    }
 468
 469    fn select_next(
 470        &mut self,
 471        provider: Option<&dyn CompletionProvider>,
 472        window: &mut Window,
 473        cx: &mut Context<Editor>,
 474    ) {
 475        let index = if self.scroll_handle.y_flipped() {
 476            self.prev_match_index()
 477        } else {
 478            self.next_match_index()
 479        };
 480        self.update_selection_index(index, provider, window, cx);
 481    }
 482
 483    fn update_selection_index(
 484        &mut self,
 485        match_index: usize,
 486        provider: Option<&dyn CompletionProvider>,
 487        window: &mut Window,
 488        cx: &mut Context<Editor>,
 489    ) {
 490        if self.selected_item != match_index {
 491            self.selected_item = match_index;
 492            self.handle_selection_changed(provider, window, cx);
 493        }
 494    }
 495
 496    fn prev_match_index(&self) -> usize {
 497        if self.selected_item > 0 {
 498            self.selected_item - 1
 499        } else {
 500            self.entries.borrow().len() - 1
 501        }
 502    }
 503
 504    fn next_match_index(&self) -> usize {
 505        if self.selected_item + 1 < self.entries.borrow().len() {
 506            self.selected_item + 1
 507        } else {
 508            0
 509        }
 510    }
 511
 512    fn handle_selection_changed(
 513        &mut self,
 514        provider: Option<&dyn CompletionProvider>,
 515        window: &mut Window,
 516        cx: &mut Context<Editor>,
 517    ) {
 518        self.scroll_handle
 519            .scroll_to_item(self.selected_item, ScrollStrategy::Nearest);
 520        if let Some(provider) = provider {
 521            let entries = self.entries.borrow();
 522            let entry = if self.selected_item < entries.len() {
 523                Some(&entries[self.selected_item])
 524            } else {
 525                None
 526            };
 527            provider.selection_changed(entry, window, cx);
 528        }
 529        self.resolve_visible_completions(provider, cx);
 530        self.start_markdown_parse_for_nearby_entries(cx);
 531        cx.notify();
 532    }
 533
 534    pub fn resolve_visible_completions(
 535        &mut self,
 536        provider: Option<&dyn CompletionProvider>,
 537        cx: &mut Context<Editor>,
 538    ) {
 539        if !self.resolve_completions {
 540            return;
 541        }
 542        let Some(provider) = provider else {
 543            return;
 544        };
 545
 546        let entries = self.entries.borrow();
 547        if entries.is_empty() {
 548            return;
 549        }
 550        if self.selected_item >= entries.len() {
 551            log::error!(
 552                "bug: completion selected_item >= entries.len(): {} >= {}",
 553                self.selected_item,
 554                entries.len()
 555            );
 556            self.selected_item = entries.len() - 1;
 557        }
 558
 559        // Attempt to resolve completions for every item that will be displayed. This matters
 560        // because single line documentation may be displayed inline with the completion.
 561        //
 562        // When navigating to the very beginning or end of completions, `last_rendered_range` may
 563        // have no overlap with the completions that will be displayed, so instead use a range based
 564        // on the last rendered count.
 565        const APPROXIMATE_VISIBLE_COUNT: usize = 12;
 566        let last_rendered_range = self.last_rendered_range.borrow().clone();
 567        let visible_count = last_rendered_range
 568            .clone()
 569            .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
 570        let entry_range = if self.selected_item == 0 {
 571            0..min(visible_count, entries.len())
 572        } else if self.selected_item == entries.len() - 1 {
 573            entries.len().saturating_sub(visible_count)..entries.len()
 574        } else {
 575            last_rendered_range.map_or(0..0, |range| {
 576                min(range.start, entries.len())..min(range.end, entries.len())
 577            })
 578        };
 579
 580        // Expand the range to resolve more completions than are predicted to be visible, to reduce
 581        // jank on navigation.
 582        let entry_indices = util::expanded_and_wrapped_usize_range(
 583            entry_range,
 584            RESOLVE_BEFORE_ITEMS,
 585            RESOLVE_AFTER_ITEMS,
 586            entries.len(),
 587        );
 588
 589        // Avoid work by sometimes filtering out completions that already have documentation.
 590        // This filtering doesn't happen if the completions are currently being updated.
 591        let completions = self.completions.borrow();
 592        let candidate_ids = entry_indices
 593            .map(|i| entries[i].candidate_id)
 594            .filter(|i| completions[*i].documentation.is_none());
 595
 596        // Current selection is always resolved even if it already has documentation, to handle
 597        // out-of-spec language servers that return more results later.
 598        let selected_candidate_id = entries[self.selected_item].candidate_id;
 599        let candidate_ids = iter::once(selected_candidate_id)
 600            .chain(candidate_ids.filter(|id| *id != selected_candidate_id))
 601            .collect::<Vec<usize>>();
 602        drop(entries);
 603
 604        if candidate_ids.is_empty() {
 605            return;
 606        }
 607
 608        let resolve_task = provider.resolve_completions(
 609            self.buffer.clone(),
 610            candidate_ids,
 611            self.completions.clone(),
 612            cx,
 613        );
 614
 615        let completion_id = self.id;
 616        cx.spawn(async move |editor, cx| {
 617            if let Some(true) = resolve_task.await.log_err() {
 618                editor
 619                    .update(cx, |editor, cx| {
 620                        // `resolve_completions` modified state affecting display.
 621                        cx.notify();
 622                        editor.with_completions_menu_matching_id(completion_id, |menu| {
 623                            if let Some(menu) = menu {
 624                                menu.start_markdown_parse_for_nearby_entries(cx)
 625                            }
 626                        });
 627                    })
 628                    .ok();
 629            }
 630        })
 631        .detach();
 632    }
 633
 634    fn start_markdown_parse_for_nearby_entries(&self, cx: &mut Context<Editor>) {
 635        // Enqueue parse tasks of nearer items first.
 636        //
 637        // TODO: This means that the nearer items will actually be further back in the cache, which
 638        // is not ideal. In practice this is fine because `get_or_create_markdown` moves the current
 639        // selection to the front (when `is_render = true`).
 640        let entry_indices = util::wrapped_usize_outward_from(
 641            self.selected_item,
 642            MARKDOWN_CACHE_BEFORE_ITEMS,
 643            MARKDOWN_CACHE_AFTER_ITEMS,
 644            self.entries.borrow().len(),
 645        );
 646
 647        for index in entry_indices {
 648            self.get_or_create_entry_markdown(index, cx);
 649        }
 650    }
 651
 652    fn get_or_create_entry_markdown(
 653        &self,
 654        index: usize,
 655        cx: &mut Context<Editor>,
 656    ) -> Option<Entity<Markdown>> {
 657        let entries = self.entries.borrow();
 658        if index >= entries.len() {
 659            return None;
 660        }
 661        let candidate_id = entries[index].candidate_id;
 662        let completions = self.completions.borrow();
 663        match &completions[candidate_id].documentation {
 664            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
 665                .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
 666                .map(|(_, markdown)| markdown),
 667            Some(_) => None,
 668            _ => None,
 669        }
 670    }
 671
 672    fn get_or_create_markdown(
 673        &self,
 674        candidate_id: usize,
 675        source: Option<&SharedString>,
 676        is_render: bool,
 677        completions: &[Completion],
 678        cx: &mut Context<Editor>,
 679    ) -> Option<(bool, Entity<Markdown>)> {
 680        let mut markdown_cache = self.markdown_cache.borrow_mut();
 681
 682        let mut has_completion_match_cache_entry = false;
 683        let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
 684            MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
 685            MarkdownCacheKey::ForCompletionMatch { .. } => {
 686                has_completion_match_cache_entry = true;
 687                false
 688            }
 689        });
 690
 691        if has_completion_match_cache_entry && matching_entry.is_none() {
 692            if let Some(source) = source {
 693                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
 694                    matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
 695                                if markdown_source == source)
 696                });
 697            } else {
 698                // Heuristic guess that documentation can be reused when new_text matches. This is
 699                // to mitigate documentation flicker while typing. If this is wrong, then resolution
 700                // should cause the correct documentation to be displayed soon.
 701                let completion = &completions[candidate_id];
 702                matching_entry = markdown_cache.iter().find_position(|(key, _)| {
 703                    matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
 704                                if new_text == &completion.new_text)
 705                });
 706            }
 707        }
 708
 709        if let Some((cache_index, (key, markdown))) = matching_entry {
 710            let markdown = markdown.clone();
 711
 712            // Since the markdown source matches, the key can now be ForCandidate.
 713            if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
 714                markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
 715            }
 716
 717            if is_render && cache_index != 0 {
 718                // Move the current selection's cache entry to the front.
 719                markdown_cache.rotate_right(1);
 720                let cache_len = markdown_cache.len();
 721                markdown_cache.swap(0, (cache_index + 1) % cache_len);
 722            }
 723
 724            let is_parsing = markdown.update(cx, |markdown, cx| {
 725                if let Some(source) = source {
 726                    // `reset` is called as it's possible for documentation to change due to resolve
 727                    // requests. It does nothing if `source` is unchanged.
 728                    markdown.reset(source.clone(), cx);
 729                }
 730                markdown.is_parsing()
 731            });
 732            return Some((is_parsing, markdown));
 733        }
 734
 735        let Some(source) = source else {
 736            // Can't create markdown as there is no source.
 737            return None;
 738        };
 739
 740        if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
 741            let markdown = cx.new(|cx| {
 742                Markdown::new(
 743                    source.clone(),
 744                    self.language_registry.clone(),
 745                    self.language.clone(),
 746                    cx,
 747                )
 748            });
 749            // Handles redraw when the markdown is done parsing. The current render is for a
 750            // deferred draw, and so without this did not redraw when `markdown` notified.
 751            cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
 752            markdown_cache.push_front((
 753                MarkdownCacheKey::ForCandidate { candidate_id },
 754                markdown.clone(),
 755            ));
 756            Some((true, markdown))
 757        } else {
 758            debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
 759            // Moves the last cache entry to the start. The ring buffer is full, so this does no
 760            // copying and just shifts indexes.
 761            markdown_cache.rotate_right(1);
 762            markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
 763            let markdown = &markdown_cache[0].1;
 764            markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
 765            Some((true, markdown.clone()))
 766        }
 767    }
 768
 769    pub fn visible(&self) -> bool {
 770        !self.entries.borrow().is_empty()
 771    }
 772
 773    fn origin(&self) -> ContextMenuOrigin {
 774        ContextMenuOrigin::Cursor
 775    }
 776
 777    fn render(
 778        &self,
 779        style: &EditorStyle,
 780        max_height_in_lines: u32,
 781        window: &mut Window,
 782        cx: &mut Context<Editor>,
 783    ) -> AnyElement {
 784        let show_completion_documentation = self.show_completion_documentation;
 785        let completion_detail_alignment =
 786            EditorSettings::get_global(cx).completion_detail_alignment;
 787        let widest_completion_ix = if self.display_options.dynamic_width {
 788            let completions = self.completions.borrow();
 789            let widest_completion_ix = self
 790                .entries
 791                .borrow()
 792                .iter()
 793                .enumerate()
 794                .max_by_key(|(_, mat)| {
 795                    let completion = &completions[mat.candidate_id];
 796                    let documentation = &completion.documentation;
 797
 798                    let mut len = completion.label.text.chars().count();
 799                    if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
 800                        if show_completion_documentation {
 801                            len += text.chars().count();
 802                        }
 803                    }
 804
 805                    len
 806                })
 807                .map(|(ix, _)| ix);
 808            drop(completions);
 809            widest_completion_ix
 810        } else {
 811            None
 812        };
 813
 814        let selected_item = self.selected_item;
 815        let completions = self.completions.clone();
 816        let entries = self.entries.clone();
 817        let last_rendered_range = self.last_rendered_range.clone();
 818        let style = style.clone();
 819        let list = uniform_list(
 820            "completions",
 821            self.entries.borrow().len(),
 822            cx.processor(move |_editor, range: Range<usize>, _window, cx| {
 823                last_rendered_range.borrow_mut().replace(range.clone());
 824                let start_ix = range.start;
 825                let completions_guard = completions.borrow_mut();
 826
 827                entries.borrow()[range]
 828                    .iter()
 829                    .enumerate()
 830                    .map(|(ix, mat)| {
 831                        let item_ix = start_ix + ix;
 832                        let completion = &completions_guard[mat.candidate_id];
 833                        let documentation = if show_completion_documentation {
 834                            &completion.documentation
 835                        } else {
 836                            &None
 837                        };
 838
 839                        let filter_start = completion.label.filter_range.start;
 840
 841                        let highlights = gpui::combine_highlights(
 842                            mat.ranges().map(|range| {
 843                                (
 844                                    filter_start + range.start..filter_start + range.end,
 845                                    FontWeight::BOLD.into(),
 846                                )
 847                            }),
 848                            styled_runs_for_code_label(
 849                                &completion.label,
 850                                &style.syntax,
 851                                &style.local_player,
 852                            )
 853                            .map(|(range, mut highlight)| {
 854                                // Ignore font weight for syntax highlighting, as we'll use it
 855                                // for fuzzy matches.
 856                                highlight.font_weight = None;
 857                                if completion
 858                                    .source
 859                                    .lsp_completion(false)
 860                                    .and_then(|lsp_completion| {
 861                                        match (lsp_completion.deprecated, &lsp_completion.tags) {
 862                                            (Some(true), _) => Some(true),
 863                                            (_, Some(tags)) => {
 864                                                Some(tags.contains(&CompletionItemTag::DEPRECATED))
 865                                            }
 866                                            _ => None,
 867                                        }
 868                                    })
 869                                    .unwrap_or(false)
 870                                {
 871                                    highlight.strikethrough = Some(StrikethroughStyle {
 872                                        thickness: 1.0.into(),
 873                                        ..Default::default()
 874                                    });
 875                                    highlight.color = Some(cx.theme().colors().text_muted);
 876                                }
 877
 878                                (range, highlight)
 879                            }),
 880                        );
 881
 882                        let highlights: Vec<_> = highlights.collect();
 883
 884                        let filter_range = &completion.label.filter_range;
 885                        let full_text = &completion.label.text;
 886
 887                        let main_text: String = full_text[filter_range.clone()].to_string();
 888                        let main_highlights: Vec<_> = highlights
 889                            .iter()
 890                            .filter_map(|(range, highlight)| {
 891                                if range.end <= filter_range.start
 892                                    || range.start >= filter_range.end
 893                                {
 894                                    return None;
 895                                }
 896                                let clamped_start =
 897                                    range.start.max(filter_range.start) - filter_range.start;
 898                                let clamped_end =
 899                                    range.end.min(filter_range.end) - filter_range.start;
 900                                Some((clamped_start..clamped_end, (*highlight)))
 901                            })
 902                            .collect();
 903                        let main_label = StyledText::new(main_text)
 904                            .with_default_highlights(&style.text, main_highlights);
 905
 906                        let suffix_text: String = full_text[filter_range.end..].to_string();
 907                        let suffix_highlights: Vec<_> = highlights
 908                            .iter()
 909                            .filter_map(|(range, highlight)| {
 910                                if range.end <= filter_range.end {
 911                                    return None;
 912                                }
 913                                let shifted_start = range.start.saturating_sub(filter_range.end);
 914                                let shifted_end = range.end - filter_range.end;
 915                                Some((shifted_start..shifted_end, (*highlight)))
 916                            })
 917                            .collect();
 918                        let suffix_label = if !suffix_text.is_empty() {
 919                            Some(
 920                                StyledText::new(suffix_text)
 921                                    .with_default_highlights(&style.text, suffix_highlights),
 922                            )
 923                        } else {
 924                            None
 925                        };
 926
 927                        let left_aligned_suffix =
 928                            matches!(completion_detail_alignment, CompletionDetailAlignment::Left);
 929
 930                        let right_aligned_suffix = matches!(
 931                            completion_detail_alignment,
 932                            CompletionDetailAlignment::Right,
 933                        );
 934
 935                        let documentation_label = match documentation {
 936                            Some(CompletionDocumentation::SingleLine(text))
 937                            | Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
 938                                single_line: text,
 939                                ..
 940                            }) => {
 941                                if text.trim().is_empty() {
 942                                    None
 943                                } else {
 944                                    Some(
 945                                        Label::new(text.trim().to_string())
 946                                            .ml_4()
 947                                            .size(LabelSize::Small)
 948                                            .color(Color::Muted),
 949                                    )
 950                                }
 951                            }
 952                            _ => None,
 953                        };
 954
 955                        let start_slot = completion
 956                            .color()
 957                            .map(|color| {
 958                                div()
 959                                    .flex_shrink_0()
 960                                    .size_3p5()
 961                                    .rounded_xs()
 962                                    .bg(color)
 963                                    .into_any_element()
 964                            })
 965                            .or_else(|| {
 966                                completion.icon_path.as_ref().map(|path| {
 967                                    Icon::from_path(path)
 968                                        .size(IconSize::XSmall)
 969                                        .color(Color::Muted)
 970                                        .into_any_element()
 971                                })
 972                            });
 973
 974                        div()
 975                            .min_w(COMPLETION_MENU_MIN_WIDTH)
 976                            .max_w(COMPLETION_MENU_MAX_WIDTH)
 977                            .child(
 978                                ListItem::new(mat.candidate_id)
 979                                    .inset(true)
 980                                    .toggle_state(item_ix == selected_item)
 981                                    .on_click(cx.listener(move |editor, _event, window, cx| {
 982                                        cx.stop_propagation();
 983                                        if let Some(task) = editor.confirm_completion(
 984                                            &ConfirmCompletion {
 985                                                item_ix: Some(item_ix),
 986                                            },
 987                                            window,
 988                                            cx,
 989                                        ) {
 990                                            task.detach_and_log_err(cx)
 991                                        }
 992                                    }))
 993                                    .start_slot::<AnyElement>(start_slot)
 994                                    .child(
 995                                        h_flex()
 996                                            .min_w_0()
 997                                            .w_full()
 998                                            .when(left_aligned_suffix, |this| this.justify_start())
 999                                            .when(right_aligned_suffix, |this| {
1000                                                this.justify_between()
1001                                            })
1002                                            .child(
1003                                                div()
1004                                                    .flex_none()
1005                                                    .whitespace_nowrap()
1006                                                    .child(main_label),
1007                                            )
1008                                            .when_some(suffix_label, |this, suffix| {
1009                                                this.child(div().truncate().child(suffix))
1010                                            }),
1011                                    )
1012                                    .end_slot::<Label>(documentation_label),
1013                            )
1014                    })
1015                    .collect()
1016            }),
1017        )
1018        .occlude()
1019        .max_h(max_height_in_lines as f32 * window.line_height())
1020        .track_scroll(&self.scroll_handle)
1021        .with_sizing_behavior(ListSizingBehavior::Infer)
1022        .map(|this| {
1023            if self.display_options.dynamic_width {
1024                this.with_width_from_item(widest_completion_ix)
1025            } else {
1026                this.w(rems(34.))
1027            }
1028        });
1029
1030        Popover::new()
1031            .child(
1032                div().child(list).custom_scrollbars(
1033                    Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
1034                        .show_along(ScrollAxes::Vertical)
1035                        .tracked_scroll_handle(&self.scroll_handle),
1036                    window,
1037                    cx,
1038                ),
1039            )
1040            .into_any_element()
1041    }
1042
1043    fn render_aside(
1044        &mut self,
1045        max_size: Size<Pixels>,
1046        window: &mut Window,
1047        cx: &mut Context<Editor>,
1048    ) -> Option<AnyElement> {
1049        if !self.show_completion_documentation {
1050            return None;
1051        }
1052
1053        let mat = &self.entries.borrow()[self.selected_item];
1054        let completions = self.completions.borrow();
1055        let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
1056            Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
1057            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1058                plain_text: Some(text),
1059                ..
1060            }) => div().child(text.clone()),
1061            Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
1062                let Some((false, markdown)) = self.get_or_create_markdown(
1063                    mat.candidate_id,
1064                    Some(source),
1065                    true,
1066                    &completions,
1067                    cx,
1068                ) else {
1069                    return None;
1070                };
1071                Self::render_markdown(markdown, window, cx)
1072            }
1073            None => {
1074                // Handle the case where documentation hasn't yet been resolved but there's a
1075                // `new_text` match in the cache.
1076                //
1077                // TODO: It's inconsistent that documentation caching based on matching `new_text`
1078                // only works for markdown. Consider generally caching the results of resolving
1079                // completions.
1080                let Some((false, markdown)) =
1081                    self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
1082                else {
1083                    return None;
1084                };
1085                Self::render_markdown(markdown, window, cx)
1086            }
1087            Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
1088            Some(CompletionDocumentation::SingleLine(_)) => return None,
1089            Some(CompletionDocumentation::Undocumented) => return None,
1090            Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
1091                plain_text: None,
1092                ..
1093            }) => {
1094                return None;
1095            }
1096        };
1097
1098        Some(
1099            Popover::new()
1100                .child(
1101                    multiline_docs
1102                        .id("multiline_docs")
1103                        .px(MENU_ASIDE_X_PADDING / 2.)
1104                        .max_w(max_size.width)
1105                        .max_h(max_size.height)
1106                        .overflow_y_scroll()
1107                        .track_scroll(&self.scroll_handle_aside)
1108                        .occlude(),
1109                )
1110                .into_any_element(),
1111        )
1112    }
1113
1114    fn render_markdown(
1115        markdown: Entity<Markdown>,
1116        window: &mut Window,
1117        cx: &mut Context<Editor>,
1118    ) -> Div {
1119        div().child(
1120            MarkdownElement::new(markdown, hover_markdown_style(window, cx))
1121                .code_block_renderer(markdown::CodeBlockRenderer::Default {
1122                    copy_button_visibility: CopyButtonVisibility::Hidden,
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 action: CodeAction,
1385    pub provider: Rc<dyn CodeActionProvider>,
1386}
1387
1388#[derive(Clone)]
1389pub struct CodeActionContents {
1390    tasks: Option<Rc<ResolvedTasks>>,
1391    actions: Option<Rc<[AvailableCodeAction]>>,
1392    debug_scenarios: Vec<DebugScenario>,
1393    pub(crate) context: TaskContext,
1394}
1395
1396impl CodeActionContents {
1397    pub(crate) fn new(
1398        tasks: Option<ResolvedTasks>,
1399        actions: Option<Rc<[AvailableCodeAction]>>,
1400        debug_scenarios: Vec<DebugScenario>,
1401        context: TaskContext,
1402    ) -> Self {
1403        Self {
1404            tasks: tasks.map(Rc::new),
1405            actions,
1406            debug_scenarios,
1407            context,
1408        }
1409    }
1410
1411    pub fn tasks(&self) -> Option<&ResolvedTasks> {
1412        self.tasks.as_deref()
1413    }
1414
1415    fn len(&self) -> usize {
1416        let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1417        let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1418        tasks_len + code_actions_len + self.debug_scenarios.len()
1419    }
1420
1421    pub fn is_empty(&self) -> bool {
1422        self.len() == 0
1423    }
1424
1425    fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1426        self.tasks
1427            .iter()
1428            .flat_map(|tasks| {
1429                tasks
1430                    .templates
1431                    .iter()
1432                    .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1433            })
1434            .chain(self.actions.iter().flat_map(|actions| {
1435                actions.iter().map(|available| CodeActionsItem::CodeAction {
1436                    action: available.action.clone(),
1437                    provider: available.provider.clone(),
1438                })
1439            }))
1440            .chain(
1441                self.debug_scenarios
1442                    .iter()
1443                    .cloned()
1444                    .map(CodeActionsItem::DebugScenario),
1445            )
1446    }
1447
1448    pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1449        if let Some(tasks) = &self.tasks {
1450            if let Some((kind, task)) = tasks.templates.get(index) {
1451                return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1452            } else {
1453                index -= tasks.templates.len();
1454            }
1455        }
1456        if let Some(actions) = &self.actions {
1457            if let Some(available) = actions.get(index) {
1458                return Some(CodeActionsItem::CodeAction {
1459                    action: available.action.clone(),
1460                    provider: available.provider.clone(),
1461                });
1462            } else {
1463                index -= actions.len();
1464            }
1465        }
1466
1467        self.debug_scenarios
1468            .get(index)
1469            .cloned()
1470            .map(CodeActionsItem::DebugScenario)
1471    }
1472}
1473
1474#[derive(Clone)]
1475pub enum CodeActionsItem {
1476    Task(TaskSourceKind, ResolvedTask),
1477    CodeAction {
1478        action: CodeAction,
1479        provider: Rc<dyn CodeActionProvider>,
1480    },
1481    DebugScenario(DebugScenario),
1482}
1483
1484impl CodeActionsItem {
1485    pub fn label(&self) -> String {
1486        match self {
1487            Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1488            Self::Task(_, task) => task.resolved_label.clone(),
1489            Self::DebugScenario(scenario) => scenario.label.to_string(),
1490        }
1491    }
1492
1493    pub fn menu_label(&self) -> String {
1494        match self {
1495            Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1496            Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1497            Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1498        }
1499    }
1500}
1501
1502pub struct CodeActionsMenu {
1503    pub actions: CodeActionContents,
1504    pub buffer: Entity<Buffer>,
1505    pub selected_item: usize,
1506    pub scroll_handle: UniformListScrollHandle,
1507    pub deployed_from: Option<CodeActionSource>,
1508}
1509
1510impl CodeActionsMenu {
1511    fn select_first(&mut self, cx: &mut Context<Editor>) {
1512        self.selected_item = if self.scroll_handle.y_flipped() {
1513            self.actions.len() - 1
1514        } else {
1515            0
1516        };
1517        self.scroll_handle
1518            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1519        cx.notify()
1520    }
1521
1522    fn select_last(&mut self, cx: &mut Context<Editor>) {
1523        self.selected_item = if self.scroll_handle.y_flipped() {
1524            0
1525        } else {
1526            self.actions.len() - 1
1527        };
1528        self.scroll_handle
1529            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1530        cx.notify()
1531    }
1532
1533    fn select_prev(&mut self, cx: &mut Context<Editor>) {
1534        self.selected_item = if self.scroll_handle.y_flipped() {
1535            self.next_match_index()
1536        } else {
1537            self.prev_match_index()
1538        };
1539        self.scroll_handle
1540            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1541        cx.notify();
1542    }
1543
1544    fn select_next(&mut self, cx: &mut Context<Editor>) {
1545        self.selected_item = if self.scroll_handle.y_flipped() {
1546            self.prev_match_index()
1547        } else {
1548            self.next_match_index()
1549        };
1550        self.scroll_handle
1551            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1552        cx.notify();
1553    }
1554
1555    fn prev_match_index(&self) -> usize {
1556        if self.selected_item > 0 {
1557            self.selected_item - 1
1558        } else {
1559            self.actions.len() - 1
1560        }
1561    }
1562
1563    fn next_match_index(&self) -> usize {
1564        if self.selected_item + 1 < self.actions.len() {
1565            self.selected_item + 1
1566        } else {
1567            0
1568        }
1569    }
1570
1571    pub fn visible(&self) -> bool {
1572        !self.actions.is_empty()
1573    }
1574
1575    fn origin(&self) -> ContextMenuOrigin {
1576        match &self.deployed_from {
1577            Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1578                ContextMenuOrigin::GutterIndicator(*row)
1579            }
1580            Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1581            None => ContextMenuOrigin::Cursor,
1582        }
1583    }
1584
1585    fn render(
1586        &self,
1587        _style: &EditorStyle,
1588        max_height_in_lines: u32,
1589        window: &mut Window,
1590        cx: &mut Context<Editor>,
1591    ) -> AnyElement {
1592        let actions = self.actions.clone();
1593        let selected_item = self.selected_item;
1594        let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1595
1596        let list = uniform_list(
1597            "code_actions_menu",
1598            self.actions.len(),
1599            cx.processor(move |_this, range: Range<usize>, _, cx| {
1600                actions
1601                    .iter()
1602                    .skip(range.start)
1603                    .take(range.end - range.start)
1604                    .enumerate()
1605                    .map(|(ix, action)| {
1606                        let item_ix = range.start + ix;
1607                        let selected = item_ix == selected_item;
1608                        let colors = cx.theme().colors();
1609
1610                        ListItem::new(item_ix)
1611                            .inset(true)
1612                            .toggle_state(selected)
1613                            .overflow_x()
1614                            .child(
1615                                div()
1616                                    .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1617                                    .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1618                                    .overflow_hidden()
1619                                    .text_ellipsis()
1620                                    .when(is_quick_action_bar, |this| this.text_ui(cx))
1621                                    .when(selected, |this| this.text_color(colors.text_accent))
1622                                    .child(action.menu_label()),
1623                            )
1624                            .on_click(cx.listener(move |editor, _, window, cx| {
1625                                cx.stop_propagation();
1626                                if let Some(task) = editor.confirm_code_action(
1627                                    &ConfirmCodeAction {
1628                                        item_ix: Some(item_ix),
1629                                    },
1630                                    window,
1631                                    cx,
1632                                ) {
1633                                    task.detach_and_log_err(cx)
1634                                }
1635                            }))
1636                    })
1637                    .collect()
1638            }),
1639        )
1640        .occlude()
1641        .max_h(max_height_in_lines as f32 * window.line_height())
1642        .track_scroll(&self.scroll_handle)
1643        .with_width_from_item(
1644            self.actions
1645                .iter()
1646                .enumerate()
1647                .max_by_key(|(_, action)| match action {
1648                    CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1649                    CodeActionsItem::CodeAction { action, .. } => {
1650                        action.lsp_action.title().chars().count()
1651                    }
1652                    CodeActionsItem::DebugScenario(scenario) => {
1653                        format!("debug: {}", scenario.label).chars().count()
1654                    }
1655                })
1656                .map(|(ix, _)| ix),
1657        )
1658        .with_sizing_behavior(ListSizingBehavior::Infer);
1659
1660        Popover::new().child(list).into_any_element()
1661    }
1662
1663    fn render_aside(
1664        &mut self,
1665        max_size: Size<Pixels>,
1666        window: &mut Window,
1667        _cx: &mut Context<Editor>,
1668    ) -> Option<AnyElement> {
1669        let Some(action) = self.actions.get(self.selected_item) else {
1670            return None;
1671        };
1672
1673        let label = action.menu_label();
1674        let text_system = window.text_system();
1675        let mut line_wrapper = text_system.line_wrapper(
1676            window.text_style().font(),
1677            window.text_style().font_size.to_pixels(window.rem_size()),
1678        );
1679        let is_truncated = line_wrapper.should_truncate_line(
1680            &label,
1681            CODE_ACTION_MENU_MAX_WIDTH,
1682            "",
1683            gpui::TruncateFrom::End,
1684        );
1685
1686        if is_truncated.is_none() {
1687            return None;
1688        }
1689
1690        Some(
1691            Popover::new()
1692                .child(
1693                    div()
1694                        .child(label)
1695                        .id("code_actions_menu_extended")
1696                        .px(MENU_ASIDE_X_PADDING / 2.)
1697                        .max_w(max_size.width)
1698                        .max_h(max_size.height)
1699                        .occlude(),
1700                )
1701                .into_any_element(),
1702        )
1703    }
1704}