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_exact_case_matches: Reverse<usize>,
1260                sort_text: Option<&'a str>,
1261                sort_kind: usize,
1262                sort_label: &'a str,
1263            },
1264            OtherMatch {
1265                sort_score: Reverse<OrderedFloat<f64>>,
1266            },
1267        }
1268
1269        let query_start_lower = query
1270            .as_ref()
1271            .and_then(|q| q.chars().next())
1272            .and_then(|c| c.to_lowercase().next());
1273
1274        if snippet_sort_order == SnippetSortOrder::None {
1275            matches
1276                .retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
1277        }
1278
1279        matches.sort_unstable_by_key(|string_match| {
1280            let completion = &completions[string_match.candidate_id];
1281
1282            let sort_text = match &completion.source {
1283                CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
1284                CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
1285                _ => None,
1286            };
1287
1288            let (sort_kind, sort_label) = completion.sort_key();
1289
1290            let score = string_match.score;
1291            let sort_score = Reverse(OrderedFloat(score));
1292
1293            // Snippets do their own first-letter matching logic elsewhere.
1294            let is_snippet = completion.is_snippet_kind();
1295            let query_start_doesnt_match_split_words = !is_snippet
1296                && query_start_lower
1297                    .map(|query_char| {
1298                        !split_words(&string_match.string).any(|word| {
1299                            word.chars().next().and_then(|c| c.to_lowercase().next())
1300                                == Some(query_char)
1301                        })
1302                    })
1303                    .unwrap_or(false);
1304
1305            if query_start_doesnt_match_split_words {
1306                MatchTier::OtherMatch { sort_score }
1307            } else {
1308                let sort_snippet = match snippet_sort_order {
1309                    SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1310                    SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1311                    SnippetSortOrder::Inline => Reverse(0),
1312                    SnippetSortOrder::None => Reverse(0),
1313                };
1314                let sort_positions = string_match.positions.clone();
1315                let sort_exact_case_matches = Reverse(exact_case_match_count(
1316                    query.unwrap_or_default(),
1317                    string_match,
1318                ));
1319                // This exact matching won't work for multi-word snippets, but it's fine
1320                let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1321                    1
1322                } else {
1323                    0
1324                });
1325
1326                MatchTier::WordStartMatch {
1327                    sort_exact,
1328                    sort_snippet,
1329                    sort_score,
1330                    sort_positions,
1331                    sort_exact_case_matches,
1332                    sort_text,
1333                    sort_kind,
1334                    sort_label,
1335                }
1336            }
1337        });
1338
1339        matches
1340    }
1341
1342    pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
1343        self.markdown_cache = prev_menu.markdown_cache.clone();
1344
1345        // Convert ForCandidate cache keys to ForCompletionMatch keys.
1346        let prev_completions = prev_menu.completions.borrow();
1347        self.markdown_cache
1348            .borrow_mut()
1349            .retain_mut(|(key, _markdown)| match key {
1350                MarkdownCacheKey::ForCompletionMatch { .. } => true,
1351                MarkdownCacheKey::ForCandidate { candidate_id } => {
1352                    if let Some(completion) = prev_completions.get(*candidate_id) {
1353                        match &completion.documentation {
1354                            Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
1355                                *key = MarkdownCacheKey::ForCompletionMatch {
1356                                    new_text: completion.new_text.clone(),
1357                                    markdown_source: source.clone(),
1358                                };
1359                                true
1360                            }
1361                            _ => false,
1362                        }
1363                    } else {
1364                        false
1365                    }
1366                }
1367            });
1368    }
1369
1370    pub fn scroll_aside(
1371        &mut self,
1372        amount: ScrollAmount,
1373        window: &mut Window,
1374        cx: &mut Context<Editor>,
1375    ) {
1376        let mut offset = self.scroll_handle_aside.offset();
1377
1378        offset.y -= amount.pixels(
1379            window.line_height(),
1380            self.scroll_handle_aside.bounds().size.height - px(16.),
1381        ) / 2.0;
1382
1383        cx.notify();
1384        self.scroll_handle_aside.set_offset(offset);
1385    }
1386}
1387
1388fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize {
1389    let mut exact_matches = 0;
1390    let mut query_chars = query.chars();
1391    let mut next_query_char = query_chars.next();
1392    let mut matched_positions = string_match.positions.iter().copied().peekable();
1393
1394    for (index, candidate_char) in string_match.string.char_indices() {
1395        if matched_positions.peek() == Some(&index) {
1396            let Some(query_char) = next_query_char else {
1397                break;
1398            };
1399
1400            if query_char == candidate_char {
1401                exact_matches += 1;
1402            }
1403
1404            matched_positions.next();
1405            next_query_char = query_chars.next();
1406        }
1407    }
1408
1409    exact_matches
1410}
1411
1412#[derive(Clone)]
1413pub struct AvailableCodeAction {
1414    pub action: CodeAction,
1415    pub provider: Rc<dyn CodeActionProvider>,
1416}
1417
1418#[derive(Clone)]
1419pub struct CodeActionContents {
1420    tasks: Option<Rc<ResolvedTasks>>,
1421    actions: Option<Rc<[AvailableCodeAction]>>,
1422    debug_scenarios: Vec<DebugScenario>,
1423    pub(crate) context: TaskContext,
1424}
1425
1426impl CodeActionContents {
1427    pub(crate) fn new(
1428        tasks: Option<ResolvedTasks>,
1429        actions: Option<Rc<[AvailableCodeAction]>>,
1430        debug_scenarios: Vec<DebugScenario>,
1431        context: TaskContext,
1432    ) -> Self {
1433        Self {
1434            tasks: tasks.map(Rc::new),
1435            actions,
1436            debug_scenarios,
1437            context,
1438        }
1439    }
1440
1441    pub fn tasks(&self) -> Option<&ResolvedTasks> {
1442        self.tasks.as_deref()
1443    }
1444
1445    fn len(&self) -> usize {
1446        let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
1447        let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
1448        tasks_len + code_actions_len + self.debug_scenarios.len()
1449    }
1450
1451    pub fn is_empty(&self) -> bool {
1452        self.len() == 0
1453    }
1454
1455    fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
1456        self.tasks
1457            .iter()
1458            .flat_map(|tasks| {
1459                tasks
1460                    .templates
1461                    .iter()
1462                    .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
1463            })
1464            .chain(self.actions.iter().flat_map(|actions| {
1465                actions.iter().map(|available| CodeActionsItem::CodeAction {
1466                    action: available.action.clone(),
1467                    provider: available.provider.clone(),
1468                })
1469            }))
1470            .chain(
1471                self.debug_scenarios
1472                    .iter()
1473                    .cloned()
1474                    .map(CodeActionsItem::DebugScenario),
1475            )
1476    }
1477
1478    pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
1479        if let Some(tasks) = &self.tasks {
1480            if let Some((kind, task)) = tasks.templates.get(index) {
1481                return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
1482            } else {
1483                index -= tasks.templates.len();
1484            }
1485        }
1486        if let Some(actions) = &self.actions {
1487            if let Some(available) = actions.get(index) {
1488                return Some(CodeActionsItem::CodeAction {
1489                    action: available.action.clone(),
1490                    provider: available.provider.clone(),
1491                });
1492            } else {
1493                index -= actions.len();
1494            }
1495        }
1496
1497        self.debug_scenarios
1498            .get(index)
1499            .cloned()
1500            .map(CodeActionsItem::DebugScenario)
1501    }
1502}
1503
1504#[derive(Clone)]
1505pub enum CodeActionsItem {
1506    Task(TaskSourceKind, ResolvedTask),
1507    CodeAction {
1508        action: CodeAction,
1509        provider: Rc<dyn CodeActionProvider>,
1510    },
1511    DebugScenario(DebugScenario),
1512}
1513
1514impl CodeActionsItem {
1515    pub fn label(&self) -> String {
1516        match self {
1517            Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
1518            Self::Task(_, task) => task.resolved_label.clone(),
1519            Self::DebugScenario(scenario) => scenario.label.to_string(),
1520        }
1521    }
1522
1523    pub fn menu_label(&self) -> String {
1524        match self {
1525            Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
1526            Self::Task(_, task) => task.resolved_label.replace("\n", ""),
1527            Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
1528        }
1529    }
1530}
1531
1532pub struct CodeActionsMenu {
1533    pub actions: CodeActionContents,
1534    pub buffer: Entity<Buffer>,
1535    pub selected_item: usize,
1536    pub scroll_handle: UniformListScrollHandle,
1537    pub deployed_from: Option<CodeActionSource>,
1538}
1539
1540impl CodeActionsMenu {
1541    fn select_first(&mut self, cx: &mut Context<Editor>) {
1542        self.selected_item = if self.scroll_handle.y_flipped() {
1543            self.actions.len() - 1
1544        } else {
1545            0
1546        };
1547        self.scroll_handle
1548            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1549        cx.notify()
1550    }
1551
1552    fn select_last(&mut self, cx: &mut Context<Editor>) {
1553        self.selected_item = if self.scroll_handle.y_flipped() {
1554            0
1555        } else {
1556            self.actions.len() - 1
1557        };
1558        self.scroll_handle
1559            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1560        cx.notify()
1561    }
1562
1563    fn select_prev(&mut self, cx: &mut Context<Editor>) {
1564        self.selected_item = if self.scroll_handle.y_flipped() {
1565            self.next_match_index()
1566        } else {
1567            self.prev_match_index()
1568        };
1569        self.scroll_handle
1570            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1571        cx.notify();
1572    }
1573
1574    fn select_next(&mut self, cx: &mut Context<Editor>) {
1575        self.selected_item = if self.scroll_handle.y_flipped() {
1576            self.prev_match_index()
1577        } else {
1578            self.next_match_index()
1579        };
1580        self.scroll_handle
1581            .scroll_to_item(self.selected_item, ScrollStrategy::Top);
1582        cx.notify();
1583    }
1584
1585    fn prev_match_index(&self) -> usize {
1586        if self.selected_item > 0 {
1587            self.selected_item - 1
1588        } else {
1589            self.actions.len() - 1
1590        }
1591    }
1592
1593    fn next_match_index(&self) -> usize {
1594        if self.selected_item + 1 < self.actions.len() {
1595            self.selected_item + 1
1596        } else {
1597            0
1598        }
1599    }
1600
1601    pub fn visible(&self) -> bool {
1602        !self.actions.is_empty()
1603    }
1604
1605    fn origin(&self) -> ContextMenuOrigin {
1606        match &self.deployed_from {
1607            Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1608                ContextMenuOrigin::GutterIndicator(*row)
1609            }
1610            Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1611            None => ContextMenuOrigin::Cursor,
1612        }
1613    }
1614
1615    fn render(
1616        &self,
1617        _style: &EditorStyle,
1618        max_height_in_lines: u32,
1619        window: &mut Window,
1620        cx: &mut Context<Editor>,
1621    ) -> AnyElement {
1622        let actions = self.actions.clone();
1623        let selected_item = self.selected_item;
1624        let is_quick_action_bar = matches!(self.origin(), ContextMenuOrigin::QuickActionBar);
1625
1626        let list = uniform_list(
1627            "code_actions_menu",
1628            self.actions.len(),
1629            cx.processor(move |_this, range: Range<usize>, _, cx| {
1630                actions
1631                    .iter()
1632                    .skip(range.start)
1633                    .take(range.end - range.start)
1634                    .enumerate()
1635                    .map(|(ix, action)| {
1636                        let item_ix = range.start + ix;
1637                        let selected = item_ix == selected_item;
1638                        let colors = cx.theme().colors();
1639
1640                        ListItem::new(item_ix)
1641                            .inset(true)
1642                            .toggle_state(selected)
1643                            .overflow_x()
1644                            .child(
1645                                div()
1646                                    .min_w(CODE_ACTION_MENU_MIN_WIDTH)
1647                                    .max_w(CODE_ACTION_MENU_MAX_WIDTH)
1648                                    .overflow_hidden()
1649                                    .text_ellipsis()
1650                                    .when(is_quick_action_bar, |this| this.text_ui(cx))
1651                                    .when(selected, |this| this.text_color(colors.text_accent))
1652                                    .child(action.menu_label()),
1653                            )
1654                            .on_click(cx.listener(move |editor, _, window, cx| {
1655                                cx.stop_propagation();
1656                                if let Some(task) = editor.confirm_code_action(
1657                                    &ConfirmCodeAction {
1658                                        item_ix: Some(item_ix),
1659                                    },
1660                                    window,
1661                                    cx,
1662                                ) {
1663                                    task.detach_and_log_err(cx)
1664                                }
1665                            }))
1666                    })
1667                    .collect()
1668            }),
1669        )
1670        .occlude()
1671        .max_h(max_height_in_lines as f32 * window.line_height())
1672        .track_scroll(&self.scroll_handle)
1673        .with_width_from_item(
1674            self.actions
1675                .iter()
1676                .enumerate()
1677                .max_by_key(|(_, action)| match action {
1678                    CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
1679                    CodeActionsItem::CodeAction { action, .. } => {
1680                        action.lsp_action.title().chars().count()
1681                    }
1682                    CodeActionsItem::DebugScenario(scenario) => {
1683                        format!("debug: {}", scenario.label).chars().count()
1684                    }
1685                })
1686                .map(|(ix, _)| ix),
1687        )
1688        .with_sizing_behavior(ListSizingBehavior::Infer);
1689
1690        Popover::new().child(list).into_any_element()
1691    }
1692
1693    fn render_aside(
1694        &mut self,
1695        max_size: Size<Pixels>,
1696        window: &mut Window,
1697        _cx: &mut Context<Editor>,
1698    ) -> Option<AnyElement> {
1699        let Some(action) = self.actions.get(self.selected_item) else {
1700            return None;
1701        };
1702
1703        let label = action.menu_label();
1704        let text_system = window.text_system();
1705        let mut line_wrapper = text_system.line_wrapper(
1706            window.text_style().font(),
1707            window.text_style().font_size.to_pixels(window.rem_size()),
1708        );
1709        let is_truncated = line_wrapper.should_truncate_line(
1710            &label,
1711            CODE_ACTION_MENU_MAX_WIDTH,
1712            "",
1713            gpui::TruncateFrom::End,
1714        );
1715
1716        if is_truncated.is_none() {
1717            return None;
1718        }
1719
1720        Some(
1721            Popover::new()
1722                .child(
1723                    div()
1724                        .child(label)
1725                        .id("code_actions_menu_extended")
1726                        .px(MENU_ASIDE_X_PADDING / 2.)
1727                        .max_w(max_size.width)
1728                        .max_h(max_size.height)
1729                        .occlude(),
1730                )
1731                .into_any_element(),
1732        )
1733    }
1734}