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, ) -> 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, ) -> 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, ) -> 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, ) -> 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>, cx: &mut ViewContext, ) -> (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, pub completions: Arc>>, match_candidates: Arc<[StringMatchCandidate]>, pub matches: Arc<[StringMatch]>, pub selected_item: usize, scroll_handle: UniformListScrollHandle, resolve_completions: bool, pub aside_was_displayed: Cell, show_completion_documentation: bool, } impl CompletionsMenu { pub fn new( id: CompletionId, sort_completions: bool, show_completion_documentation: bool, initial_position: Anchor, buffer: Model, 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, selection: Range, buffer: Model, ) -> 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, ) { 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, ) { 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, ) { 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, ) { 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, ) { 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>, cx: &mut ViewContext, ) -> 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::
(color_swatch) .child(h_flex().overflow_hidden().child(completion_label)) .end_slot::