code_context_menus.rs

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