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:
parent
56f0418c93
commit
08f9c3f568
6 changed files with 187 additions and 4 deletions
|
@ -135,10 +135,21 @@
|
||||||
"focus": true
|
"focus": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"alt-\\": "copilot::Suggest",
|
"ctrl->": "assistant::QuoteSelection"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && mode == full && copilot_suggestion",
|
||||||
|
"bindings": {
|
||||||
"alt-]": "copilot::NextSuggestion",
|
"alt-]": "copilot::NextSuggestion",
|
||||||
"alt-[": "copilot::PreviousSuggestion",
|
"alt-[": "copilot::PreviousSuggestion",
|
||||||
"ctrl->": "assistant::QuoteSelection"
|
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && !copilot_suggestion",
|
||||||
|
"bindings": {
|
||||||
|
"alt-\\": "copilot::Suggest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -176,10 +176,21 @@
|
||||||
"focus": false
|
"focus": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"alt-\\": "copilot::Suggest",
|
"cmd->": "assistant::QuoteSelection"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && mode == full && copilot_suggestion",
|
||||||
|
"bindings": {
|
||||||
"alt-]": "copilot::NextSuggestion",
|
"alt-]": "copilot::NextSuggestion",
|
||||||
"alt-[": "copilot::PreviousSuggestion",
|
"alt-[": "copilot::PreviousSuggestion",
|
||||||
"cmd->": "assistant::QuoteSelection"
|
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && !copilot_suggestion",
|
||||||
|
"bindings": {
|
||||||
|
"alt-\\": "copilot::Suggest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -119,6 +119,7 @@ impl_actions!(
|
||||||
gpui::actions!(
|
gpui::actions!(
|
||||||
editor,
|
editor,
|
||||||
[
|
[
|
||||||
|
AcceptPartialCopilotSuggestion,
|
||||||
AddSelectionAbove,
|
AddSelectionAbove,
|
||||||
AddSelectionBelow,
|
AddSelectionBelow,
|
||||||
Backspace,
|
Backspace,
|
||||||
|
|
|
@ -1627,6 +1627,10 @@ impl Editor {
|
||||||
key_context.set("extension", extension.to_string());
|
key_context.set("extension", extension.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
|
key_context.add("copilot_suggestion");
|
||||||
|
}
|
||||||
|
|
||||||
key_context
|
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 {
|
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
||||||
if let Some(copilot) = Copilot::global(cx) {
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
|
|
@ -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]
|
#[gpui::test]
|
||||||
async fn test_copilot_completion_invalidation(
|
async fn test_copilot_completion_invalidation(
|
||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
|
|
|
@ -338,6 +338,7 @@ impl EditorElement {
|
||||||
register_action(view, cx, Editor::display_cursor_names);
|
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_insensitive);
|
||||||
register_action(view, cx, Editor::unique_lines_case_sensitive);
|
register_action(view, cx, Editor::unique_lines_case_sensitive);
|
||||||
|
register_action(view, cx, Editor::accept_partial_copilot_suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_key_listeners(
|
fn register_key_listeners(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue