diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3de58a5d9d39da0a828a2286c4f6bdf33d58ac79..45804909abb6d94517a21090ab39ec02f5ad7b05 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -471,22 +471,13 @@ } }, { - "context": "Editor && !inline_completion && showing_completions", + "context": "Editor && showing_completions", "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmCompletion", "tab": "editor::ComposeCompletion" } }, - { - "context": "Editor && inline_completion && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "tab": "editor::ComposeCompletion", - "shift-tab": "editor::AcceptInlineCompletion" - } - }, { "context": "Editor && inline_completion && !showing_completions", "use_key_equivalents": true, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index be29106089aecb43aee44dfea35fd73648969252..4d9c196e750ed9a33df240ced99e206c190f28dc 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -542,22 +542,13 @@ } }, { - "context": "Editor && !inline_completion && showing_completions", + "context": "Editor && showing_completions", "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmCompletion", "tab": "editor::ComposeCompletion" } }, - { - "context": "Editor && inline_completion && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "tab": "editor::ComposeCompletion", - "shift-tab": "editor::AcceptInlineCompletion" - } - }, { "context": "Editor && inline_completion && !showing_completions", "use_key_equivalents": true, diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 730401adc77c21607e09b0d7c8d261bced21c27b..7ef8d9ac7be2a1cef376f50f14d68bdb045863d4 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -55,6 +55,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider { "copilot" } + fn display_name() -> &'static str { + "Copilot" + } + fn is_enabled( &self, buffer: &Model, @@ -324,10 +328,15 @@ mod tests { cx.update_editor(|editor, cx| { // We want to show both: the inline completion and the completion menu assert!(editor.context_menu_visible()); + assert!(editor.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion()); + // Since we have both, the copilot suggestion is not shown inline + assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); - // Confirming a completion inserts it and hides the context menu, without showing + // Confirming a non-copilot completion inserts it and hides the context menu, without showing // the copilot suggestion afterwards. + editor.context_menu_next(&Default::default(), cx); editor .confirm_completion(&Default::default(), cx) .unwrap() @@ -338,13 +347,14 @@ mod tests { assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); - // Reset editor and test that accepting completions works + // Reset editor and only return copilot suggestions cx.set_state(indoc! {" oneˇ two three "}); cx.simulate_keystroke("."); + drop(handle_completion_request( &mut cx, indoc! {" @@ -352,7 +362,7 @@ mod tests { two three "}, - vec!["completion_a", "completion_b"], + vec![], )); handle_copilot_completion_request( &copilot_lsp, @@ -365,16 +375,15 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { - assert!(editor.context_menu_visible()); + assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); + // Since only the copilot is available, it's shown inline assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); }); // Ensure existing inline completion is interpolated when inserting again. cx.simulate_keystroke("c"); - // We still request a normal LSP completion, but we interpolate the - // existing inline completion. drop(handle_completion_request( &mut cx, indoc! {" @@ -382,13 +391,16 @@ mod tests { two three "}, - vec!["ompletion_a", "ompletion_b"], + vec!["completion_a", "completion_b"], )); executor.run_until_parked(); cx.update_editor(|editor, cx| { - assert!(!editor.context_menu_visible()); + // Since we have an LSP completion too, the inline completion is + // shown in the menu now + assert!(editor.context_menu_visible()); + assert!(editor.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion()); - assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -404,6 +416,14 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { + assert!(editor.context_menu_visible()); + assert!(editor.has_active_inline_completion()); + assert!(editor.context_menu_contains_inline_completion()); + assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); + assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); + + // Canceling should first hide the menu and make Copilot suggestion visible. + editor.cancel(&Default::default(), cx); assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); @@ -908,8 +928,8 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); + assert!(editor.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion(),); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntwo.\nthree\n"); }); } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 065f5897af253f8eb51c9352286d06c804d81aa3..97ed06451fccf1c4dc67fbf5967526d0be17ab64 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -28,6 +28,7 @@ use crate::{ 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), @@ -141,7 +142,7 @@ pub struct CompletionsMenu { pub buffer: Model, pub completions: Rc>>, match_candidates: Rc<[StringMatchCandidate]>, - pub matches: Rc<[StringMatch]>, + pub entries: Rc<[CompletionEntry]>, pub selected_item: usize, scroll_handle: UniformListScrollHandle, resolve_completions: bool, @@ -149,6 +150,12 @@ pub struct CompletionsMenu { show_completion_documentation: bool, } +#[derive(Clone, Debug)] +pub(crate) enum CompletionEntry { + Match(StringMatch), + InlineCompletionHint(InlineCompletionMenuHint), +} + impl CompletionsMenu { pub fn new( id: CompletionId, @@ -173,7 +180,7 @@ impl CompletionsMenu { show_completion_documentation, completions: RefCell::new(completions).into(), match_candidates, - matches: Vec::new().into(), + entries: Vec::new().into(), selected_item: 0, scroll_handle: UniformListScrollHandle::new(), resolve_completions: true, @@ -210,14 +217,16 @@ impl CompletionsMenu { .enumerate() .map(|(id, completion)| StringMatchCandidate::new(id, &completion)) .collect(); - let matches = choices + let entries = choices .iter() .enumerate() - .map(|(id, completion)| StringMatch { - candidate_id: id, - score: 1., - positions: vec![], - string: completion.clone(), + .map(|(id, completion)| { + CompletionEntry::Match(StringMatch { + candidate_id: id, + score: 1., + positions: vec![], + string: completion.clone(), + }) }) .collect(); Self { @@ -227,7 +236,7 @@ impl CompletionsMenu { buffer, completions: RefCell::new(completions).into(), match_candidates, - matches, + entries, selected_item: 0, scroll_handle: UniformListScrollHandle::new(), resolve_completions: false, @@ -256,7 +265,7 @@ impl CompletionsMenu { if self.selected_item > 0 { self.selected_item -= 1; } else { - self.selected_item = self.matches.len() - 1; + self.selected_item = self.entries.len() - 1; } self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); @@ -269,7 +278,7 @@ impl CompletionsMenu { provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { - if self.selected_item + 1 < self.matches.len() { + if self.selected_item + 1 < self.entries.len() { self.selected_item += 1; } else { self.selected_item = 0; @@ -285,13 +294,33 @@ impl CompletionsMenu { provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { - self.selected_item = self.matches.len() - 1; + 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>, @@ -304,24 +333,29 @@ impl CompletionsMenu { 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(); + 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(); } - }) - .detach(); + CompletionEntry::InlineCompletionHint { .. } => {} + } } - fn visible(&self) -> bool { - !self.matches.is_empty() + pub fn visible(&self) -> bool { + !self.entries.is_empty() } fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin { @@ -340,21 +374,27 @@ impl CompletionsMenu { let completions = self.completions.borrow_mut(); let show_completion_documentation = self.show_completion_documentation; let widest_completion_ix = self - .matches + .entries .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(Documentation::SingleLine(text)) = documentation { - if show_completion_documentation { - len += text.chars().count(); + .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 + len + } + CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint { + provider_name, + .. + }) => provider_name.len(), }) .map(|(ix, _)| ix); @@ -362,24 +402,36 @@ impl CompletionsMenu { let style = style.clone(); let multiline_docs = if show_completion_documentation { - let mat = &self.matches[selected_item]; - 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, + match &self.entries[selected_item] { + CompletionEntry::Match(mat) => 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_md() + .bg(cx.theme().colors().editor_background) + .child( + gpui::StyledText::new(text.clone()) + .with_highlights(&style.text, highlights.clone()), + ), + InlineCompletionText::Move(text) => div().child(text.clone()), + }), } } else { None @@ -409,7 +461,7 @@ impl CompletionsMenu { drop(completions); let completions = self.completions.clone(); - let matches = self.matches.clone(); + let matches = self.entries.clone(); let list = uniform_list( cx.view().clone(), "completions", @@ -423,82 +475,111 @@ impl CompletionsMenu { .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 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) { - 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()); + match mat { + CompletionEntry::Match(mat) => { + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; - 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::