Accept partial copilot suggestions (#8682)

Fixes https://github.com/zed-industries/zed/issues/8020
 
This PR adds a new shortcut cmd-right, if a copilot suggestion exists.
The suggestions is accepted word by word.
It emulates the behaviour of VS Code's Github Copilot implementation.


Release Notes:

- Added ability to accept partial copilot suggestions ([8020](https://github.com/zed-industries/zed/issues/8020))
This commit is contained in:
Jonathan 2024-03-03 17:24:48 +01:00 committed by GitHub
parent 56f0418c93
commit 08f9c3f568
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 187 additions and 4 deletions

View file

@ -135,10 +135,21 @@
"focus": true
}
],
"alt-\\": "copilot::Suggest",
"ctrl->": "assistant::QuoteSelection"
}
},
{
"context": "Editor && mode == full && copilot_suggestion",
"bindings": {
"alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion",
"ctrl->": "assistant::QuoteSelection"
"alt-right": "editor::AcceptPartialCopilotSuggestion"
}
},
{
"context": "Editor && !copilot_suggestion",
"bindings": {
"alt-\\": "copilot::Suggest"
}
},
{

View file

@ -176,10 +176,21 @@
"focus": false
}
],
"alt-\\": "copilot::Suggest",
"cmd->": "assistant::QuoteSelection"
}
},
{
"context": "Editor && mode == full && copilot_suggestion",
"bindings": {
"alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion",
"cmd->": "assistant::QuoteSelection"
"alt-right": "editor::AcceptPartialCopilotSuggestion"
}
},
{
"context": "Editor && !copilot_suggestion",
"bindings": {
"alt-\\": "copilot::Suggest"
}
},
{

View file

@ -119,6 +119,7 @@ impl_actions!(
gpui::actions!(
editor,
[
AcceptPartialCopilotSuggestion,
AddSelectionAbove,
AddSelectionBelow,
Backspace,

View file

@ -1627,6 +1627,10 @@ impl Editor {
key_context.set("extension", extension.to_string());
}
if self.has_active_copilot_suggestion(cx) {
key_context.add("copilot_suggestion");
}
key_context
}
@ -3965,6 +3969,39 @@ impl Editor {
}
}
fn accept_partial_copilot_suggestion(
&mut self,
_: &AcceptPartialCopilotSuggestion,
cx: &mut ViewContext<Self>,
) {
if self.selections.count() == 1 && self.has_active_copilot_suggestion(cx) {
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
let mut partial_suggestion = suggestion
.text
.chars()
.by_ref()
.take_while(|c| c.is_alphabetic())
.collect::<String>();
if partial_suggestion.is_empty() {
partial_suggestion = suggestion
.text
.chars()
.by_ref()
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
.collect::<String>();
}
cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None,
text: partial_suggestion.clone().into(),
});
self.insert_with_autoindent_mode(&partial_suggestion, None, cx);
self.refresh_copilot_suggestions(true, cx);
cx.notify();
}
}
}
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
if let Some(copilot) = Copilot::global(cx) {

View file

@ -7623,6 +7623,128 @@ async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContex
});
}
#[gpui::test(iterations = 10)]
async fn test_accept_partial_copilot_suggestion(
executor: BackgroundExecutor,
cx: &mut gpui::TestAppContext,
) {
// flaky
init_test(cx, |_| {});
let (copilot, copilot_lsp) = Copilot::fake(cx);
_ = cx.update(|cx| Copilot::set_global(copilot, cx));
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
// Setup the editor with a completion request.
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec![],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_copilot_suggestion(cx));
// Accepting the first word of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_copilot_suggestion(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
// Accepting next word should accept the non-word and copilot suggestion should be gone
editor.accept_partial_copilot_suggestion(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
});
// Reset the editor and check non-word and whitespace completion
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec![],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "one.123. copilot\n 456".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_copilot_suggestion(cx));
// Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_copilot_suggestion(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
"one.123. copilot\n 456\ntwo\nthree\n"
);
// Accepting next word should accept the next word and copilot suggestion should still exist
editor.accept_partial_copilot_suggestion(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
"one.123. copilot\n 456\ntwo\nthree\n"
);
// Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
editor.accept_partial_copilot_suggestion(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
"one.123. copilot\n 456\ntwo\nthree\n"
);
});
}
#[gpui::test]
async fn test_copilot_completion_invalidation(
executor: BackgroundExecutor,

View file

@ -338,6 +338,7 @@ impl EditorElement {
register_action(view, cx, Editor::display_cursor_names);
register_action(view, cx, Editor::unique_lines_case_insensitive);
register_action(view, cx, Editor::unique_lines_case_sensitive);
register_action(view, cx, Editor::accept_partial_copilot_suggestion);
}
fn register_key_listeners(