895 lines · 31.5 KB · Rust
RawDownload
use std::{cell::Cell, cmp::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::RwLock;use project::{CodeAction, Completion, TaskSourceKind};use task::ResolvedTask;use ui::{    h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,    Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,    StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable 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,} 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,            show_completion_documentation,            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),        }    }     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,        }    }     fn select_first(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.selected_item = 0;        self.scroll_handle            .scroll_to_item(self.selected_item, ScrollStrategy::Top);        self.resolve_selected_completion(provider, cx);        cx.notify();    }     fn select_prev(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        if self.selected_item > 0 {            self.selected_item -= 1;        } else {            self.selected_item = self.matches.len() - 1;        }        self.scroll_handle            .scroll_to_item(self.selected_item, ScrollStrategy::Top);        self.resolve_selected_completion(provider, cx);        cx.notify();    }     fn select_next(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        if self.selected_item + 1 < self.matches.len() {            self.selected_item += 1;        } else {            self.selected_item = 0;        }        self.scroll_handle            .scroll_to_item(self.selected_item, ScrollStrategy::Top);        self.resolve_selected_completion(provider, cx);        cx.notify();    }     fn select_last(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        self.selected_item = self.matches.len() - 1;        self.scroll_handle            .scroll_to_item(self.selected_item, ScrollStrategy::Top);        self.resolve_selected_completion(provider, cx);        cx.notify();    }     pub fn resolve_selected_completion(        &mut self,        provider: Option<&dyn CompletionProvider>,        cx: &mut ViewContext<Editor>,    ) {        if !self.resolve_completions {            return;        }        let Some(provider) = provider else {            return;        };         let completion_index = self.matches[self.selected_item].candidate_id;        let resolve_task = provider.resolve_completions(            self.buffer.clone(),            vec![completion_index],            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 list = uniform_list(            cx.view().clone(),            "completions",            matches.len(),            move |_editor, range, cx| {                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())),                            styled_runs_for_code_label(&completion.label, &style.syntax).map(                                |(range, mut highlight)| {                                    // Ignore font weight for syntax highlighting, as we'll use it                                    // for fuzzy matches.                                    highlight.font_weight = None;                                     if completion.lsp_completion.deprecated.unwrap_or(false) {                                        highlight.strikethrough = Some(StrikethroughStyle {                                            thickness: 1.0.into(),                                            ..Default::default()                                        });                                        highlight.color = Some(cx.theme().colors().text_muted);                                    }                                     (range, highlight)                                },                            ),                        );                        let completion_label = StyledText::new(completion.label.text.clone())                            .with_highlights(&style.text, highlights);                        let documentation_label =                            if let Some(Documentation::SingleLine(text)) = documentation {                                if text.trim().is_empty() {                                    None                                } else {                                    Some(                                        Label::new(text.clone())                                            .ml_4()                                            .size(LabelSize::Small)                                            .color(Color::Muted),                                    )                                }                            } else {                                None                            };                         let color_swatch = completion                            .color()                            .map(|color| div().size_4().bg(color).rounded_sm());                         div().min_w(px(220.)).max_w(px(540.)).child(                            ListItem::new(mat.candidate_id)                                .inset(true)                                .toggle_state(item_ix == selected_item)                                .on_click(cx.listener(move |editor, _event, cx| {                                    cx.stop_propagation();                                    if let Some(task) = editor.confirm_completion(                                        &ConfirmCompletion {                                            item_ix: Some(item_ix),                                        },                                        cx,                                    ) {                                        task.detach_and_log_err(cx)                                    }                                }))                                .start_slot::<Div>(color_swatch)                                .child(h_flex().overflow_hidden().child(completion_label))                                .end_slot::<Label>(documentation_label),                        )                    })                    .collect()            },        )        .occlude()        .max_h(max_height)        .track_scroll(self.scroll_handle.clone())        .with_width_from_item(widest_completion_ix)        .with_sizing_behavior(ListSizingBehavior::Infer);

Lines 1–500 of 895.