1041 lines · 37.3 KB · Rust
RawDownload
use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    div, px, uniform_list, AnyElement, BackgroundExecutor, Div, Entity, Focusable, FontWeight,    ListSizingBehavior, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,    UniformListScrollHandle,};use language::Buffer;use language::CodeLabel;use lsp::LanguageServerId;use markdown::Markdown;use multi_buffer::{Anchor, ExcerptId};use ordered_float::OrderedFloat;use project::lsp_store::CompletionDocumentation;use project::{CodeAction, Completion, TaskSourceKind}; use std::{    cell::RefCell,    cmp::{min, Reverse},    iter,    ops::Range,    rc::Rc,};use task::ResolvedTask;use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};use util::ResultExt; use crate::hover_popover::{hover_markdown_style, open_markdown_url};use crate::{    actions::{ConfirmCodeAction, ConfirmCompletion},    split_words, styled_runs_for_code_label, CodeActionProvider, CompletionId, CompletionProvider,    DisplayRow, Editor, EditorStyle, ResolvedTasks,}; pub const MENU_GAP: Pixels = px(4.);pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.); pub enum CodeContextMenu {    Completions(CompletionsMenu),    CodeActions(CodeActionsMenu),} impl CodeContextMenu {    pub fn select_first(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) -> bool {        if self.visible() {            match self {                CodeContextMenu::Completions(menu) => menu.select_first(provider, cx),                CodeContextMenu::CodeActions(menu) => menu.select_first(cx),            }            true        } else {            false        }    }     pub fn select_prev(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) -> bool {        if self.visible() {            match self {                CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx),                CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),            }            true        } else {            false        }    }     pub fn select_next(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) -> bool {        if self.visible() {            match self {                CodeContextMenu::Completions(menu) => menu.select_next(provider, cx),                CodeContextMenu::CodeActions(menu) => menu.select_next(cx),            }            true        } else {            false        }    }     pub fn select_last(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) -> bool {        if self.visible() {            match self {                CodeContextMenu::Completions(menu) => menu.select_last(provider, cx),                CodeContextMenu::CodeActions(menu) => menu.select_last(cx),            }            true        } else {            false        }    }     pub fn visible(&self) -> bool {        match self {            CodeContextMenu::Completions(menu) => menu.visible(),            CodeContextMenu::CodeActions(menu) => menu.visible(),        }    }     pub fn origin(&self) -> ContextMenuOrigin {        match self {            CodeContextMenu::Completions(menu) => menu.origin(),            CodeContextMenu::CodeActions(menu) => menu.origin(),        }    }     pub fn render(        &self,        style: &EditorStyle,        max_height_in_lines: u32,        y_flipped: bool,        window: &mut Window,        cx: &mut Context<Editor>,    ) -> AnyElement {        match self {            CodeContextMenu::Completions(menu) => {                menu.render(style, max_height_in_lines, y_flipped, window, cx)            }            CodeContextMenu::CodeActions(menu) => {                menu.render(style, max_height_in_lines, y_flipped, window, cx)            }        }    }     pub fn render_aside(        &mut self,        editor: &Editor,        max_size: Size<Pixels>,        window: &mut Window,        cx: &mut Context<Editor>,    ) -> Option<AnyElement> {        match self {            CodeContextMenu::Completions(menu) => menu.render_aside(editor, max_size, window, cx),            CodeContextMenu::CodeActions(_) => None,        }    }     pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {        match self {            CodeContextMenu::Completions(completions_menu) => completions_menu                .markdown_element                .as_ref()                .is_some_and(|markdown| markdown.focus_handle(cx).contains_focused(window, cx)),            CodeContextMenu::CodeActions(_) => false,        }    }} pub enum ContextMenuOrigin {    Cursor,    GutterIndicator(DisplayRow),} #[derive(Clone, Debug)]pub struct CompletionsMenu {    pub id: CompletionId,    sort_completions: bool,    pub initial_position: Anchor,    pub buffer: Entity<Buffer>,    pub completions: Rc<RefCell<Box<[Completion]>>>,    match_candidates: Rc<[StringMatchCandidate]>,    pub entries: Rc<RefCell<Vec<StringMatch>>>,    pub selected_item: usize,    scroll_handle: UniformListScrollHandle,    resolve_completions: bool,    show_completion_documentation: bool,    last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,    markdown_element: Option<Entity<Markdown>>,} impl CompletionsMenu {    pub fn new(        id: CompletionId,        sort_completions: bool,        show_completion_documentation: bool,        initial_position: Anchor,        buffer: Entity<Buffer>,        completions: Box<[Completion]>,    ) -> Self {        let match_candidates = completions            .iter()            .enumerate()            .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))            .collect();         Self {            id,            sort_completions,            initial_position,            buffer,            show_completion_documentation,            completions: RefCell::new(completions).into(),            match_candidates,            entries: RefCell::new(Vec::new()).into(),            selected_item: 0,            scroll_handle: UniformListScrollHandle::new(),            resolve_completions: true,            last_rendered_range: RefCell::new(None).into(),            markdown_element: None,        }    }     pub fn new_snippet_choices(        id: CompletionId,        sort_completions: bool,        choices: &Vec<String>,        selection: Range<Anchor>,        buffer: Entity<Buffer>,    ) -> Self {        let completions = choices            .iter()            .map(|choice| Completion {                old_range: selection.start.text_anchor..selection.end.text_anchor,                new_text: choice.to_string(),                label: CodeLabel {                    text: choice.to_string(),                    runs: Default::default(),                    filter_range: Default::default(),                },                server_id: LanguageServerId(usize::MAX),                documentation: None,                lsp_completion: Default::default(),                confirm: None,                resolved: true,            })            .collect();         let match_candidates = choices            .iter()            .enumerate()            .map(|(id, completion)| StringMatchCandidate::new(id, &completion))            .collect();        let entries = choices            .iter()            .enumerate()            .map(|(id, completion)| StringMatch {                candidate_id: id,                score: 1.,                positions: vec![],                string: completion.clone(),            })            .collect::<Vec<_>>();        Self {            id,            sort_completions,            initial_position: selection.start,            buffer,            completions: RefCell::new(completions).into(),            match_candidates,            entries: RefCell::new(entries).into(),            selected_item: 0,            scroll_handle: UniformListScrollHandle::new(),            resolve_completions: false,            show_completion_documentation: false,            last_rendered_range: RefCell::new(None).into(),            markdown_element: None,        }    }     fn select_first(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) {        let index = if self.scroll_handle.y_flipped() {            self.entries.borrow().len() - 1        } else {            0        };        self.update_selection_index(index, provider, cx);    }     fn select_last(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {        let index = if self.scroll_handle.y_flipped() {            0        } else {            self.entries.borrow().len() - 1        };        self.update_selection_index(index, provider, cx);    }     fn select_prev(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {        let index = if self.scroll_handle.y_flipped() {            self.next_match_index()        } else {            self.prev_match_index()        };        self.update_selection_index(index, provider, cx);    }     fn select_next(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context<Editor>) {        let index = if self.scroll_handle.y_flipped() {            self.prev_match_index()        } else {            self.next_match_index()        };        self.update_selection_index(index, provider, cx);    }     fn update_selection_index(        &mut self,        match_index: usize,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) {        if self.selected_item != match_index {            self.selected_item = match_index;            self.scroll_handle                .scroll_to_item(self.selected_item, ScrollStrategy::Top);            self.resolve_visible_completions(provider, cx);            cx.notify();        }    }     fn prev_match_index(&self) -> usize {        if self.selected_item > 0 {            self.selected_item - 1        } else {            self.entries.borrow().len() - 1        }    }     fn next_match_index(&self) -> usize {        if self.selected_item + 1 < self.entries.borrow().len() {            self.selected_item + 1        } else {            0        }    }     pub fn resolve_visible_completions(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut Context<Editor>,    ) {        if !self.resolve_completions {            return;        }        let Some(provider) = provider else {            return;        };         // Attempt to resolve completions for every item that will be displayed. This matters        // because single line documentation may be displayed inline with the completion.        //        // When navigating to the very beginning or end of completions, `last_rendered_range` may        // have no overlap with the completions that will be displayed, so instead use a range based        // on the last rendered count.        const APPROXIMATE_VISIBLE_COUNT: usize = 12;        let last_rendered_range = self.last_rendered_range.borrow().clone();        let visible_count = last_rendered_range            .clone()            .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());        let entries = self.entries.borrow();        let entry_range = if self.selected_item == 0 {            0..min(visible_count, entries.len())        } else if self.selected_item == entries.len() - 1 {            entries.len().saturating_sub(visible_count)..entries.len()        } else {            last_rendered_range.map_or(0..0, |range| {                min(range.start, entries.len())..min(range.end, entries.len())            })        };         // Expand the range to resolve more completions than are predicted to be visible, to reduce        // jank on navigation.        const EXTRA_TO_RESOLVE: usize = 4;        let entry_indices = util::iterate_expanded_and_wrapped_usize_range(            entry_range.clone(),            EXTRA_TO_RESOLVE,            EXTRA_TO_RESOLVE,            entries.len(),        );         // Avoid work by sometimes filtering out completions that already have documentation.        // This filtering doesn't happen if the completions are currently being updated.        let completions = self.completions.borrow();        let candidate_ids = entry_indices            .map(|i| entries[i].candidate_id)            .filter(|i| completions[*i].documentation.is_none());         // Current selection is always resolved even if it already has documentation, to handle        // out-of-spec language servers that return more results later.        let selected_candidate_id = entries[self.selected_item].candidate_id;        let candidate_ids = iter::once(selected_candidate_id)            .chain(candidate_ids.filter(|id| *id != selected_candidate_id))            .collect::<Vec<usize>>();        drop(entries);         if candidate_ids.is_empty() {            return;        }         let resolve_task = provider.resolve_completions(            self.buffer.clone(),            candidate_ids,            self.completions.clone(),            cx,        );         cx.spawn(move |editor, mut cx| async move {            if let Some(true) = resolve_task.await.log_err() {                editor.update(&mut cx, |_, cx| cx.notify()).ok();            }        })        .detach();    }     pub fn visible(&self) -> bool {        !self.entries.borrow().is_empty()    }     fn origin(&self) -> ContextMenuOrigin {        ContextMenuOrigin::Cursor    }     fn render(        &self,        style: &EditorStyle,        max_height_in_lines: u32,        y_flipped: bool,        window: &mut Window,        cx: &mut Context<Editor>,    ) -> AnyElement {        let completions = self.completions.borrow_mut();        let show_completion_documentation = self.show_completion_documentation;        let widest_completion_ix = self            .entries            .borrow()            .iter()            .enumerate()            .max_by_key(|(_, mat)| {                let completion = &completions[mat.candidate_id];                let documentation = &completion.documentation;                 let mut len = completion.label.text.chars().count();                if let Some(CompletionDocumentation::SingleLine(text)) = documentation {                    if show_completion_documentation {                        len += text.chars().count();                    }                }                 len            })            .map(|(ix, _)| ix);        drop(completions);         let selected_item = self.selected_item;        let completions = self.completions.clone();        let entries = self.entries.clone();        let last_rendered_range = self.last_rendered_range.clone();        let style = style.clone();        let list = uniform_list(            cx.entity().clone(),            "completions",            self.entries.borrow().len(),            move |_editor, range, _window, cx| {                last_rendered_range.borrow_mut().replace(range.clone());                let start_ix = range.start;                let completions_guard = completions.borrow_mut();                 entries.borrow()[range]                    .iter()                    .enumerate()                    .map(|(ix, mat)| {                        let item_ix = start_ix + ix;                        let completion = &completions_guard[mat.candidate_id];                        let documentation = if show_completion_documentation {                            &completion.documentation                        } else {                            &None                        };                         let filter_start = completion.label.filter_range.start;                        let highlights = gpui::combine_highlights(                            mat.ranges().map(|range| {                                (                                    filter_start + range.start..filter_start + range.end,                                    FontWeight::BOLD.into(),                                )                            }),                            styled_runs_for_code_label(&completion.label, &style.syntax).map(                                |(range, mut highlight)| {                                    // Ignore font weight for syntax highlighting, as we'll use it

Lines 1–500 of 1041.