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