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
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -119,6 +119,7 @@ impl_actions!(
|
|||
gpui::actions!(
|
||||
editor,
|
||||
[
|
||||
AcceptPartialCopilotSuggestion,
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
Backspace,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue