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