diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 865d0648848499d8a308bcc4682298c2c43233e4..459f7e04db7d3d592d83de643f61566dc1757c15 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -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" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 957820e586100a12bc88531d4da6aca9727a95b8..dcf08178150422fd4e2603b2dade7f3ddf4caf4b 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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" } }, { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ae7cd99c075fbeb4ef79a3add6c3189c397353cc..d0bbab0335967de321c2a11cc5173bc7229a6ab8 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -119,6 +119,7 @@ impl_actions!( gpui::actions!( editor, [ + AcceptPartialCopilotSuggestion, AddSelectionAbove, AddSelectionBelow, Backspace, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 86312b0134fbddb2cf9a1e3a63374071ab981506..258e3fb8a9b883db7c8c20439786ca9dc3ae3b87 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + ) { + 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::(); + if partial_suggestion.is_empty() { + partial_suggestion = suggestion + .text + .chars() + .by_ref() + .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .collect::(); + } + + 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) -> bool { if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 21072c908d2f7dcc9f84a5dd85b48d1ae6b8c7b0..c1d5e052e1c02905134296c3adeb56b5e47dfbab 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7823140a12cf0b3a32f4f4dc31ab871c92f74bef..43a9c8f746cf0ba13f58fa1350fa04d9541d9caf 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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(