1use crate::{Completion, Copilot};
2use anyhow::Result;
3use edit_prediction::{Direction, EditPrediction, EditPredictionProvider};
4use gpui::{App, Context, Entity, EntityId, Task};
5use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
6use settings::Settings;
7use std::{path::Path, time::Duration};
8
9pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
10
11pub struct CopilotCompletionProvider {
12 cycled: bool,
13 buffer_id: Option<EntityId>,
14 completions: Vec<Completion>,
15 active_completion_index: usize,
16 file_extension: Option<String>,
17 pending_refresh: Option<Task<Result<()>>>,
18 pending_cycling_refresh: Option<Task<Result<()>>>,
19 copilot: Entity<Copilot>,
20}
21
22impl CopilotCompletionProvider {
23 pub fn new(copilot: Entity<Copilot>) -> Self {
24 Self {
25 cycled: false,
26 buffer_id: None,
27 completions: Vec::new(),
28 active_completion_index: 0,
29 file_extension: None,
30 pending_refresh: None,
31 pending_cycling_refresh: None,
32 copilot,
33 }
34 }
35
36 fn active_completion(&self) -> Option<&Completion> {
37 self.completions.get(self.active_completion_index)
38 }
39
40 fn push_completion(&mut self, new_completion: Completion) {
41 for completion in &self.completions {
42 if completion.text == new_completion.text && completion.range == new_completion.range {
43 return;
44 }
45 }
46 self.completions.push(new_completion);
47 }
48}
49
50impl EditPredictionProvider for CopilotCompletionProvider {
51 fn name() -> &'static str {
52 "copilot"
53 }
54
55 fn display_name() -> &'static str {
56 "Copilot"
57 }
58
59 fn show_completions_in_menu() -> bool {
60 true
61 }
62
63 fn show_tab_accept_marker() -> bool {
64 true
65 }
66
67 fn supports_jump_to_edit() -> bool {
68 false
69 }
70
71 fn is_refreshing(&self) -> bool {
72 self.pending_refresh.is_some() && self.completions.is_empty()
73 }
74
75 fn is_enabled(
76 &self,
77 _buffer: &Entity<Buffer>,
78 _cursor_position: language::Anchor,
79 cx: &App,
80 ) -> bool {
81 self.copilot.read(cx).status().is_authorized()
82 }
83
84 fn refresh(
85 &mut self,
86 buffer: Entity<Buffer>,
87 cursor_position: language::Anchor,
88 debounce: bool,
89 cx: &mut Context<Self>,
90 ) {
91 let copilot = self.copilot.clone();
92 self.pending_refresh = Some(cx.spawn(async move |this, cx| {
93 if debounce {
94 cx.background_executor()
95 .timer(COPILOT_DEBOUNCE_TIMEOUT)
96 .await;
97 }
98
99 let completions = copilot
100 .update(cx, |copilot, cx| {
101 copilot.completions(&buffer, cursor_position, cx)
102 })?
103 .await?;
104
105 this.update(cx, |this, cx| {
106 if !completions.is_empty() {
107 this.cycled = false;
108 this.pending_refresh = None;
109 this.pending_cycling_refresh = None;
110 this.completions.clear();
111 this.active_completion_index = 0;
112 this.buffer_id = Some(buffer.entity_id());
113 this.file_extension = buffer.read(cx).file().and_then(|file| {
114 Some(
115 Path::new(file.file_name(cx))
116 .extension()?
117 .to_str()?
118 .to_string(),
119 )
120 });
121
122 for completion in completions {
123 this.push_completion(completion);
124 }
125 cx.notify();
126 }
127 })?;
128
129 Ok(())
130 }));
131 }
132
133 fn cycle(
134 &mut self,
135 buffer: Entity<Buffer>,
136 cursor_position: language::Anchor,
137 direction: Direction,
138 cx: &mut Context<Self>,
139 ) {
140 if self.cycled {
141 match direction {
142 Direction::Prev => {
143 self.active_completion_index = if self.active_completion_index == 0 {
144 self.completions.len().saturating_sub(1)
145 } else {
146 self.active_completion_index - 1
147 };
148 }
149 Direction::Next => {
150 if self.completions.is_empty() {
151 self.active_completion_index = 0
152 } else {
153 self.active_completion_index =
154 (self.active_completion_index + 1) % self.completions.len();
155 }
156 }
157 }
158
159 cx.notify();
160 } else {
161 let copilot = self.copilot.clone();
162 self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
163 let completions = copilot
164 .update(cx, |copilot, cx| {
165 copilot.completions_cycling(&buffer, cursor_position, cx)
166 })?
167 .await?;
168
169 this.update(cx, |this, cx| {
170 this.cycled = true;
171 this.file_extension = buffer.read(cx).file().and_then(|file| {
172 Some(
173 Path::new(file.file_name(cx))
174 .extension()?
175 .to_str()?
176 .to_string(),
177 )
178 });
179 for completion in completions {
180 this.push_completion(completion);
181 }
182 this.cycle(buffer, cursor_position, direction, cx);
183 })?;
184
185 Ok(())
186 }));
187 }
188 }
189
190 fn accept(&mut self, cx: &mut Context<Self>) {
191 if let Some(completion) = self.active_completion() {
192 self.copilot
193 .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
194 .detach_and_log_err(cx);
195 }
196 }
197
198 fn discard(&mut self, cx: &mut Context<Self>) {
199 let settings = AllLanguageSettings::get_global(cx);
200
201 let copilot_enabled = settings.show_edit_predictions(None, cx);
202
203 if !copilot_enabled {
204 return;
205 }
206
207 self.copilot
208 .update(cx, |copilot, cx| {
209 copilot.discard_completions(&self.completions, cx)
210 })
211 .detach_and_log_err(cx);
212 }
213
214 fn suggest(
215 &mut self,
216 buffer: &Entity<Buffer>,
217 cursor_position: language::Anchor,
218 cx: &mut Context<Self>,
219 ) -> Option<EditPrediction> {
220 let buffer_id = buffer.entity_id();
221 let buffer = buffer.read(cx);
222 let completion = self.active_completion()?;
223 if Some(buffer_id) != self.buffer_id
224 || !completion.range.start.is_valid(buffer)
225 || !completion.range.end.is_valid(buffer)
226 {
227 return None;
228 }
229
230 let mut completion_range = completion.range.to_offset(buffer);
231 let prefix_len = common_prefix(
232 buffer.chars_for_range(completion_range.clone()),
233 completion.text.chars(),
234 );
235 completion_range.start += prefix_len;
236 let suffix_len = common_prefix(
237 buffer.reversed_chars_for_range(completion_range.clone()),
238 completion.text[prefix_len..].chars().rev(),
239 );
240 completion_range.end = completion_range.end.saturating_sub(suffix_len);
241
242 if completion_range.is_empty()
243 && completion_range.start == cursor_position.to_offset(buffer)
244 {
245 let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
246 if completion_text.trim().is_empty() {
247 None
248 } else {
249 let position = cursor_position.bias_right(buffer);
250 Some(EditPrediction::Local {
251 id: None,
252 edits: vec![(position..position, completion_text.into())],
253 edit_preview: None,
254 })
255 }
256 } else {
257 None
258 }
259 }
260}
261
262fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
263 a.zip(b)
264 .take_while(|(a, b)| a == b)
265 .map(|(a, _)| a.len_utf8())
266 .sum()
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use editor::{
273 Editor, ExcerptRange, MultiBuffer, SelectionEffects,
274 test::editor_lsp_test_context::EditorLspTestContext,
275 };
276 use fs::FakeFs;
277 use futures::StreamExt;
278 use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
279 use indoc::indoc;
280 use language::{
281 Point,
282 language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
283 };
284 use project::Project;
285 use serde_json::json;
286 use settings::{AllLanguageSettingsContent, SettingsStore};
287 use std::future::Future;
288 use util::{
289 path,
290 test::{TextRangeMarker, marked_text_ranges_by},
291 };
292
293 #[gpui::test(iterations = 10)]
294 async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
295 // flaky
296 init_test(cx, |settings| {
297 settings.defaults.completions = Some(CompletionSettingsContent {
298 words: Some(WordsCompletionMode::Disabled),
299 words_min_length: Some(0),
300 lsp_insert_mode: Some(LspInsertMode::Insert),
301 ..Default::default()
302 });
303 });
304
305 let (copilot, copilot_lsp) = Copilot::fake(cx);
306 let mut cx = EditorLspTestContext::new_rust(
307 lsp::ServerCapabilities {
308 completion_provider: Some(lsp::CompletionOptions {
309 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
310 ..Default::default()
311 }),
312 ..Default::default()
313 },
314 cx,
315 )
316 .await;
317 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
318 cx.update_editor(|editor, window, cx| {
319 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
320 });
321
322 cx.set_state(indoc! {"
323 oneˇ
324 two
325 three
326 "});
327 cx.simulate_keystroke(".");
328 drop(handle_completion_request(
329 &mut cx,
330 indoc! {"
331 one.|<>
332 two
333 three
334 "},
335 vec!["completion_a", "completion_b"],
336 ));
337 handle_copilot_completion_request(
338 &copilot_lsp,
339 vec![crate::request::Completion {
340 text: "one.copilot1".into(),
341 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
342 ..Default::default()
343 }],
344 vec![],
345 );
346 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
347 cx.update_editor(|editor, window, cx| {
348 assert!(editor.context_menu_visible());
349 assert!(editor.has_active_edit_prediction());
350 // Since we have both, the copilot suggestion is existing but does not show up as ghost text
351 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
352 assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
353
354 // Confirming a non-copilot completion inserts it and hides the context menu, without showing
355 // the copilot suggestion afterwards.
356 editor
357 .confirm_completion(&Default::default(), window, cx)
358 .unwrap()
359 .detach();
360 assert!(!editor.context_menu_visible());
361 assert!(!editor.has_active_edit_prediction());
362 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
363 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
364 });
365
366 // Reset editor and only return copilot suggestions
367 cx.set_state(indoc! {"
368 oneˇ
369 two
370 three
371 "});
372 cx.simulate_keystroke(".");
373
374 drop(handle_completion_request(
375 &mut cx,
376 indoc! {"
377 one.|<>
378 two
379 three
380 "},
381 vec![],
382 ));
383 handle_copilot_completion_request(
384 &copilot_lsp,
385 vec![crate::request::Completion {
386 text: "one.copilot1".into(),
387 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
388 ..Default::default()
389 }],
390 vec![],
391 );
392 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
393 cx.update_editor(|editor, _, cx| {
394 assert!(!editor.context_menu_visible());
395 assert!(editor.has_active_edit_prediction());
396 // Since only the copilot is available, it's shown inline
397 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
398 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
399 });
400
401 // Ensure existing edit prediction is interpolated when inserting again.
402 cx.simulate_keystroke("c");
403 executor.run_until_parked();
404 cx.update_editor(|editor, _, cx| {
405 assert!(!editor.context_menu_visible());
406 assert!(editor.has_active_edit_prediction());
407 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
408 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
409 });
410
411 // After debouncing, new Copilot completions should be requested.
412 handle_copilot_completion_request(
413 &copilot_lsp,
414 vec![crate::request::Completion {
415 text: "one.copilot2".into(),
416 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
417 ..Default::default()
418 }],
419 vec![],
420 );
421 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
422 cx.update_editor(|editor, window, cx| {
423 assert!(!editor.context_menu_visible());
424 assert!(editor.has_active_edit_prediction());
425 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
426 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
427
428 // Canceling should remove the active Copilot suggestion.
429 editor.cancel(&Default::default(), window, cx);
430 assert!(!editor.has_active_edit_prediction());
431 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
432 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
433
434 // After canceling, tabbing shouldn't insert the previously shown suggestion.
435 editor.tab(&Default::default(), window, cx);
436 assert!(!editor.has_active_edit_prediction());
437 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
438 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
439
440 // When undoing the previously active suggestion is shown again.
441 editor.undo(&Default::default(), window, cx);
442 assert!(editor.has_active_edit_prediction());
443 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
444 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
445 });
446
447 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
448 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
449 cx.update_editor(|editor, window, cx| {
450 assert!(editor.has_active_edit_prediction());
451 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
452 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
453
454 // AcceptEditPrediction when there is an active suggestion inserts it.
455 editor.accept_edit_prediction(&Default::default(), window, cx);
456 assert!(!editor.has_active_edit_prediction());
457 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
458 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
459
460 // When undoing the previously active suggestion is shown again.
461 editor.undo(&Default::default(), window, cx);
462 assert!(editor.has_active_edit_prediction());
463 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
464 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
465
466 // Hide suggestion.
467 editor.cancel(&Default::default(), window, cx);
468 assert!(!editor.has_active_edit_prediction());
469 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
470 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
471 });
472
473 // If an edit occurs outside of this editor but no suggestion is being shown,
474 // we won't make it visible.
475 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
476 cx.update_editor(|editor, _, cx| {
477 assert!(!editor.has_active_edit_prediction());
478 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
479 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
480 });
481
482 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
483 cx.update_editor(|editor, window, cx| {
484 editor.set_text("fn foo() {\n \n}", window, cx);
485 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
486 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
487 });
488 });
489 handle_copilot_completion_request(
490 &copilot_lsp,
491 vec![crate::request::Completion {
492 text: " let x = 4;".into(),
493 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
494 ..Default::default()
495 }],
496 vec![],
497 );
498
499 cx.update_editor(|editor, window, cx| {
500 editor.next_edit_prediction(&Default::default(), window, cx)
501 });
502 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
503 cx.update_editor(|editor, window, cx| {
504 assert!(editor.has_active_edit_prediction());
505 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
506 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
507
508 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
509 editor.tab(&Default::default(), window, cx);
510 assert!(editor.has_active_edit_prediction());
511 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
512 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
513
514 // Using AcceptEditPrediction again accepts the suggestion.
515 editor.accept_edit_prediction(&Default::default(), window, cx);
516 assert!(!editor.has_active_edit_prediction());
517 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
518 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
519 });
520 }
521
522 #[gpui::test(iterations = 10)]
523 async fn test_accept_partial_copilot_suggestion(
524 executor: BackgroundExecutor,
525 cx: &mut TestAppContext,
526 ) {
527 // flaky
528 init_test(cx, |settings| {
529 settings.defaults.completions = Some(CompletionSettingsContent {
530 words: Some(WordsCompletionMode::Disabled),
531 words_min_length: Some(0),
532 lsp_insert_mode: Some(LspInsertMode::Insert),
533 ..Default::default()
534 });
535 });
536
537 let (copilot, copilot_lsp) = Copilot::fake(cx);
538 let mut cx = EditorLspTestContext::new_rust(
539 lsp::ServerCapabilities {
540 completion_provider: Some(lsp::CompletionOptions {
541 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
542 ..Default::default()
543 }),
544 ..Default::default()
545 },
546 cx,
547 )
548 .await;
549 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
550 cx.update_editor(|editor, window, cx| {
551 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
552 });
553
554 // Setup the editor with a completion request.
555 cx.set_state(indoc! {"
556 oneˇ
557 two
558 three
559 "});
560 cx.simulate_keystroke(".");
561 drop(handle_completion_request(
562 &mut cx,
563 indoc! {"
564 one.|<>
565 two
566 three
567 "},
568 vec![],
569 ));
570 handle_copilot_completion_request(
571 &copilot_lsp,
572 vec![crate::request::Completion {
573 text: "one.copilot1".into(),
574 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
575 ..Default::default()
576 }],
577 vec![],
578 );
579 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
580 cx.update_editor(|editor, window, cx| {
581 assert!(editor.has_active_edit_prediction());
582
583 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
584 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
585 assert!(editor.has_active_edit_prediction());
586 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
587 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
588
589 // Accepting next word should accept the non-word and copilot suggestion should be gone
590 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
591 assert!(!editor.has_active_edit_prediction());
592 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
593 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
594 });
595
596 // Reset the editor and check non-word and whitespace completion
597 cx.set_state(indoc! {"
598 oneˇ
599 two
600 three
601 "});
602 cx.simulate_keystroke(".");
603 drop(handle_completion_request(
604 &mut cx,
605 indoc! {"
606 one.|<>
607 two
608 three
609 "},
610 vec![],
611 ));
612 handle_copilot_completion_request(
613 &copilot_lsp,
614 vec![crate::request::Completion {
615 text: "one.123. copilot\n 456".into(),
616 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
617 ..Default::default()
618 }],
619 vec![],
620 );
621 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
622 cx.update_editor(|editor, window, cx| {
623 assert!(editor.has_active_edit_prediction());
624
625 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
626 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
627 assert!(editor.has_active_edit_prediction());
628 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
629 assert_eq!(
630 editor.display_text(cx),
631 "one.123. copilot\n 456\ntwo\nthree\n"
632 );
633
634 // Accepting next word should accept the next word and copilot suggestion should still exist
635 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
636 assert!(editor.has_active_edit_prediction());
637 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
638 assert_eq!(
639 editor.display_text(cx),
640 "one.123. copilot\n 456\ntwo\nthree\n"
641 );
642
643 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
644 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
645 assert!(!editor.has_active_edit_prediction());
646 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
647 assert_eq!(
648 editor.display_text(cx),
649 "one.123. copilot\n 456\ntwo\nthree\n"
650 );
651 });
652 }
653
654 #[gpui::test]
655 async fn test_copilot_completion_invalidation(
656 executor: BackgroundExecutor,
657 cx: &mut TestAppContext,
658 ) {
659 init_test(cx, |_| {});
660
661 let (copilot, copilot_lsp) = Copilot::fake(cx);
662 let mut cx = EditorLspTestContext::new_rust(
663 lsp::ServerCapabilities {
664 completion_provider: Some(lsp::CompletionOptions {
665 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
666 ..Default::default()
667 }),
668 ..Default::default()
669 },
670 cx,
671 )
672 .await;
673 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
674 cx.update_editor(|editor, window, cx| {
675 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
676 });
677
678 cx.set_state(indoc! {"
679 one
680 twˇ
681 three
682 "});
683
684 handle_copilot_completion_request(
685 &copilot_lsp,
686 vec![crate::request::Completion {
687 text: "two.foo()".into(),
688 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
689 ..Default::default()
690 }],
691 vec![],
692 );
693 cx.update_editor(|editor, window, cx| {
694 editor.next_edit_prediction(&Default::default(), window, cx)
695 });
696 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
697 cx.update_editor(|editor, window, cx| {
698 assert!(editor.has_active_edit_prediction());
699 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
700 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
701
702 editor.backspace(&Default::default(), window, cx);
703 assert!(editor.has_active_edit_prediction());
704 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
705 assert_eq!(editor.text(cx), "one\nt\nthree\n");
706
707 editor.backspace(&Default::default(), window, cx);
708 assert!(editor.has_active_edit_prediction());
709 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
710 assert_eq!(editor.text(cx), "one\n\nthree\n");
711
712 // Deleting across the original suggestion range invalidates it.
713 editor.backspace(&Default::default(), window, cx);
714 assert!(!editor.has_active_edit_prediction());
715 assert_eq!(editor.display_text(cx), "one\nthree\n");
716 assert_eq!(editor.text(cx), "one\nthree\n");
717
718 // Undoing the deletion restores the suggestion.
719 editor.undo(&Default::default(), window, cx);
720 assert!(editor.has_active_edit_prediction());
721 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
722 assert_eq!(editor.text(cx), "one\n\nthree\n");
723 });
724 }
725
726 #[gpui::test]
727 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
728 init_test(cx, |_| {});
729
730 let (copilot, copilot_lsp) = Copilot::fake(cx);
731
732 let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx));
733 let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx));
734 let multibuffer = cx.new(|cx| {
735 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
736 multibuffer.push_excerpts(
737 buffer_1.clone(),
738 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
739 cx,
740 );
741 multibuffer.push_excerpts(
742 buffer_2.clone(),
743 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
744 cx,
745 );
746 multibuffer
747 });
748 let editor =
749 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
750 editor
751 .update(cx, |editor, window, cx| {
752 use gpui::Focusable;
753 window.focus(&editor.focus_handle(cx));
754 })
755 .unwrap();
756 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
757 editor
758 .update(cx, |editor, window, cx| {
759 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
760 })
761 .unwrap();
762
763 handle_copilot_completion_request(
764 &copilot_lsp,
765 vec![crate::request::Completion {
766 text: "b = 2 + a".into(),
767 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
768 ..Default::default()
769 }],
770 vec![],
771 );
772 _ = editor.update(cx, |editor, window, cx| {
773 // Ensure copilot suggestions are shown for the first excerpt.
774 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
775 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
776 });
777 editor.next_edit_prediction(&Default::default(), window, cx);
778 });
779 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
780 _ = editor.update(cx, |editor, _, cx| {
781 assert!(editor.has_active_edit_prediction());
782 assert_eq!(
783 editor.display_text(cx),
784 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
785 );
786 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
787 });
788
789 handle_copilot_completion_request(
790 &copilot_lsp,
791 vec![crate::request::Completion {
792 text: "d = 4 + c".into(),
793 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
794 ..Default::default()
795 }],
796 vec![],
797 );
798 _ = editor.update(cx, |editor, window, cx| {
799 // Move to another excerpt, ensuring the suggestion gets cleared.
800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
801 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
802 });
803 assert!(!editor.has_active_edit_prediction());
804 assert_eq!(
805 editor.display_text(cx),
806 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
807 );
808 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
809
810 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
811 editor.handle_input(" ", window, cx);
812 assert!(!editor.has_active_edit_prediction());
813 assert_eq!(
814 editor.display_text(cx),
815 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
816 );
817 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
818 });
819
820 // Ensure the new suggestion is displayed when the debounce timeout expires.
821 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
822 _ = editor.update(cx, |editor, _, cx| {
823 assert!(editor.has_active_edit_prediction());
824 assert_eq!(
825 editor.display_text(cx),
826 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
827 );
828 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
829 });
830 }
831
832 #[gpui::test]
833 async fn test_copilot_does_not_prevent_completion_triggers(
834 executor: BackgroundExecutor,
835 cx: &mut TestAppContext,
836 ) {
837 init_test(cx, |_| {});
838
839 let (copilot, copilot_lsp) = Copilot::fake(cx);
840 let mut cx = EditorLspTestContext::new_rust(
841 lsp::ServerCapabilities {
842 completion_provider: Some(lsp::CompletionOptions {
843 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
844 ..lsp::CompletionOptions::default()
845 }),
846 ..lsp::ServerCapabilities::default()
847 },
848 cx,
849 )
850 .await;
851 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
852 cx.update_editor(|editor, window, cx| {
853 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
854 });
855
856 cx.set_state(indoc! {"
857 one
858 twˇ
859 three
860 "});
861
862 drop(handle_completion_request(
863 &mut cx,
864 indoc! {"
865 one
866 tw|<>
867 three
868 "},
869 vec!["completion_a", "completion_b"],
870 ));
871 handle_copilot_completion_request(
872 &copilot_lsp,
873 vec![crate::request::Completion {
874 text: "two.foo()".into(),
875 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
876 ..Default::default()
877 }],
878 vec![],
879 );
880 cx.update_editor(|editor, window, cx| {
881 editor.next_edit_prediction(&Default::default(), window, cx)
882 });
883 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
884 cx.update_editor(|editor, _, cx| {
885 assert!(!editor.context_menu_visible());
886 assert!(editor.has_active_edit_prediction());
887 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
888 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
889 });
890
891 cx.simulate_keystroke("o");
892 drop(handle_completion_request(
893 &mut cx,
894 indoc! {"
895 one
896 two|<>
897 three
898 "},
899 vec!["completion_a_2", "completion_b_2"],
900 ));
901 handle_copilot_completion_request(
902 &copilot_lsp,
903 vec![crate::request::Completion {
904 text: "two.foo()".into(),
905 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
906 ..Default::default()
907 }],
908 vec![],
909 );
910 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
911 cx.update_editor(|editor, _, cx| {
912 assert!(!editor.context_menu_visible());
913 assert!(editor.has_active_edit_prediction());
914 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
915 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
916 });
917
918 cx.simulate_keystroke(".");
919 drop(handle_completion_request(
920 &mut cx,
921 indoc! {"
922 one
923 two.|<>
924 three
925 "},
926 vec!["something_else()"],
927 ));
928 handle_copilot_completion_request(
929 &copilot_lsp,
930 vec![crate::request::Completion {
931 text: "two.foo()".into(),
932 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
933 ..Default::default()
934 }],
935 vec![],
936 );
937 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
938 cx.update_editor(|editor, _, cx| {
939 assert!(editor.context_menu_visible());
940 assert!(editor.has_active_edit_prediction());
941 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
942 assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
943 });
944 }
945
946 #[gpui::test]
947 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
948 init_test(cx, |settings| {
949 settings
950 .edit_predictions
951 .get_or_insert(Default::default())
952 .disabled_globs = Some(vec![".env*".to_string()]);
953 });
954
955 let (copilot, copilot_lsp) = Copilot::fake(cx);
956
957 let fs = FakeFs::new(cx.executor());
958 fs.insert_tree(
959 path!("/test"),
960 json!({
961 ".env": "SECRET=something\n",
962 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
963 }),
964 )
965 .await;
966 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
967
968 let private_buffer = project
969 .update(cx, |project, cx| {
970 project.open_local_buffer(path!("/test/.env"), cx)
971 })
972 .await
973 .unwrap();
974 let public_buffer = project
975 .update(cx, |project, cx| {
976 project.open_local_buffer(path!("/test/README.md"), cx)
977 })
978 .await
979 .unwrap();
980
981 let multibuffer = cx.new(|cx| {
982 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
983 multibuffer.push_excerpts(
984 private_buffer.clone(),
985 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
986 cx,
987 );
988 multibuffer.push_excerpts(
989 public_buffer.clone(),
990 [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
991 cx,
992 );
993 multibuffer
994 });
995 let editor =
996 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
997 editor
998 .update(cx, |editor, window, cx| {
999 use gpui::Focusable;
1000 window.focus(&editor.focus_handle(cx))
1001 })
1002 .unwrap();
1003 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
1004 editor
1005 .update(cx, |editor, window, cx| {
1006 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1007 })
1008 .unwrap();
1009
1010 let mut copilot_requests = copilot_lsp
1011 .set_request_handler::<crate::request::GetCompletions, _, _>(
1012 move |_params, _cx| async move {
1013 Ok(crate::request::GetCompletionsResult {
1014 completions: vec![crate::request::Completion {
1015 text: "next line".into(),
1016 range: lsp::Range::new(
1017 lsp::Position::new(1, 0),
1018 lsp::Position::new(1, 0),
1019 ),
1020 ..Default::default()
1021 }],
1022 })
1023 },
1024 );
1025
1026 _ = editor.update(cx, |editor, window, cx| {
1027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1028 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1029 });
1030 editor.refresh_edit_prediction(true, false, window, cx);
1031 });
1032
1033 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1034 assert!(copilot_requests.try_next().is_err());
1035
1036 _ = editor.update(cx, |editor, window, cx| {
1037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1038 s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1039 });
1040 editor.refresh_edit_prediction(true, false, window, cx);
1041 });
1042
1043 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1044 assert!(copilot_requests.try_next().is_ok());
1045 }
1046
1047 fn handle_copilot_completion_request(
1048 lsp: &lsp::FakeLanguageServer,
1049 completions: Vec<crate::request::Completion>,
1050 completions_cycling: Vec<crate::request::Completion>,
1051 ) {
1052 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1053 let completions = completions.clone();
1054 async move {
1055 Ok(crate::request::GetCompletionsResult {
1056 completions: completions.clone(),
1057 })
1058 }
1059 });
1060 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1061 move |_params, _cx| {
1062 let completions_cycling = completions_cycling.clone();
1063 async move {
1064 Ok(crate::request::GetCompletionsResult {
1065 completions: completions_cycling.clone(),
1066 })
1067 }
1068 },
1069 );
1070 }
1071
1072 fn handle_completion_request(
1073 cx: &mut EditorLspTestContext,
1074 marked_string: &str,
1075 completions: Vec<&'static str>,
1076 ) -> impl Future<Output = ()> {
1077 let complete_from_marker: TextRangeMarker = '|'.into();
1078 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1079 let (_, mut marked_ranges) = marked_text_ranges_by(
1080 marked_string,
1081 vec![complete_from_marker, replace_range_marker.clone()],
1082 );
1083
1084 let replace_range =
1085 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1086
1087 let mut request =
1088 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1089 let completions = completions.clone();
1090 async move {
1091 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1092 Ok(Some(lsp::CompletionResponse::Array(
1093 completions
1094 .iter()
1095 .map(|completion_text| lsp::CompletionItem {
1096 label: completion_text.to_string(),
1097 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1098 range: replace_range,
1099 new_text: completion_text.to_string(),
1100 })),
1101 ..Default::default()
1102 })
1103 .collect(),
1104 )))
1105 }
1106 });
1107
1108 async move {
1109 request.next().await;
1110 }
1111 }
1112
1113 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1114 cx.update(|cx| {
1115 let store = SettingsStore::test(cx);
1116 cx.set_global(store);
1117 theme::init(theme::LoadThemes::JustBase, cx);
1118 client::init_settings(cx);
1119 language::init(cx);
1120 editor::init_settings(cx);
1121 Project::init_settings(cx);
1122 workspace::init_settings(cx);
1123 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1124 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
1125 });
1126 });
1127 }
1128}