use crate::{ CompletionSource, Copilot, CopilotEditPrediction, request::{ DidShowCompletion, DidShowCompletionParams, DidShowInlineEdit, DidShowInlineEditParams, InlineCompletionItem, },};use anyhow::Result;use edit_prediction_types::{ EditPrediction, EditPredictionDelegate, EditPredictionDiscardReason, EditPredictionIconSet, EditPredictionRequestTrigger, interpolate_edits,};use gpui::{App, Context, Entity, Task, TaskExt};use icons::IconName;use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16};use std::{ops::Range, sync::Arc, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotEditPredictionDelegate { completion: Option<(CopilotEditPrediction, EditPreview)>, pending_refresh: Option<Task<Result<()>>>, copilot: Entity<Copilot>,} impl CopilotEditPredictionDelegate { pub fn new(copilot: Entity<Copilot>) -> Self { Self { completion: None, pending_refresh: None, copilot, } } fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> { self.completion.as_ref() }} impl EditPredictionDelegate for CopilotEditPredictionDelegate { fn name() -> &'static str { "copilot" } fn display_name() -> &'static str { "Copilot" } fn show_predictions_in_menu() -> bool { true } fn show_tab_accept_marker() -> bool { true } fn icons(&self, _cx: &App) -> EditPredictionIconSet { EditPredictionIconSet::new(IconName::Copilot) .with_disabled(IconName::CopilotDisabled) .with_error(IconName::CopilotError) } fn is_refreshing(&self, _cx: &App) -> bool { self.pending_refresh.is_some() && self.completion.is_none() } fn is_enabled( &self, _buffer: &Entity<Buffer>, _cursor_position: language::Anchor, cx: &App, ) -> bool { self.copilot.read(cx).status().is_authorized() } fn refresh( &mut self, buffer: Entity<Buffer>, cursor_position: language::Anchor, debounce: bool, _trigger: EditPredictionRequestTrigger, cx: &mut Context<Self>, ) { let copilot = self.copilot.clone(); self.pending_refresh = Some(cx.spawn(async move |this, cx| { if debounce { cx.background_executor() .timer(COPILOT_DEBOUNCE_TIMEOUT) .await; } let completions = copilot .update(cx, |copilot, cx| { copilot.completions(&buffer, cursor_position, cx) }) .await?; if let Some(mut completion) = completions.into_iter().next() && let Some((trimmed_range, trimmed_text, snapshot)) = cx.update(|cx| trim_completion(&completion, cx)) { let preview = buffer .update(cx, |this, cx| { this.preview_edits( Arc::from([(trimmed_range.clone(), trimmed_text.clone())].as_slice()), cx, ) }) .await; this.update(cx, |this, cx| { this.pending_refresh = None; completion.range = trimmed_range; completion.text = trimmed_text.to_string(); completion.snapshot = snapshot; this.completion = Some((completion, preview)); cx.notify(); })?; } Ok(()) })); } fn accept(&mut self, cx: &mut Context<Self>) { if let Some((completion, _)) = self.active_completion() { self.copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); } } fn discard(&mut self, _reason: EditPredictionDiscardReason, _: &mut Context<Self>) { self.completion.take(); } fn suggest( &mut self, buffer: &Entity<Buffer>, _: language::Anchor, cx: &mut Context<Self>, ) -> Option<EditPrediction> { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let (completion, edit_preview) = self.active_completion()?; if Some(buffer_id) != Some(completion.buffer.entity_id()) || !completion.range.start.is_valid(buffer) || !completion.range.end.is_valid(buffer) { return None; } let edits = vec![( completion.range.clone(), Arc::from(completion.text.as_ref()), )]; let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits) .filter(|edits| !edits.is_empty())?; self.copilot.update(cx, |this, _| { if let Ok(server) = this.server.as_authenticated() { match completion.source { CompletionSource::NextEditSuggestion => { if let Some(cmd) = completion.command.as_ref() { _ = server .lsp .notify::<DidShowInlineEdit>(DidShowInlineEditParams { item: serde_json::json!({"command": {"arguments": cmd.arguments}}), }); } } CompletionSource::InlineCompletion => { _ = server.lsp.notify::<DidShowCompletion>(DidShowCompletionParams { item: InlineCompletionItem { insert_text: completion.text.clone(), range: lsp::Range::new( language::point_to_lsp( completion.range.start.to_point_utf16(&completion.snapshot), ), language::point_to_lsp( completion.range.end.to_point_utf16(&completion.snapshot), ), ), command: completion.command.clone(), }, }); } } } }); Some(EditPrediction::Local { id: None, edits, cursor_position: None, edit_preview: Some(edit_preview.clone()), }) }} fn trim_completion( completion: &CopilotEditPrediction, cx: &mut App,) -> Option<(Range<Anchor>, Arc<str>, BufferSnapshot)> { let buffer = completion.buffer.read(cx); let mut completion_range = completion.range.to_offset(buffer); let prefix_len = common_prefix( buffer.chars_for_range(completion_range.clone()), completion.text.chars(), ); completion_range.start += prefix_len; let suffix_len = common_prefix( buffer.reversed_chars_for_range(completion_range.clone()), completion.text[prefix_len..].chars().rev(), ); completion_range.end = completion_range.end.saturating_sub(suffix_len); let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; if completion_text.trim().is_empty() { None } else { let snapshot = buffer.snapshot(); let completion_range = snapshot.anchor_after(completion_range.start) ..snapshot.anchor_after(completion_range.end); Some((completion_range, Arc::from(completion_text), snapshot)) }} fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize { a.zip(b) .take_while(|(a, b)| a == b) .map(|(a, _)| a.len_utf8()) .sum()} #[cfg(test)]mod tests { use super::*; use edit_prediction_types::EditPredictionGranularity; use editor::{ Editor, MultiBuffer, MultiBufferOffset, PathKey, SelectionEffects, test::{editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext}, }; use fs::FakeFs; use futures::StreamExt; use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal}; use indoc::indoc; use language::{ Point, language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode}, }; use lsp::Uri; use project::Project; use serde_json::json; use settings::{AllLanguageSettingsContent, SettingsStore}; use std::future::Future; use util::{ path, test::{TextRangeMarker, marked_text_ranges_by}, }; #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { // flaky init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettingsContent { words: Some(WordsCompletionMode::Disabled), words_min_length: Some(0), lsp_insert_mode: Some(LspInsertMode::Insert), ..Default::default() }); }); let (copilot, copilot_lsp) = Copilot::fake(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; let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(copilot_provider), EditPredictionRequestTrigger::EditorCreated, window, cx, ) }); cx.set_state(indoc! {" oneˇ two three "}); cx.simulate_keystroke("."); drop(handle_completion_request( &mut cx, indoc! {" one.|<> two three "}, vec!["completion_a", "completion_b"], )); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); // Since we have both, the copilot suggestion is existing but does not show up as ghost text assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); // Confirming a non-copilot completion inserts it and hides the context menu, without showing // the copilot suggestion afterwards. editor .confirm_completion(&Default::default(), window, cx) .unwrap() .detach(); assert!(!editor.context_menu_visible()); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); // Reset editor and only return copilot suggestions cx.set_state(indoc! {" oneˇ two three "}); cx.simulate_keystroke("."); drop(handle_completion_request( &mut cx, indoc! {" one.|<> two three "}, vec![], )); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); // 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 edit prediction is interpolated when inserting again. cx.simulate_keystroke("c"); executor.run_until_parked(); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); // After debouncing, new Copilot completions should be requested. handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot2".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. editor.tab(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion isn't shown again. editor.undo(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.editor(|editor, _, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); cx.update_editor(|editor, window, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptEditPrediction when there is an active suggestion inserts it. editor.accept_edit_prediction(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); }); // If an edit occurs outside of this editor but no suggestion is being shown, // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); cx.update_editor(|editor, _, cx| { assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); } #[gpui::test(iterations = 10)] async fn test_accept_partial_copilot_suggestion( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { // flaky init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettingsContent { words: Some(WordsCompletionMode::Disabled), words_min_length: Some(0), lsp_insert_mode: Some(LspInsertMode::Insert), ..Default::default() }); }); let (copilot, copilot_lsp) = Copilot::fake(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; let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(copilot_provider),Lines 1–500 of 1172. Load more
use crate::{ CompletionSource, Copilot, CopilotEditPrediction, request::{ DidShowCompletion, DidShowCompletionParams, DidShowInlineEdit, DidShowInlineEditParams, InlineCompletionItem, }, }; use anyhow::Result; use edit_prediction_types::{ EditPrediction, EditPredictionDelegate, EditPredictionDiscardReason, EditPredictionIconSet, EditPredictionRequestTrigger, interpolate_edits, }; use gpui::{App, Context, Entity, Task, TaskExt}; use icons::IconName; use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16}; use std::{ops::Range, sync::Arc, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotEditPredictionDelegate { completion: Option<(CopilotEditPrediction, EditPreview)>, pending_refresh: Option<Task<Result<()>>>, copilot: Entity<Copilot>, } impl CopilotEditPredictionDelegate { pub fn new(copilot: Entity<Copilot>) -> Self { Self { completion: None, pending_refresh: None, copilot, } } fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> { self.completion.as_ref() } } impl EditPredictionDelegate for CopilotEditPredictionDelegate { fn name() -> &'static str { "copilot" } fn display_name() -> &'static str { "Copilot" } fn show_predictions_in_menu() -> bool { true } fn show_tab_accept_marker() -> bool { true } fn icons(&self, _cx: &App) -> EditPredictionIconSet { EditPredictionIconSet::new(IconName::Copilot) .with_disabled(IconName::CopilotDisabled) .with_error(IconName::CopilotError) } fn is_refreshing(&self, _cx: &App) -> bool { self.pending_refresh.is_some() && self.completion.is_none() } fn is_enabled( &self, _buffer: &Entity<Buffer>, _cursor_position: language::Anchor, cx: &App, ) -> bool { self.copilot.read(cx).status().is_authorized() } fn refresh( &mut self, buffer: Entity<Buffer>, cursor_position: language::Anchor, debounce: bool, _trigger: EditPredictionRequestTrigger, cx: &mut Context<Self>, ) { let copilot = self.copilot.clone(); self.pending_refresh = Some(cx.spawn(async move |this, cx| { if debounce { cx.background_executor() .timer(COPILOT_DEBOUNCE_TIMEOUT) .await; } let completions = copilot .update(cx, |copilot, cx| { copilot.completions(&buffer, cursor_position, cx) }) .await?; if let Some(mut completion) = completions.into_iter().next() && let Some((trimmed_range, trimmed_text, snapshot)) = cx.update(|cx| trim_completion(&completion, cx)) { let preview = buffer .update(cx, |this, cx| { this.preview_edits( Arc::from([(trimmed_range.clone(), trimmed_text.clone())].as_slice()), cx, ) }) .await; this.update(cx, |this, cx| { this.pending_refresh = None; completion.range = trimmed_range; completion.text = trimmed_text.to_string(); completion.snapshot = snapshot; this.completion = Some((completion, preview)); cx.notify(); })?; } Ok(()) })); } fn accept(&mut self, cx: &mut Context<Self>) { if let Some((completion, _)) = self.active_completion() { self.copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); } } fn discard(&mut self, _reason: EditPredictionDiscardReason, _: &mut Context<Self>) { self.completion.take(); } fn suggest( &mut self, buffer: &Entity<Buffer>, _: language::Anchor, cx: &mut Context<Self>, ) -> Option<EditPrediction> { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let (completion, edit_preview) = self.active_completion()?; if Some(buffer_id) != Some(completion.buffer.entity_id()) || !completion.range.start.is_valid(buffer) || !completion.range.end.is_valid(buffer) { return None; } let edits = vec![( completion.range.clone(), Arc::from(completion.text.as_ref()), )]; let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits) .filter(|edits| !edits.is_empty())?; self.copilot.update(cx, |this, _| { if let Ok(server) = this.server.as_authenticated() { match completion.source { CompletionSource::NextEditSuggestion => { if let Some(cmd) = completion.command.as_ref() { _ = server .lsp .notify::<DidShowInlineEdit>(DidShowInlineEditParams { item: serde_json::json!({"command": {"arguments": cmd.arguments}}), }); } } CompletionSource::InlineCompletion => { _ = server.lsp.notify::<DidShowCompletion>(DidShowCompletionParams { item: InlineCompletionItem { insert_text: completion.text.clone(), range: lsp::Range::new( language::point_to_lsp( completion.range.start.to_point_utf16(&completion.snapshot), ), language::point_to_lsp( completion.range.end.to_point_utf16(&completion.snapshot), ), ), command: completion.command.clone(), }, }); } } } }); Some(EditPrediction::Local { id: None, edits, cursor_position: None, edit_preview: Some(edit_preview.clone()), }) } } fn trim_completion( completion: &CopilotEditPrediction, cx: &mut App, ) -> Option<(Range<Anchor>, Arc<str>, BufferSnapshot)> { let buffer = completion.buffer.read(cx); let mut completion_range = completion.range.to_offset(buffer); let prefix_len = common_prefix( buffer.chars_for_range(completion_range.clone()), completion.text.chars(), ); completion_range.start += prefix_len; let suffix_len = common_prefix( buffer.reversed_chars_for_range(completion_range.clone()), completion.text[prefix_len..].chars().rev(), ); completion_range.end = completion_range.end.saturating_sub(suffix_len); let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; if completion_text.trim().is_empty() { None } else { let snapshot = buffer.snapshot(); let completion_range = snapshot.anchor_after(completion_range.start) ..snapshot.anchor_after(completion_range.end); Some((completion_range, Arc::from(completion_text), snapshot)) } } fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize { a.zip(b) .take_while(|(a, b)| a == b) .map(|(a, _)| a.len_utf8()) .sum() } #[cfg(test)] mod tests { use super::*; use edit_prediction_types::EditPredictionGranularity; use editor::{ Editor, MultiBuffer, MultiBufferOffset, PathKey, SelectionEffects, test::{editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext}, }; use fs::FakeFs; use futures::StreamExt; use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal}; use indoc::indoc; use language::{ Point, language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode}, }; use lsp::Uri; use project::Project; use serde_json::json; use settings::{AllLanguageSettingsContent, SettingsStore}; use std::future::Future; use util::{ path, test::{TextRangeMarker, marked_text_ranges_by}, }; #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { // flaky init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettingsContent { words: Some(WordsCompletionMode::Disabled), words_min_length: Some(0), lsp_insert_mode: Some(LspInsertMode::Insert), ..Default::default() }); }); let (copilot, copilot_lsp) = Copilot::fake(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; let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(copilot_provider), EditPredictionRequestTrigger::EditorCreated, window, cx, ) }); cx.set_state(indoc! {" oneˇ two three "}); cx.simulate_keystroke("."); drop(handle_completion_request( &mut cx, indoc! {" one.|<> two three "}, vec!["completion_a", "completion_b"], )); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); // Since we have both, the copilot suggestion is existing but does not show up as ghost text assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); // Confirming a non-copilot completion inserts it and hides the context menu, without showing // the copilot suggestion afterwards. editor .confirm_completion(&Default::default(), window, cx) .unwrap() .detach(); assert!(!editor.context_menu_visible()); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); // Reset editor and only return copilot suggestions cx.set_state(indoc! {" oneˇ two three "}); cx.simulate_keystroke("."); drop(handle_completion_request( &mut cx, indoc! {" one.|<> two three "}, vec![], )); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot1".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); // 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 edit prediction is interpolated when inserting again. cx.simulate_keystroke("c"); executor.run_until_parked(); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); // After debouncing, new Copilot completions should be requested. handle_copilot_completion_request( &copilot_lsp, vec![crate::request::NextEditSuggestion { text: "one.copilot2".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), command: None, text_document: lsp::VersionedTextDocumentIdentifier { uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: 0, }, }], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. editor.tab(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion isn't shown again. editor.undo(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.editor(|editor, _, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); cx.update_editor(|editor, window, cx| { assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptEditPrediction when there is an active suggestion inserts it. editor.accept_edit_prediction(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); }); // If an edit occurs outside of this editor but no suggestion is being shown, // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); cx.update_editor(|editor, _, cx| { assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); } #[gpui::test(iterations = 10)] async fn test_accept_partial_copilot_suggestion( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { // flaky init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettingsContent { words: Some(WordsCompletionMode::Disabled), words_min_length: Some(0), lsp_insert_mode: Some(LspInsertMode::Insert), ..Default::default() }); }); let (copilot, copilot_lsp) = Copilot::fake(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; let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(copilot_provider),