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::{
285 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LspInsertMode,
286 WordsCompletionMode,
287 },
288 };
289 use project::Project;
290 use serde_json::json;
291 use settings::SettingsStore;
292 use std::future::Future;
293 use util::{
294 path,
295 test::{TextRangeMarker, marked_text_ranges_by},
296 };
297
298 #[gpui::test(iterations = 10)]
299 async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
300 // flaky
301 init_test(cx, |settings| {
302 settings.defaults.completions = Some(CompletionSettings {
303 words: WordsCompletionMode::Disabled,
304 words_min_length: 0,
305 lsp: true,
306 lsp_fetch_timeout_ms: 0,
307 lsp_insert_mode: LspInsertMode::Insert,
308 });
309 });
310
311 let (copilot, copilot_lsp) = Copilot::fake(cx);
312 let mut cx = EditorLspTestContext::new_rust(
313 lsp::ServerCapabilities {
314 completion_provider: Some(lsp::CompletionOptions {
315 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
316 ..Default::default()
317 }),
318 ..Default::default()
319 },
320 cx,
321 )
322 .await;
323 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
324 cx.update_editor(|editor, window, cx| {
325 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
326 });
327
328 cx.set_state(indoc! {"
329 oneˇ
330 two
331 three
332 "});
333 cx.simulate_keystroke(".");
334 drop(handle_completion_request(
335 &mut cx,
336 indoc! {"
337 one.|<>
338 two
339 three
340 "},
341 vec!["completion_a", "completion_b"],
342 ));
343 handle_copilot_completion_request(
344 &copilot_lsp,
345 vec![crate::request::Completion {
346 text: "one.copilot1".into(),
347 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
348 ..Default::default()
349 }],
350 vec![],
351 );
352 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
353 cx.update_editor(|editor, window, cx| {
354 assert!(editor.context_menu_visible());
355 assert!(editor.has_active_edit_prediction());
356 // Since we have both, the copilot suggestion is existing but does not show up as ghost text
357 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
358 assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
359
360 // Confirming a non-copilot completion inserts it and hides the context menu, without showing
361 // the copilot suggestion afterwards.
362 editor
363 .confirm_completion(&Default::default(), window, cx)
364 .unwrap()
365 .detach();
366 assert!(!editor.context_menu_visible());
367 assert!(!editor.has_active_edit_prediction());
368 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
369 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
370 });
371
372 // Reset editor and only return copilot suggestions
373 cx.set_state(indoc! {"
374 oneˇ
375 two
376 three
377 "});
378 cx.simulate_keystroke(".");
379
380 drop(handle_completion_request(
381 &mut cx,
382 indoc! {"
383 one.|<>
384 two
385 three
386 "},
387 vec![],
388 ));
389 handle_copilot_completion_request(
390 &copilot_lsp,
391 vec![crate::request::Completion {
392 text: "one.copilot1".into(),
393 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
394 ..Default::default()
395 }],
396 vec![],
397 );
398 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
399 cx.update_editor(|editor, _, cx| {
400 assert!(!editor.context_menu_visible());
401 assert!(editor.has_active_edit_prediction());
402 // Since only the copilot is available, it's shown inline
403 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
404 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
405 });
406
407 // Ensure existing edit prediction is interpolated when inserting again.
408 cx.simulate_keystroke("c");
409 executor.run_until_parked();
410 cx.update_editor(|editor, _, cx| {
411 assert!(!editor.context_menu_visible());
412 assert!(editor.has_active_edit_prediction());
413 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
414 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
415 });
416
417 // After debouncing, new Copilot completions should be requested.
418 handle_copilot_completion_request(
419 &copilot_lsp,
420 vec![crate::request::Completion {
421 text: "one.copilot2".into(),
422 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
423 ..Default::default()
424 }],
425 vec![],
426 );
427 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
428 cx.update_editor(|editor, window, cx| {
429 assert!(!editor.context_menu_visible());
430 assert!(editor.has_active_edit_prediction());
431 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
432 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
433
434 // Canceling should remove the active Copilot suggestion.
435 editor.cancel(&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 // After canceling, tabbing shouldn't insert the previously shown suggestion.
441 editor.tab(&Default::default(), window, cx);
442 assert!(!editor.has_active_edit_prediction());
443 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
444 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
445
446 // When undoing the previously active suggestion is shown again.
447 editor.undo(&Default::default(), window, cx);
448 assert!(editor.has_active_edit_prediction());
449 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
450 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
451 });
452
453 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
454 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
455 cx.update_editor(|editor, 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.co\ntwo\nthree\n");
459
460 // AcceptEditPrediction when there is an active suggestion inserts it.
461 editor.accept_edit_prediction(&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.copilot2\ntwo\nthree\n");
465
466 // When undoing the previously active suggestion is shown again.
467 editor.undo(&Default::default(), window, cx);
468 assert!(editor.has_active_edit_prediction());
469 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
470 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
471
472 // Hide suggestion.
473 editor.cancel(&Default::default(), window, cx);
474 assert!(!editor.has_active_edit_prediction());
475 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
476 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
477 });
478
479 // If an edit occurs outside of this editor but no suggestion is being shown,
480 // we won't make it visible.
481 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
482 cx.update_editor(|editor, _, cx| {
483 assert!(!editor.has_active_edit_prediction());
484 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
485 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
486 });
487
488 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
489 cx.update_editor(|editor, window, cx| {
490 editor.set_text("fn foo() {\n \n}", window, cx);
491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
492 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
493 });
494 });
495 handle_copilot_completion_request(
496 &copilot_lsp,
497 vec![crate::request::Completion {
498 text: " let x = 4;".into(),
499 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
500 ..Default::default()
501 }],
502 vec![],
503 );
504
505 cx.update_editor(|editor, window, cx| {
506 editor.next_edit_prediction(&Default::default(), window, cx)
507 });
508 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
509 cx.update_editor(|editor, window, cx| {
510 assert!(editor.has_active_edit_prediction());
511 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
512 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
513
514 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
515 editor.tab(&Default::default(), window, cx);
516 assert!(editor.has_active_edit_prediction());
517 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
518 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
519
520 // Using AcceptEditPrediction again accepts the suggestion.
521 editor.accept_edit_prediction(&Default::default(), window, cx);
522 assert!(!editor.has_active_edit_prediction());
523 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
524 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
525 });
526 }
527
528 #[gpui::test(iterations = 10)]
529 async fn test_accept_partial_copilot_suggestion(
530 executor: BackgroundExecutor,
531 cx: &mut TestAppContext,
532 ) {
533 // flaky
534 init_test(cx, |settings| {
535 settings.defaults.completions = Some(CompletionSettings {
536 words: WordsCompletionMode::Disabled,
537 words_min_length: 0,
538 lsp: true,
539 lsp_fetch_timeout_ms: 0,
540 lsp_insert_mode: LspInsertMode::Insert,
541 });
542 });
543
544 let (copilot, copilot_lsp) = Copilot::fake(cx);
545 let mut cx = EditorLspTestContext::new_rust(
546 lsp::ServerCapabilities {
547 completion_provider: Some(lsp::CompletionOptions {
548 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
549 ..Default::default()
550 }),
551 ..Default::default()
552 },
553 cx,
554 )
555 .await;
556 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
557 cx.update_editor(|editor, window, cx| {
558 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
559 });
560
561 // Setup the editor with a completion request.
562 cx.set_state(indoc! {"
563 oneˇ
564 two
565 three
566 "});
567 cx.simulate_keystroke(".");
568 drop(handle_completion_request(
569 &mut cx,
570 indoc! {"
571 one.|<>
572 two
573 three
574 "},
575 vec![],
576 ));
577 handle_copilot_completion_request(
578 &copilot_lsp,
579 vec![crate::request::Completion {
580 text: "one.copilot1".into(),
581 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
582 ..Default::default()
583 }],
584 vec![],
585 );
586 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
587 cx.update_editor(|editor, window, cx| {
588 assert!(editor.has_active_edit_prediction());
589
590 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
591 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
592 assert!(editor.has_active_edit_prediction());
593 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
594 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
595
596 // Accepting next word should accept the non-word and copilot suggestion should be gone
597 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
598 assert!(!editor.has_active_edit_prediction());
599 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
600 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
601 });
602
603 // Reset the editor and check non-word and whitespace completion
604 cx.set_state(indoc! {"
605 oneˇ
606 two
607 three
608 "});
609 cx.simulate_keystroke(".");
610 drop(handle_completion_request(
611 &mut cx,
612 indoc! {"
613 one.|<>
614 two
615 three
616 "},
617 vec![],
618 ));
619 handle_copilot_completion_request(
620 &copilot_lsp,
621 vec![crate::request::Completion {
622 text: "one.123. copilot\n 456".into(),
623 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
624 ..Default::default()
625 }],
626 vec![],
627 );
628 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
629 cx.update_editor(|editor, window, cx| {
630 assert!(editor.has_active_edit_prediction());
631
632 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
633 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
634 assert!(editor.has_active_edit_prediction());
635 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
636 assert_eq!(
637 editor.display_text(cx),
638 "one.123. copilot\n 456\ntwo\nthree\n"
639 );
640
641 // Accepting next word should accept the next word and copilot suggestion should still exist
642 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
643 assert!(editor.has_active_edit_prediction());
644 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
645 assert_eq!(
646 editor.display_text(cx),
647 "one.123. copilot\n 456\ntwo\nthree\n"
648 );
649
650 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
651 editor.accept_partial_edit_prediction(&Default::default(), window, cx);
652 assert!(!editor.has_active_edit_prediction());
653 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
654 assert_eq!(
655 editor.display_text(cx),
656 "one.123. copilot\n 456\ntwo\nthree\n"
657 );
658 });
659 }
660
661 #[gpui::test]
662 async fn test_copilot_completion_invalidation(
663 executor: BackgroundExecutor,
664 cx: &mut TestAppContext,
665 ) {
666 init_test(cx, |_| {});
667
668 let (copilot, copilot_lsp) = Copilot::fake(cx);
669 let mut cx = EditorLspTestContext::new_rust(
670 lsp::ServerCapabilities {
671 completion_provider: Some(lsp::CompletionOptions {
672 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
673 ..Default::default()
674 }),
675 ..Default::default()
676 },
677 cx,
678 )
679 .await;
680 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
681 cx.update_editor(|editor, window, cx| {
682 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
683 });
684
685 cx.set_state(indoc! {"
686 one
687 twˇ
688 three
689 "});
690
691 handle_copilot_completion_request(
692 &copilot_lsp,
693 vec![crate::request::Completion {
694 text: "two.foo()".into(),
695 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
696 ..Default::default()
697 }],
698 vec![],
699 );
700 cx.update_editor(|editor, window, cx| {
701 editor.next_edit_prediction(&Default::default(), window, cx)
702 });
703 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
704 cx.update_editor(|editor, 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\ntw\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\nt\nthree\n");
713
714 editor.backspace(&Default::default(), window, cx);
715 assert!(editor.has_active_edit_prediction());
716 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
717 assert_eq!(editor.text(cx), "one\n\nthree\n");
718
719 // Deleting across the original suggestion range invalidates it.
720 editor.backspace(&Default::default(), window, cx);
721 assert!(!editor.has_active_edit_prediction());
722 assert_eq!(editor.display_text(cx), "one\nthree\n");
723 assert_eq!(editor.text(cx), "one\nthree\n");
724
725 // Undoing the deletion restores the suggestion.
726 editor.undo(&Default::default(), window, cx);
727 assert!(editor.has_active_edit_prediction());
728 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
729 assert_eq!(editor.text(cx), "one\n\nthree\n");
730 });
731 }
732
733 #[gpui::test]
734 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
735 init_test(cx, |_| {});
736
737 let (copilot, copilot_lsp) = Copilot::fake(cx);
738
739 let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx));
740 let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx));
741 let multibuffer = cx.new(|cx| {
742 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
743 multibuffer.push_excerpts(
744 buffer_1.clone(),
745 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
746 cx,
747 );
748 multibuffer.push_excerpts(
749 buffer_2.clone(),
750 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
751 cx,
752 );
753 multibuffer
754 });
755 let editor =
756 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
757 editor
758 .update(cx, |editor, window, cx| {
759 use gpui::Focusable;
760 window.focus(&editor.focus_handle(cx));
761 })
762 .unwrap();
763 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
764 editor
765 .update(cx, |editor, window, cx| {
766 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
767 })
768 .unwrap();
769
770 handle_copilot_completion_request(
771 &copilot_lsp,
772 vec![crate::request::Completion {
773 text: "b = 2 + a".into(),
774 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
775 ..Default::default()
776 }],
777 vec![],
778 );
779 _ = editor.update(cx, |editor, window, cx| {
780 // Ensure copilot suggestions are shown for the first excerpt.
781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
782 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
783 });
784 editor.next_edit_prediction(&Default::default(), window, cx);
785 });
786 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
787 _ = editor.update(cx, |editor, _, cx| {
788 assert!(editor.has_active_edit_prediction());
789 assert_eq!(
790 editor.display_text(cx),
791 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
792 );
793 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
794 });
795
796 handle_copilot_completion_request(
797 &copilot_lsp,
798 vec![crate::request::Completion {
799 text: "d = 4 + c".into(),
800 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
801 ..Default::default()
802 }],
803 vec![],
804 );
805 _ = editor.update(cx, |editor, window, cx| {
806 // Move to another excerpt, ensuring the suggestion gets cleared.
807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
808 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
809 });
810 assert!(!editor.has_active_edit_prediction());
811 assert_eq!(
812 editor.display_text(cx),
813 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
814 );
815 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
816
817 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
818 editor.handle_input(" ", window, cx);
819 assert!(!editor.has_active_edit_prediction());
820 assert_eq!(
821 editor.display_text(cx),
822 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
823 );
824 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
825 });
826
827 // Ensure the new suggestion is displayed when the debounce timeout expires.
828 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
829 _ = editor.update(cx, |editor, _, cx| {
830 assert!(editor.has_active_edit_prediction());
831 assert_eq!(
832 editor.display_text(cx),
833 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
834 );
835 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
836 });
837 }
838
839 #[gpui::test]
840 async fn test_copilot_does_not_prevent_completion_triggers(
841 executor: BackgroundExecutor,
842 cx: &mut TestAppContext,
843 ) {
844 init_test(cx, |_| {});
845
846 let (copilot, copilot_lsp) = Copilot::fake(cx);
847 let mut cx = EditorLspTestContext::new_rust(
848 lsp::ServerCapabilities {
849 completion_provider: Some(lsp::CompletionOptions {
850 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
851 ..lsp::CompletionOptions::default()
852 }),
853 ..lsp::ServerCapabilities::default()
854 },
855 cx,
856 )
857 .await;
858 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
859 cx.update_editor(|editor, window, cx| {
860 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
861 });
862
863 cx.set_state(indoc! {"
864 one
865 twˇ
866 three
867 "});
868
869 drop(handle_completion_request(
870 &mut cx,
871 indoc! {"
872 one
873 tw|<>
874 three
875 "},
876 vec!["completion_a", "completion_b"],
877 ));
878 handle_copilot_completion_request(
879 &copilot_lsp,
880 vec![crate::request::Completion {
881 text: "two.foo()".into(),
882 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
883 ..Default::default()
884 }],
885 vec![],
886 );
887 cx.update_editor(|editor, window, cx| {
888 editor.next_edit_prediction(&Default::default(), window, cx)
889 });
890 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
891 cx.update_editor(|editor, _, cx| {
892 assert!(!editor.context_menu_visible());
893 assert!(editor.has_active_edit_prediction());
894 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
895 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
896 });
897
898 cx.simulate_keystroke("o");
899 drop(handle_completion_request(
900 &mut cx,
901 indoc! {"
902 one
903 two|<>
904 three
905 "},
906 vec!["completion_a_2", "completion_b_2"],
907 ));
908 handle_copilot_completion_request(
909 &copilot_lsp,
910 vec![crate::request::Completion {
911 text: "two.foo()".into(),
912 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
913 ..Default::default()
914 }],
915 vec![],
916 );
917 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
918 cx.update_editor(|editor, _, cx| {
919 assert!(!editor.context_menu_visible());
920 assert!(editor.has_active_edit_prediction());
921 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
922 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
923 });
924
925 cx.simulate_keystroke(".");
926 drop(handle_completion_request(
927 &mut cx,
928 indoc! {"
929 one
930 two.|<>
931 three
932 "},
933 vec!["something_else()"],
934 ));
935 handle_copilot_completion_request(
936 &copilot_lsp,
937 vec![crate::request::Completion {
938 text: "two.foo()".into(),
939 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
940 ..Default::default()
941 }],
942 vec![],
943 );
944 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
945 cx.update_editor(|editor, _, cx| {
946 assert!(editor.context_menu_visible());
947 assert!(editor.has_active_edit_prediction());
948 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
949 assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
950 });
951 }
952
953 #[gpui::test]
954 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
955 init_test(cx, |settings| {
956 settings
957 .edit_predictions
958 .get_or_insert(Default::default())
959 .disabled_globs = Some(vec![".env*".to_string()]);
960 });
961
962 let (copilot, copilot_lsp) = Copilot::fake(cx);
963
964 let fs = FakeFs::new(cx.executor());
965 fs.insert_tree(
966 path!("/test"),
967 json!({
968 ".env": "SECRET=something\n",
969 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
970 }),
971 )
972 .await;
973 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
974
975 let private_buffer = project
976 .update(cx, |project, cx| {
977 project.open_local_buffer(path!("/test/.env"), cx)
978 })
979 .await
980 .unwrap();
981 let public_buffer = project
982 .update(cx, |project, cx| {
983 project.open_local_buffer(path!("/test/README.md"), cx)
984 })
985 .await
986 .unwrap();
987
988 let multibuffer = cx.new(|cx| {
989 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
990 multibuffer.push_excerpts(
991 private_buffer.clone(),
992 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
993 cx,
994 );
995 multibuffer.push_excerpts(
996 public_buffer.clone(),
997 [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
998 cx,
999 );
1000 multibuffer
1001 });
1002 let editor =
1003 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
1004 editor
1005 .update(cx, |editor, window, cx| {
1006 use gpui::Focusable;
1007 window.focus(&editor.focus_handle(cx))
1008 })
1009 .unwrap();
1010 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
1011 editor
1012 .update(cx, |editor, window, cx| {
1013 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1014 })
1015 .unwrap();
1016
1017 let mut copilot_requests = copilot_lsp
1018 .set_request_handler::<crate::request::GetCompletions, _, _>(
1019 move |_params, _cx| async move {
1020 Ok(crate::request::GetCompletionsResult {
1021 completions: vec![crate::request::Completion {
1022 text: "next line".into(),
1023 range: lsp::Range::new(
1024 lsp::Position::new(1, 0),
1025 lsp::Position::new(1, 0),
1026 ),
1027 ..Default::default()
1028 }],
1029 })
1030 },
1031 );
1032
1033 _ = editor.update(cx, |editor, window, cx| {
1034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1035 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1036 });
1037 editor.refresh_edit_prediction(true, false, window, cx);
1038 });
1039
1040 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1041 assert!(copilot_requests.try_next().is_err());
1042
1043 _ = editor.update(cx, |editor, window, cx| {
1044 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1045 s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1046 });
1047 editor.refresh_edit_prediction(true, false, window, cx);
1048 });
1049
1050 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1051 assert!(copilot_requests.try_next().is_ok());
1052 }
1053
1054 fn handle_copilot_completion_request(
1055 lsp: &lsp::FakeLanguageServer,
1056 completions: Vec<crate::request::Completion>,
1057 completions_cycling: Vec<crate::request::Completion>,
1058 ) {
1059 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1060 let completions = completions.clone();
1061 async move {
1062 Ok(crate::request::GetCompletionsResult {
1063 completions: completions.clone(),
1064 })
1065 }
1066 });
1067 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1068 move |_params, _cx| {
1069 let completions_cycling = completions_cycling.clone();
1070 async move {
1071 Ok(crate::request::GetCompletionsResult {
1072 completions: completions_cycling.clone(),
1073 })
1074 }
1075 },
1076 );
1077 }
1078
1079 fn handle_completion_request(
1080 cx: &mut EditorLspTestContext,
1081 marked_string: &str,
1082 completions: Vec<&'static str>,
1083 ) -> impl Future<Output = ()> {
1084 let complete_from_marker: TextRangeMarker = '|'.into();
1085 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1086 let (_, mut marked_ranges) = marked_text_ranges_by(
1087 marked_string,
1088 vec![complete_from_marker, replace_range_marker.clone()],
1089 );
1090
1091 let replace_range =
1092 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1093
1094 let mut request =
1095 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1096 let completions = completions.clone();
1097 async move {
1098 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1099 Ok(Some(lsp::CompletionResponse::Array(
1100 completions
1101 .iter()
1102 .map(|completion_text| lsp::CompletionItem {
1103 label: completion_text.to_string(),
1104 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1105 range: replace_range,
1106 new_text: completion_text.to_string(),
1107 })),
1108 ..Default::default()
1109 })
1110 .collect(),
1111 )))
1112 }
1113 });
1114
1115 async move {
1116 request.next().await;
1117 }
1118 }
1119
1120 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1121 cx.update(|cx| {
1122 let store = SettingsStore::test(cx);
1123 cx.set_global(store);
1124 theme::init(theme::LoadThemes::JustBase, cx);
1125 client::init_settings(cx);
1126 language::init(cx);
1127 editor::init_settings(cx);
1128 Project::init_settings(cx);
1129 workspace::init_settings(cx);
1130 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1131 store.update_user_settings::<AllLanguageSettings>(cx, f);
1132 });
1133 });
1134 }
1135}