use crate::{Completion, Copilot};use anyhow::Result;use gpui::{AppContext, EntityId, Model, ModelContext, Task};use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, OffsetRangeExt, ToOffset,};use settings::Settings;use std::{path::Path, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotCompletionProvider { cycled: bool, buffer_id: Option<EntityId>, completions: Vec<Completion>, active_completion_index: usize, file_extension: Option<String>, pending_refresh: Option<Task<Result<()>>>, pending_cycling_refresh: Option<Task<Result<()>>>, copilot: Model<Copilot>,} impl CopilotCompletionProvider { pub fn new(copilot: Model<Copilot>) -> Self { Self { cycled: false, buffer_id: None, completions: Vec::new(), active_completion_index: 0, file_extension: None, pending_refresh: None, pending_cycling_refresh: None, copilot, } } fn active_completion(&self) -> Option<&Completion> { self.completions.get(self.active_completion_index) } fn push_completion(&mut self, new_completion: Completion) { for completion in &self.completions { if completion.text == new_completion.text && completion.range == new_completion.range { return; } } self.completions.push(new_completion); }} impl InlineCompletionProvider for CopilotCompletionProvider { fn name() -> &'static str { "copilot" } fn display_name() -> &'static str { "Copilot" } fn show_completions_in_menu() -> bool { false } fn show_completions_in_normal_mode() -> bool { false } fn is_refreshing(&self) -> bool { self.pending_refresh.is_some() } fn is_enabled( &self, buffer: &Model<Buffer>, cursor_position: language::Anchor, cx: &AppContext, ) -> bool { if !self.copilot.read(cx).status().is_authorized() { return false; } let buffer = buffer.read(cx); let file = buffer.file(); let language = buffer.language_at(cursor_position); let settings = all_language_settings(file, cx); settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } fn refresh( &mut self, buffer: Model<Buffer>, cursor_position: language::Anchor, debounce: bool, cx: &mut ModelContext<Self>, ) { let copilot = self.copilot.clone(); self.pending_refresh = Some(cx.spawn(|this, mut cx| async move { if debounce { cx.background_executor() .timer(COPILOT_DEBOUNCE_TIMEOUT) .await; } let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions(&buffer, cursor_position, cx) })? .await?; this.update(&mut cx, |this, cx| { if !completions.is_empty() { this.cycled = false; this.pending_refresh = None; this.pending_cycling_refresh = None; this.completions.clear(); this.active_completion_index = 0; this.buffer_id = Some(buffer.entity_id()); this.file_extension = buffer.read(cx).file().and_then(|file| { Some( Path::new(file.file_name(cx)) .extension()? .to_str()? .to_string(), ) }); for completion in completions { this.push_completion(completion); } cx.notify(); } })?; Ok(()) })); } fn cycle( &mut self, buffer: Model<Buffer>, cursor_position: language::Anchor, direction: Direction, cx: &mut ModelContext<Self>, ) { if self.cycled { match direction { Direction::Prev => { self.active_completion_index = if self.active_completion_index == 0 { self.completions.len().saturating_sub(1) } else { self.active_completion_index - 1 }; } Direction::Next => { if self.completions.is_empty() { self.active_completion_index = 0 } else { self.active_completion_index = (self.active_completion_index + 1) % self.completions.len(); } } } cx.notify(); } else { let copilot = self.copilot.clone(); self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move { let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions_cycling(&buffer, cursor_position, cx) })? .await?; this.update(&mut cx, |this, cx| { this.cycled = true; this.file_extension = buffer.read(cx).file().and_then(|file| { Some( Path::new(file.file_name(cx)) .extension()? .to_str()? .to_string(), ) }); for completion in completions { this.push_completion(completion); } this.cycle(buffer, cursor_position, direction, cx); })?; Ok(()) })); } } fn accept(&mut self, cx: &mut ModelContext<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, cx: &mut ModelContext<Self>) { let settings = AllLanguageSettings::get_global(cx); let copilot_enabled = settings.inline_completions_enabled(None, None, cx); if !copilot_enabled { return; } self.copilot .update(cx, |copilot, cx| { copilot.discard_completions(&self.completions, cx) }) .detach_and_log_err(cx); } fn suggest( &mut self, buffer: &Model<Buffer>, cursor_position: language::Anchor, cx: &mut ModelContext<Self>, ) -> Option<InlineCompletion> { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let completion = self.active_completion()?; if Some(buffer_id) != self.buffer_id || !completion.range.start.is_valid(buffer) || !completion.range.end.is_valid(buffer) { return None; } 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); if completion_range.is_empty() && completion_range.start == cursor_position.to_offset(buffer) { let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; if completion_text.trim().is_empty() { None } else { let position = cursor_position.bias_right(buffer); Some(InlineCompletion { edits: vec![(position..position, completion_text.into())], }) } } else { None } }} 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 editor::{ test::editor_lsp_test_context::EditorLspTestContext, Editor, ExcerptRange, MultiBuffer, }; use fs::FakeFs; use futures::StreamExt; use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Point, }; use project::Project; use serde_json::json; use settings::SettingsStore; use std::future::Future; use util::test::{marked_text_ranges_by, TextRangeMarker}; #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { // flaky init_test(cx, |_| {}); 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_model(|_| CopilotCompletionProvider::new(copilot)); cx.update_editor(|editor, cx| { editor.set_inline_completion_provider(Some(copilot_provider), 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::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.context_menu_visible()); assert!(!editor.context_menu_contains_inline_completion()); assert!(!editor.has_active_inline_completion()); // Since we have both, the copilot suggestion is not shown inline 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(), cx) .unwrap() .detach(); assert!(!editor.context_menu_visible()); assert!(!editor.has_active_inline_completion()); 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::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.context_menu_visible()); assert!(editor.has_active_inline_completion()); // 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 inline completion 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.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion()); 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::Completion { text: "one.copilot2".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), ..Default::default() }], vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert!(!editor.context_menu_contains_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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 is shown again. editor.undo(&Default::default(), cx); assert!(editor.has_active_inline_completion()); 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, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptInlineCompletion when there is an active suggestion inserts it. editor.accept_inline_completion(&Default::default(), cx); assert!(!editor.has_active_inline_completion()); 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(), cx); assert!(editor.has_active_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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_inline_completion()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); // Reset the editor to verify how suggestions behave when tabbing on leading indentation. cx.update_editor(|editor, cx| { editor.set_text("fn foo() {\n \n}", cx); editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) }); }); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::Completion { text: " let x = 4;".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), ..Default::default() }], vec![], ); cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");Lines 1–500 of 1116. Load more
use crate::{Completion, Copilot}; use anyhow::Result; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, OffsetRangeExt, ToOffset, }; use settings::Settings; use std::{path::Path, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub struct CopilotCompletionProvider { cycled: bool, buffer_id: Option<EntityId>, completions: Vec<Completion>, active_completion_index: usize, file_extension: Option<String>, pending_refresh: Option<Task<Result<()>>>, pending_cycling_refresh: Option<Task<Result<()>>>, copilot: Model<Copilot>, } impl CopilotCompletionProvider { pub fn new(copilot: Model<Copilot>) -> Self { Self { cycled: false, buffer_id: None, completions: Vec::new(), active_completion_index: 0, file_extension: None, pending_refresh: None, pending_cycling_refresh: None, copilot, } } fn active_completion(&self) -> Option<&Completion> { self.completions.get(self.active_completion_index) } fn push_completion(&mut self, new_completion: Completion) { for completion in &self.completions { if completion.text == new_completion.text && completion.range == new_completion.range { return; } } self.completions.push(new_completion); } } impl InlineCompletionProvider for CopilotCompletionProvider { fn name() -> &'static str { "copilot" } fn display_name() -> &'static str { "Copilot" } fn show_completions_in_menu() -> bool { false } fn show_completions_in_normal_mode() -> bool { false } fn is_refreshing(&self) -> bool { self.pending_refresh.is_some() } fn is_enabled( &self, buffer: &Model<Buffer>, cursor_position: language::Anchor, cx: &AppContext, ) -> bool { if !self.copilot.read(cx).status().is_authorized() { return false; } let buffer = buffer.read(cx); let file = buffer.file(); let language = buffer.language_at(cursor_position); let settings = all_language_settings(file, cx); settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } fn refresh( &mut self, buffer: Model<Buffer>, cursor_position: language::Anchor, debounce: bool, cx: &mut ModelContext<Self>, ) { let copilot = self.copilot.clone(); self.pending_refresh = Some(cx.spawn(|this, mut cx| async move { if debounce { cx.background_executor() .timer(COPILOT_DEBOUNCE_TIMEOUT) .await; } let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions(&buffer, cursor_position, cx) })? .await?; this.update(&mut cx, |this, cx| { if !completions.is_empty() { this.cycled = false; this.pending_refresh = None; this.pending_cycling_refresh = None; this.completions.clear(); this.active_completion_index = 0; this.buffer_id = Some(buffer.entity_id()); this.file_extension = buffer.read(cx).file().and_then(|file| { Some( Path::new(file.file_name(cx)) .extension()? .to_str()? .to_string(), ) }); for completion in completions { this.push_completion(completion); } cx.notify(); } })?; Ok(()) })); } fn cycle( &mut self, buffer: Model<Buffer>, cursor_position: language::Anchor, direction: Direction, cx: &mut ModelContext<Self>, ) { if self.cycled { match direction { Direction::Prev => { self.active_completion_index = if self.active_completion_index == 0 { self.completions.len().saturating_sub(1) } else { self.active_completion_index - 1 }; } Direction::Next => { if self.completions.is_empty() { self.active_completion_index = 0 } else { self.active_completion_index = (self.active_completion_index + 1) % self.completions.len(); } } } cx.notify(); } else { let copilot = self.copilot.clone(); self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move { let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions_cycling(&buffer, cursor_position, cx) })? .await?; this.update(&mut cx, |this, cx| { this.cycled = true; this.file_extension = buffer.read(cx).file().and_then(|file| { Some( Path::new(file.file_name(cx)) .extension()? .to_str()? .to_string(), ) }); for completion in completions { this.push_completion(completion); } this.cycle(buffer, cursor_position, direction, cx); })?; Ok(()) })); } } fn accept(&mut self, cx: &mut ModelContext<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, cx: &mut ModelContext<Self>) { let settings = AllLanguageSettings::get_global(cx); let copilot_enabled = settings.inline_completions_enabled(None, None, cx); if !copilot_enabled { return; } self.copilot .update(cx, |copilot, cx| { copilot.discard_completions(&self.completions, cx) }) .detach_and_log_err(cx); } fn suggest( &mut self, buffer: &Model<Buffer>, cursor_position: language::Anchor, cx: &mut ModelContext<Self>, ) -> Option<InlineCompletion> { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let completion = self.active_completion()?; if Some(buffer_id) != self.buffer_id || !completion.range.start.is_valid(buffer) || !completion.range.end.is_valid(buffer) { return None; } 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); if completion_range.is_empty() && completion_range.start == cursor_position.to_offset(buffer) { let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len]; if completion_text.trim().is_empty() { None } else { let position = cursor_position.bias_right(buffer); Some(InlineCompletion { edits: vec![(position..position, completion_text.into())], }) } } else { None } } } 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 editor::{ test::editor_lsp_test_context::EditorLspTestContext, Editor, ExcerptRange, MultiBuffer, }; use fs::FakeFs; use futures::StreamExt; use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Point, }; use project::Project; use serde_json::json; use settings::SettingsStore; use std::future::Future; use util::test::{marked_text_ranges_by, TextRangeMarker}; #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { // flaky init_test(cx, |_| {}); 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_model(|_| CopilotCompletionProvider::new(copilot)); cx.update_editor(|editor, cx| { editor.set_inline_completion_provider(Some(copilot_provider), 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::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.context_menu_visible()); assert!(!editor.context_menu_contains_inline_completion()); assert!(!editor.has_active_inline_completion()); // Since we have both, the copilot suggestion is not shown inline 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(), cx) .unwrap() .detach(); assert!(!editor.context_menu_visible()); assert!(!editor.has_active_inline_completion()); 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::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.context_menu_visible()); assert!(editor.has_active_inline_completion()); // 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 inline completion 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.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion()); 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::Completion { text: "one.copilot2".into(), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), ..Default::default() }], vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert!(!editor.context_menu_contains_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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 is shown again. editor.undo(&Default::default(), cx); assert!(editor.has_active_inline_completion()); 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, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptInlineCompletion when there is an active suggestion inserts it. editor.accept_inline_completion(&Default::default(), cx); assert!(!editor.has_active_inline_completion()); 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(), cx); assert!(editor.has_active_inline_completion()); 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(), cx); assert!(!editor.has_active_inline_completion()); 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_inline_completion()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); // Reset the editor to verify how suggestions behave when tabbing on leading indentation. cx.update_editor(|editor, cx| { editor.set_text("fn foo() {\n \n}", cx); editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) }); }); handle_copilot_completion_request( &copilot_lsp, vec![crate::request::Completion { text: " let x = 4;".into(), range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), ..Default::default() }], vec![], ); cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");