977 lines · 36.4 KB · Rust
RawDownload
use std::cell::RefCell;use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc}; use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,    Model, 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 project::{CodeAction, Completion, TaskSourceKind};use task::ResolvedTask;use ui::{prelude::*, Color, IntoElement, ListItem, Popover, Styled};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,};use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText}; 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 origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {        match self {            CodeContextMenu::Completions(menu) => menu.origin(cursor_position),            CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),        }    }    pub fn render(        &self,        style: &EditorStyle,        max_height_in_lines: u32,        workspace: Option<WeakView<Workspace>>,        cx: &mut ViewContext<Editor>,    ) -> AnyElement {        match self {            CodeContextMenu::Completions(menu) => {                menu.render(style, max_height_in_lines, workspace, cx)            }            CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, 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: Rc<RefCell<Box<[Completion]>>>,    match_candidates: Rc<[StringMatchCandidate]>,    pub entries: Rc<[CompletionEntry]>,    pub selected_item: usize,    scroll_handle: UniformListScrollHandle,    resolve_completions: bool,    pub aside_was_displayed: Cell<bool>,    show_completion_documentation: bool,} #[derive(Clone, Debug)]pub(crate) enum CompletionEntry {    Match(StringMatch),    InlineCompletionHint(InlineCompletionMenuHint),} 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.filter_text()))            .collect();         Self {            id,            sort_completions,            initial_position,            buffer,            show_completion_documentation,            completions: RefCell::new(completions).into(),            match_candidates,            entries: 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))            .collect();        let entries = choices            .iter()            .enumerate()            .map(|(id, completion)| {                CompletionEntry::Match(StringMatch {                    candidate_id: id,                    score: 1.,                    positions: vec![],                    string: completion.clone(),                })            })            .collect();        Self {            id,            sort_completions,            initial_position: selection.start,            buffer,            completions: RefCell::new(completions).into(),            match_candidates,            entries,            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.entries.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.entries.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.entries.len() - 1;        self.scroll_handle            .scroll_to_item(self.selected_item, ScrollStrategy::Top);        self.resolve_selected_completion(provider, cx);        cx.notify();    }     pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {        let hint = CompletionEntry::InlineCompletionHint(hint);         self.entries = match self.entries.first() {            Some(CompletionEntry::InlineCompletionHint { .. }) => {                let mut entries = Vec::from(&*self.entries);                entries[0] = hint;                entries            }            _ => {                let mut entries = Vec::with_capacity(self.entries.len() + 1);                entries.push(hint);                entries.extend_from_slice(&self.entries);                entries            }        }        .into();        self.selected_item = 0;    }     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;        };         match &self.entries[self.selected_item] {            CompletionEntry::Match(entry) => {                let completion_index = entry.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();            }            CompletionEntry::InlineCompletionHint { .. } => {}        }    }     pub fn visible(&self) -> bool {        !self.entries.is_empty()    }     fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {        ContextMenuOrigin::EditorPoint(cursor_position)    }     fn render(        &self,        style: &EditorStyle,        max_height_in_lines: u32,        workspace: Option<WeakView<Workspace>>,        cx: &mut ViewContext<Editor>,    ) -> AnyElement {        let max_height = max_height_in_lines as f32 * cx.line_height();         let completions = self.completions.borrow_mut();        let show_completion_documentation = self.show_completion_documentation;        let widest_completion_ix = self            .entries            .iter()            .enumerate()            .max_by_key(|(_, mat)| match mat {                CompletionEntry::Match(mat) => {                    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                }                CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {                    provider_name,                    ..                }) => provider_name.len(),            })            .map(|(ix, _)| ix);         let selected_item = self.selected_item;        let style = style.clone();         let multiline_docs = match &self.entries[selected_item] {            CompletionEntry::Match(mat) if show_completion_documentation => {                match &completions[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,                }            }            CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {                InlineCompletionText::Edit { text, highlights } => div()                    .my_1()                    .rounded(px(6.))                    .bg(cx.theme().colors().editor_background)                    .border_1()                    .border_color(cx.theme().colors().border_variant)                    .child(                        gpui::StyledText::new(text.clone())                            .with_highlights(&style.text, highlights.clone()),                    ),                InlineCompletionText::Move(text) => div().child(text.clone()),            }),            _ => None,        };         let aside_contents = if let Some(multiline_docs) = multiline_docs {            Some(multiline_docs)        } else if show_completion_documentation && 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()                .max_w(px(640.))                .w(px(450.))                .overflow_y_scroll()                .occlude()        });         drop(completions);        let completions = self.completions.clone();        let matches = self.entries.clone();        let list = uniform_list(            cx.view().clone(),            "completions",            matches.len(),            move |_editor, range, cx| {                let start_ix = range.start;                let completions_guard = completions.borrow_mut();                 matches[range]                    .iter()                    .enumerate()                    .map(|(ix, mat)| {                        let item_ix = start_ix + ix;                        match mat {                            CompletionEntry::Match(mat) => {                                let candidate_id = mat.candidate_id;                                let completion = &completions_guard[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                                            // for fuzzy matches.                                            highlight.font_weight = None;                                             if completion.lsp_completion.deprecated.unwrap_or(false)                                            {

Lines 1–500 of 977.