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