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