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