963 lines · 34.3 KB · Rust
RawDownload
use std::{    cell::Cell,    cmp::{min, Reverse},    ops::Range,    sync::Arc,}; use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,    Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText,    UniformListScrollHandle, ViewContext, WeakView,};use language::Buffer;use language::{CodeLabel, Documentation};use lsp::LanguageServerId;use multi_buffer::{Anchor, ExcerptId};use ordered_float::OrderedFloat;use parking_lot::{Mutex, RwLock};use project::{CodeAction, Completion, TaskSourceKind};use std::iter;use task::ResolvedTask;use ui::{    h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,    Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,    StatefulInteractiveElement as _, Styled, StyledExt as _,};use util::ResultExt as _;use workspace::Workspace; use crate::{    actions::{ConfirmCodeAction, ConfirmCompletion},    display_map::DisplayPoint,    render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,    CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,}; pub enum CodeContextMenu {    Completions(CompletionsMenu),    CodeActions(CodeActionsMenu),} impl CodeContextMenu {    pub fn select_first(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<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 ViewContext<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 ViewContext<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 ViewContext<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 render(        &self,        cursor_position: DisplayPoint,        style: &EditorStyle,        max_height: Pixels,        workspace: Option<WeakView<Workspace>>,        cx: &mut ViewContext<Editor>,    ) -> (ContextMenuOrigin, AnyElement) {        match self {            CodeContextMenu::Completions(menu) => (                ContextMenuOrigin::EditorPoint(cursor_position),                menu.render(style, max_height, workspace, cx),            ),            CodeContextMenu::CodeActions(menu) => {                menu.render(cursor_position, style, max_height, cx)            }        }    }} pub enum ContextMenuOrigin {    EditorPoint(DisplayPoint),    GutterIndicator(DisplayRow),} #[derive(Clone, Debug)]pub struct CompletionsMenu {    pub id: CompletionId,    sort_completions: bool,    pub initial_position: Anchor,    pub buffer: Model<Buffer>,    pub completions: Arc<RwLock<Box<[Completion]>>>,    match_candidates: Arc<[StringMatchCandidate]>,    pub matches: Arc<[StringMatch]>,    pub selected_item: usize,    scroll_handle: UniformListScrollHandle,    resolve_completions: bool,    pub aside_was_displayed: Cell<bool>,    show_completion_documentation: bool,    last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,} impl CompletionsMenu {    pub fn new(        id: CompletionId,        sort_completions: bool,        show_completion_documentation: bool,        initial_position: Anchor,        buffer: Model<Buffer>,        completions: Box<[Completion]>,        aside_was_displayed: bool,    ) -> Self {        let match_candidates = completions            .iter()            .enumerate()            .map(|(id, completion)| {                StringMatchCandidate::new(                    id,                    completion.label.text[completion.label.filter_range.clone()].into(),                )            })            .collect();         Self {            id,            sort_completions,            initial_position,            buffer,            completions: Arc::new(RwLock::new(completions)),            match_candidates,            matches: Vec::new().into(),            selected_item: 0,            scroll_handle: UniformListScrollHandle::new(),            resolve_completions: true,            aside_was_displayed: Cell::new(aside_was_displayed),            show_completion_documentation,            last_rendered_range: Arc::new(Mutex::new(None)),        }    }     pub fn new_snippet_choices(        id: CompletionId,        sort_completions: bool,        choices: &Vec<String>,        selection: Range<Anchor>,        buffer: Model<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,            })            .collect();         let match_candidates = choices            .iter()            .enumerate()            .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))            .collect();        let matches = choices            .iter()            .enumerate()            .map(|(id, completion)| StringMatch {                candidate_id: id,                score: 1.,                positions: vec![],                string: completion.clone(),            })            .collect();        Self {            id,            sort_completions,            initial_position: selection.start,            buffer,            completions: Arc::new(RwLock::new(completions)),            match_candidates,            matches,            selected_item: 0,            scroll_handle: UniformListScrollHandle::new(),            resolve_completions: false,            aside_was_displayed: Cell::new(false),            show_completion_documentation: false,            last_rendered_range: Arc::new(Mutex::new(None)),        }    }     fn select_first(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.update_selection_index(0, provider, cx);    }     fn select_prev(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.update_selection_index(self.prev_match_index(), provider, cx);    }     fn select_next(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.update_selection_index(self.next_match_index(), provider, cx);    }     fn select_last(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.update_selection_index(self.matches.len() - 1, provider, cx);    }     fn update_selection_index(        &mut self,        match_index: usize,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<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.matches.len() - 1        }    }     fn next_match_index(&self) -> usize {        if self.selected_item + 1 < self.matches.len() {            self.selected_item + 1        } else {            0        }    }     pub fn resolve_visible_completions(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<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.lock().clone();        let visible_count = last_rendered_range            .clone()            .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());        let matches_range = if self.selected_item == 0 {            0..min(visible_count, self.matches.len())        } else if self.selected_item == self.matches.len() - 1 {            self.matches.len().saturating_sub(visible_count)..self.matches.len()        } else {            last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)        };         // 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 matches_indices = util::iterate_expanded_and_wrapped_usize_range(            matches_range.clone(),            EXTRA_TO_RESOLVE,            EXTRA_TO_RESOLVE,            self.matches.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 candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);        let candidate_ids = match self.completions.try_read() {            None => candidate_ids.collect::<Vec<usize>>(),            Some(completions) => candidate_ids                .filter(|i| completions[*i].documentation.is_none())                .collect::<Vec<usize>>(),        };         // 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 = self.matches[self.selected_item].candidate_id;        let candidate_ids = iter::once(selected_candidate_id)            .chain(                candidate_ids                    .into_iter()                    .filter(|id| *id != selected_candidate_id),            )            .collect::<Vec<usize>>();         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();    }     fn visible(&self) -> bool {        !self.matches.is_empty()    }     fn render(        &self,        style: &EditorStyle,        max_height: Pixels,        workspace: Option<WeakView<Workspace>>,        cx: &mut ViewContext<Editor>,    ) -> AnyElement {        let show_completion_documentation = self.show_completion_documentation;        let widest_completion_ix = self            .matches            .iter()            .enumerate()            .max_by_key(|(_, mat)| {                let completions = self.completions.read();                let completion = &completions[mat.candidate_id];                let documentation = &completion.documentation;                 let mut len = completion.label.text.chars().count();                if let Some(Documentation::SingleLine(text)) = documentation {                    if show_completion_documentation {                        len += text.chars().count();                    }                }                 len            })            .map(|(ix, _)| ix);         let completions = self.completions.clone();        let matches = self.matches.clone();        let selected_item = self.selected_item;        let style = style.clone();         let multiline_docs = if show_completion_documentation {            let mat = &self.matches[selected_item];            match &self.completions.read()[mat.candidate_id].documentation {                Some(Documentation::MultiLinePlainText(text)) => {                    Some(div().child(SharedString::from(text.clone())))                }                Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {                    Some(div().child(render_parsed_markdown(                        "completions_markdown",                        parsed,                        &style,                        workspace,                        cx,                    )))                }                Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {                    Some(div().child("No documentation"))                }                _ => None,            }        } else {            None        };         let aside_contents = if let Some(multiline_docs) = multiline_docs {            Some(multiline_docs)        } else if self.aside_was_displayed.get() {            Some(div().child("Fetching documentation..."))        } else {            None        };        self.aside_was_displayed.set(aside_contents.is_some());         let aside_contents = aside_contents.map(|div| {            div.id("multiline_docs")                .max_h(max_height)                .flex_1()                .px_1p5()                .py_1()                .min_w(px(260.))                .max_w(px(640.))                .w(px(500.))                .overflow_y_scroll()                .occlude()        });         let last_rendered_range = self.last_rendered_range.clone();         let list = uniform_list(            cx.view().clone(),            "completions",            matches.len(),            move |_editor, range, cx| {                last_rendered_range.lock().replace(range.clone());                let start_ix = range.start;                let completions_guard = completions.read();                 matches[range]                    .iter()                    .enumerate()                    .map(|(ix, mat)| {                        let item_ix = start_ix + ix;                        let candidate_id = mat.candidate_id;                        let completion = &completions_guard[candidate_id];                         let documentation = if show_completion_documentation {                            &completion.documentation                        } else {                            &None                        };                         let highlights = gpui::combine_highlights(                            mat.ranges().map(|range| (range, FontWeight::BOLD.into())),

Lines 1–500 of 963.