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