1use anyhow::Result;
2use client::telemetry::Telemetry;
3use copilot::Copilot;
4use editor::{Direction, InlineCompletionProvider};
5use gpui::{AppContext, EntityId, Model, ModelContext, Task};
6use language::language_settings::AllLanguageSettings;
7use language::{language_settings::all_language_settings, Buffer, OffsetRangeExt, ToOffset};
8use settings::Settings;
9use std::{path::Path, sync::Arc, time::Duration};
10
11pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
12
13pub struct CopilotCompletionProvider {
14 cycled: bool,
15 buffer_id: Option<EntityId>,
16 completions: Vec<copilot::Completion>,
17 active_completion_index: usize,
18 file_extension: Option<String>,
19 pending_refresh: Task<Result<()>>,
20 pending_cycling_refresh: Task<Result<()>>,
21 copilot: Model<Copilot>,
22 telemetry: Option<Arc<Telemetry>>,
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 telemetry: None,
37 }
38 }
39
40 pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
41 self.telemetry = Some(telemetry);
42 self
43 }
44
45 fn active_completion(&self) -> Option<&copilot::Completion> {
46 self.completions.get(self.active_completion_index)
47 }
48
49 fn push_completion(&mut self, new_completion: copilot::Completion) {
50 for completion in &self.completions {
51 if completion.text == new_completion.text && completion.range == new_completion.range {
52 return;
53 }
54 }
55 self.completions.push(new_completion);
56 }
57}
58
59impl InlineCompletionProvider for CopilotCompletionProvider {
60 fn is_enabled(
61 &self,
62 buffer: &Model<Buffer>,
63 cursor_position: language::Anchor,
64 cx: &AppContext,
65 ) -> bool {
66 if !self.copilot.read(cx).status().is_authorized() {
67 return false;
68 }
69
70 let buffer = buffer.read(cx);
71 let file = buffer.file();
72 let language = buffer.language_at(cursor_position);
73 let settings = all_language_settings(file, cx);
74 settings.copilot_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
75 }
76
77 fn refresh(
78 &mut self,
79 buffer: Model<Buffer>,
80 cursor_position: language::Anchor,
81 debounce: bool,
82 cx: &mut ModelContext<Self>,
83 ) {
84 let copilot = self.copilot.clone();
85 self.pending_refresh = cx.spawn(|this, mut cx| async move {
86 if debounce {
87 cx.background_executor()
88 .timer(COPILOT_DEBOUNCE_TIMEOUT)
89 .await;
90 }
91
92 let completions = copilot
93 .update(&mut cx, |copilot, cx| {
94 copilot.completions(&buffer, cursor_position, cx)
95 })?
96 .await?;
97
98 this.update(&mut cx, |this, cx| {
99 if !completions.is_empty() {
100 this.cycled = false;
101 this.pending_cycling_refresh = Task::ready(Ok(()));
102 this.completions.clear();
103 this.active_completion_index = 0;
104 this.buffer_id = Some(buffer.entity_id());
105 this.file_extension = buffer.read(cx).file().and_then(|file| {
106 Some(
107 Path::new(file.file_name(cx))
108 .extension()?
109 .to_str()?
110 .to_string(),
111 )
112 });
113
114 for completion in completions {
115 this.push_completion(completion);
116 }
117 cx.notify();
118 }
119 })?;
120
121 Ok(())
122 });
123 }
124
125 fn cycle(
126 &mut self,
127 buffer: Model<Buffer>,
128 cursor_position: language::Anchor,
129 direction: Direction,
130 cx: &mut ModelContext<Self>,
131 ) {
132 if self.cycled {
133 match direction {
134 Direction::Prev => {
135 self.active_completion_index = if self.active_completion_index == 0 {
136 self.completions.len().saturating_sub(1)
137 } else {
138 self.active_completion_index - 1
139 };
140 }
141 Direction::Next => {
142 if self.completions.len() == 0 {
143 self.active_completion_index = 0
144 } else {
145 self.active_completion_index =
146 (self.active_completion_index + 1) % self.completions.len();
147 }
148 }
149 }
150
151 cx.notify();
152 } else {
153 let copilot = self.copilot.clone();
154 self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
155 let completions = copilot
156 .update(&mut cx, |copilot, cx| {
157 copilot.completions_cycling(&buffer, cursor_position, cx)
158 })?
159 .await?;
160
161 this.update(&mut cx, |this, cx| {
162 this.cycled = true;
163 this.file_extension = buffer.read(cx).file().and_then(|file| {
164 Some(
165 Path::new(file.file_name(cx))
166 .extension()?
167 .to_str()?
168 .to_string(),
169 )
170 });
171 for completion in completions {
172 this.push_completion(completion);
173 }
174 this.cycle(buffer, cursor_position, direction, cx);
175 })?;
176
177 Ok(())
178 });
179 }
180 }
181
182 fn accept(&mut self, cx: &mut ModelContext<Self>) {
183 if let Some(completion) = self.active_completion() {
184 self.copilot
185 .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
186 .detach_and_log_err(cx);
187 if let Some(telemetry) = self.telemetry.as_ref() {
188 telemetry.report_copilot_event(
189 Some(completion.uuid.clone()),
190 true,
191 self.file_extension.clone(),
192 );
193 }
194 }
195 }
196
197 fn discard(&mut self, cx: &mut ModelContext<Self>) {
198 let settings = AllLanguageSettings::get_global(cx);
199 if !settings.copilot.feature_enabled {
200 return;
201 }
202
203 self.copilot
204 .update(cx, |copilot, cx| {
205 copilot.discard_completions(&self.completions, cx)
206 })
207 .detach_and_log_err(cx);
208 if let Some(telemetry) = self.telemetry.as_ref() {
209 telemetry.report_copilot_event(None, false, self.file_extension.clone());
210 }
211 }
212
213 fn active_completion_text(
214 &self,
215 buffer: &Model<Buffer>,
216 cursor_position: language::Anchor,
217 cx: &AppContext,
218 ) -> Option<&str> {
219 let buffer_id = buffer.entity_id();
220 let buffer = buffer.read(cx);
221 let completion = self.active_completion()?;
222 if Some(buffer_id) != self.buffer_id
223 || !completion.range.start.is_valid(buffer)
224 || !completion.range.end.is_valid(buffer)
225 {
226 return None;
227 }
228
229 let mut completion_range = completion.range.to_offset(buffer);
230 let prefix_len = common_prefix(
231 buffer.chars_for_range(completion_range.clone()),
232 completion.text.chars(),
233 );
234 completion_range.start += prefix_len;
235 let suffix_len = common_prefix(
236 buffer.reversed_chars_for_range(completion_range.clone()),
237 completion.text[prefix_len..].chars().rev(),
238 );
239 completion_range.end = completion_range.end.saturating_sub(suffix_len);
240
241 if completion_range.is_empty()
242 && completion_range.start == cursor_position.to_offset(buffer)
243 {
244 let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
245 if completion_text.trim().is_empty() {
246 None
247 } else {
248 Some(completion_text)
249 }
250 } else {
251 None
252 }
253 }
254}
255
256fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
257 a.zip(b)
258 .take_while(|(a, b)| a == b)
259 .map(|(a, _)| a.len_utf8())
260 .sum()
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use editor::{
267 test::editor_lsp_test_context::EditorLspTestContext, Editor, ExcerptRange, MultiBuffer,
268 };
269 use fs::FakeFs;
270 use futures::StreamExt;
271 use gpui::{BackgroundExecutor, BorrowAppContext, Context, TestAppContext};
272 use indoc::indoc;
273 use language::{
274 language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
275 Point,
276 };
277 use project::Project;
278 use serde_json::json;
279 use settings::SettingsStore;
280 use std::future::Future;
281 use util::test::{marked_text_ranges_by, TextRangeMarker};
282
283 #[gpui::test(iterations = 10)]
284 async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
285 // flaky
286 init_test(cx, |_| {});
287
288 let (copilot, copilot_lsp) = Copilot::fake(cx);
289 let mut cx = EditorLspTestContext::new_rust(
290 lsp::ServerCapabilities {
291 completion_provider: Some(lsp::CompletionOptions {
292 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
293 ..Default::default()
294 }),
295 ..Default::default()
296 },
297 cx,
298 )
299 .await;
300 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
301 cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
302
303 // When inserting, ensure autocompletion is favored over Copilot suggestions.
304 cx.set_state(indoc! {"
305 oneˇ
306 two
307 three
308 "});
309 cx.simulate_keystroke(".");
310 let _ = handle_completion_request(
311 &mut cx,
312 indoc! {"
313 one.|<>
314 two
315 three
316 "},
317 vec!["completion_a", "completion_b"],
318 );
319 handle_copilot_completion_request(
320 &copilot_lsp,
321 vec![copilot::request::Completion {
322 text: "one.copilot1".into(),
323 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
324 ..Default::default()
325 }],
326 vec![],
327 );
328 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
329 cx.update_editor(|editor, cx| {
330 assert!(editor.context_menu_visible());
331 assert!(!editor.has_active_inline_completion(cx));
332
333 // Confirming a completion inserts it and hides the context menu, without showing
334 // the copilot suggestion afterwards.
335 editor
336 .confirm_completion(&Default::default(), cx)
337 .unwrap()
338 .detach();
339 assert!(!editor.context_menu_visible());
340 assert!(!editor.has_active_inline_completion(cx));
341 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
342 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
343 });
344
345 // Ensure Copilot suggestions are shown right away if no autocompletion is available.
346 cx.set_state(indoc! {"
347 oneˇ
348 two
349 three
350 "});
351 cx.simulate_keystroke(".");
352 let _ = handle_completion_request(
353 &mut cx,
354 indoc! {"
355 one.|<>
356 two
357 three
358 "},
359 vec![],
360 );
361 handle_copilot_completion_request(
362 &copilot_lsp,
363 vec![copilot::request::Completion {
364 text: "one.copilot1".into(),
365 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
366 ..Default::default()
367 }],
368 vec![],
369 );
370 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
371 cx.update_editor(|editor, cx| {
372 assert!(!editor.context_menu_visible());
373 assert!(editor.has_active_inline_completion(cx));
374 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
375 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
376 });
377
378 // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
379 cx.set_state(indoc! {"
380 oneˇ
381 two
382 three
383 "});
384 cx.simulate_keystroke(".");
385 let _ = handle_completion_request(
386 &mut cx,
387 indoc! {"
388 one.|<>
389 two
390 three
391 "},
392 vec!["completion_a", "completion_b"],
393 );
394 handle_copilot_completion_request(
395 &copilot_lsp,
396 vec![copilot::request::Completion {
397 text: "one.copilot1".into(),
398 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
399 ..Default::default()
400 }],
401 vec![],
402 );
403 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
404 cx.update_editor(|editor, cx| {
405 assert!(editor.context_menu_visible());
406 assert!(!editor.has_active_inline_completion(cx));
407
408 // When hiding the context menu, the Copilot suggestion becomes visible.
409 editor.cancel(&Default::default(), cx);
410 assert!(!editor.context_menu_visible());
411 assert!(editor.has_active_inline_completion(cx));
412 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
413 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
414 });
415
416 // Ensure existing completion is interpolated when inserting again.
417 cx.simulate_keystroke("c");
418 executor.run_until_parked();
419 cx.update_editor(|editor, cx| {
420 assert!(!editor.context_menu_visible());
421 assert!(editor.has_active_inline_completion(cx));
422 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
423 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
424 });
425
426 // After debouncing, new Copilot completions should be requested.
427 handle_copilot_completion_request(
428 &copilot_lsp,
429 vec![copilot::request::Completion {
430 text: "one.copilot2".into(),
431 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
432 ..Default::default()
433 }],
434 vec![],
435 );
436 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
437 cx.update_editor(|editor, cx| {
438 assert!(!editor.context_menu_visible());
439 assert!(editor.has_active_inline_completion(cx));
440 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
441 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
442
443 // Canceling should remove the active Copilot suggestion.
444 editor.cancel(&Default::default(), cx);
445 assert!(!editor.has_active_inline_completion(cx));
446 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
447 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
448
449 // After canceling, tabbing shouldn't insert the previously shown suggestion.
450 editor.tab(&Default::default(), cx);
451 assert!(!editor.has_active_inline_completion(cx));
452 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
453 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
454
455 // When undoing the previously active suggestion is shown again.
456 editor.undo(&Default::default(), cx);
457 assert!(editor.has_active_inline_completion(cx));
458 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
459 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
460 });
461
462 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
463 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
464 cx.update_editor(|editor, cx| {
465 assert!(editor.has_active_inline_completion(cx));
466 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
467 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
468
469 // Tabbing when there is an active suggestion inserts it.
470 editor.tab(&Default::default(), cx);
471 assert!(!editor.has_active_inline_completion(cx));
472 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
473 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
474
475 // When undoing the previously active suggestion is shown again.
476 editor.undo(&Default::default(), cx);
477 assert!(editor.has_active_inline_completion(cx));
478 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
479 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
480
481 // Hide suggestion.
482 editor.cancel(&Default::default(), cx);
483 assert!(!editor.has_active_inline_completion(cx));
484 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
485 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
486 });
487
488 // If an edit occurs outside of this editor but no suggestion is being shown,
489 // we won't make it visible.
490 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
491 cx.update_editor(|editor, cx| {
492 assert!(!editor.has_active_inline_completion(cx));
493 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
494 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
495 });
496
497 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
498 cx.update_editor(|editor, cx| {
499 editor.set_text("fn foo() {\n \n}", cx);
500 editor.change_selections(None, cx, |s| {
501 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
502 });
503 });
504 handle_copilot_completion_request(
505 &copilot_lsp,
506 vec![copilot::request::Completion {
507 text: " let x = 4;".into(),
508 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
509 ..Default::default()
510 }],
511 vec![],
512 );
513
514 cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
515 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
516 cx.update_editor(|editor, cx| {
517 assert!(editor.has_active_inline_completion(cx));
518 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
519 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
520
521 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
522 editor.tab(&Default::default(), cx);
523 assert!(editor.has_active_inline_completion(cx));
524 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
525 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
526
527 // Tabbing again accepts the suggestion.
528 editor.tab(&Default::default(), cx);
529 assert!(!editor.has_active_inline_completion(cx));
530 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
531 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
532 });
533 }
534
535 #[gpui::test(iterations = 10)]
536 async fn test_accept_partial_copilot_suggestion(
537 executor: BackgroundExecutor,
538 cx: &mut TestAppContext,
539 ) {
540 // flaky
541 init_test(cx, |_| {});
542
543 let (copilot, copilot_lsp) = Copilot::fake(cx);
544 let mut cx = EditorLspTestContext::new_rust(
545 lsp::ServerCapabilities {
546 completion_provider: Some(lsp::CompletionOptions {
547 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
548 ..Default::default()
549 }),
550 ..Default::default()
551 },
552 cx,
553 )
554 .await;
555 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
556 cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
557
558 // Setup the editor with a completion request.
559 cx.set_state(indoc! {"
560 oneˇ
561 two
562 three
563 "});
564 cx.simulate_keystroke(".");
565 let _ = handle_completion_request(
566 &mut cx,
567 indoc! {"
568 one.|<>
569 two
570 three
571 "},
572 vec![],
573 );
574 handle_copilot_completion_request(
575 &copilot_lsp,
576 vec![copilot::request::Completion {
577 text: "one.copilot1".into(),
578 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
579 ..Default::default()
580 }],
581 vec![],
582 );
583 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
584 cx.update_editor(|editor, cx| {
585 assert!(editor.has_active_inline_completion(cx));
586
587 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
588 editor.accept_partial_inline_completion(&Default::default(), cx);
589 assert!(editor.has_active_inline_completion(cx));
590 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
591 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
592
593 // Accepting next word should accept the non-word and copilot suggestion should be gone
594 editor.accept_partial_inline_completion(&Default::default(), cx);
595 assert!(!editor.has_active_inline_completion(cx));
596 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
597 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
598 });
599
600 // Reset the editor and check non-word and whitespace completion
601 cx.set_state(indoc! {"
602 oneˇ
603 two
604 three
605 "});
606 cx.simulate_keystroke(".");
607 let _ = handle_completion_request(
608 &mut cx,
609 indoc! {"
610 one.|<>
611 two
612 three
613 "},
614 vec![],
615 );
616 handle_copilot_completion_request(
617 &copilot_lsp,
618 vec![copilot::request::Completion {
619 text: "one.123. copilot\n 456".into(),
620 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
621 ..Default::default()
622 }],
623 vec![],
624 );
625 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
626 cx.update_editor(|editor, cx| {
627 assert!(editor.has_active_inline_completion(cx));
628
629 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
630 editor.accept_partial_inline_completion(&Default::default(), cx);
631 assert!(editor.has_active_inline_completion(cx));
632 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
633 assert_eq!(
634 editor.display_text(cx),
635 "one.123. copilot\n 456\ntwo\nthree\n"
636 );
637
638 // Accepting next word should accept the next word and copilot suggestion should still exist
639 editor.accept_partial_inline_completion(&Default::default(), cx);
640 assert!(editor.has_active_inline_completion(cx));
641 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
642 assert_eq!(
643 editor.display_text(cx),
644 "one.123. copilot\n 456\ntwo\nthree\n"
645 );
646
647 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
648 editor.accept_partial_inline_completion(&Default::default(), cx);
649 assert!(!editor.has_active_inline_completion(cx));
650 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
651 assert_eq!(
652 editor.display_text(cx),
653 "one.123. copilot\n 456\ntwo\nthree\n"
654 );
655 });
656 }
657
658 #[gpui::test]
659 async fn test_copilot_completion_invalidation(
660 executor: BackgroundExecutor,
661 cx: &mut TestAppContext,
662 ) {
663 init_test(cx, |_| {});
664
665 let (copilot, copilot_lsp) = Copilot::fake(cx);
666 let mut cx = EditorLspTestContext::new_rust(
667 lsp::ServerCapabilities {
668 completion_provider: Some(lsp::CompletionOptions {
669 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
670 ..Default::default()
671 }),
672 ..Default::default()
673 },
674 cx,
675 )
676 .await;
677 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
678 cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
679
680 cx.set_state(indoc! {"
681 one
682 twˇ
683 three
684 "});
685
686 handle_copilot_completion_request(
687 &copilot_lsp,
688 vec![copilot::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(cx));
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(cx));
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(cx));
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(cx));
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(cx));
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(0, 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, 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(copilot_provider, cx)
760 })
761 .unwrap();
762
763 handle_copilot_completion_request(
764 &copilot_lsp,
765 vec![copilot::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(cx));
782 assert_eq!(
783 editor.display_text(cx),
784 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\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![copilot::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(cx));
804 assert_eq!(
805 editor.display_text(cx),
806 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\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(cx));
813 assert_eq!(
814 editor.display_text(cx),
815 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \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(cx));
824 assert_eq!(
825 editor.display_text(cx),
826 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\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_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
834 init_test(cx, |settings| {
835 settings
836 .copilot
837 .get_or_insert(Default::default())
838 .disabled_globs = Some(vec![".env*".to_string()]);
839 });
840
841 let (copilot, copilot_lsp) = Copilot::fake(cx);
842
843 let fs = FakeFs::new(cx.executor());
844 fs.insert_tree(
845 "/test",
846 json!({
847 ".env": "SECRET=something\n",
848 "README.md": "hello\n"
849 }),
850 )
851 .await;
852 let project = Project::test(fs, ["/test".as_ref()], cx).await;
853
854 let private_buffer = project
855 .update(cx, |project, cx| {
856 project.open_local_buffer("/test/.env", cx)
857 })
858 .await
859 .unwrap();
860 let public_buffer = project
861 .update(cx, |project, cx| {
862 project.open_local_buffer("/test/README.md", cx)
863 })
864 .await
865 .unwrap();
866
867 let multibuffer = cx.new_model(|cx| {
868 let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
869 multibuffer.push_excerpts(
870 private_buffer.clone(),
871 [ExcerptRange {
872 context: Point::new(0, 0)..Point::new(1, 0),
873 primary: None,
874 }],
875 cx,
876 );
877 multibuffer.push_excerpts(
878 public_buffer.clone(),
879 [ExcerptRange {
880 context: Point::new(0, 0)..Point::new(1, 0),
881 primary: None,
882 }],
883 cx,
884 );
885 multibuffer
886 });
887 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
888 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
889 editor
890 .update(cx, |editor, cx| {
891 editor.set_inline_completion_provider(copilot_provider, cx)
892 })
893 .unwrap();
894
895 let mut copilot_requests = copilot_lsp
896 .handle_request::<copilot::request::GetCompletions, _, _>(
897 move |_params, _cx| async move {
898 Ok(copilot::request::GetCompletionsResult {
899 completions: vec![copilot::request::Completion {
900 text: "next line".into(),
901 range: lsp::Range::new(
902 lsp::Position::new(1, 0),
903 lsp::Position::new(1, 0),
904 ),
905 ..Default::default()
906 }],
907 })
908 },
909 );
910
911 _ = editor.update(cx, |editor, cx| {
912 editor.change_selections(None, cx, |selections| {
913 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
914 });
915 editor.next_inline_completion(&Default::default(), cx);
916 });
917
918 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
919 assert!(copilot_requests.try_next().is_err());
920
921 _ = editor.update(cx, |editor, cx| {
922 editor.change_selections(None, cx, |s| {
923 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
924 });
925 editor.next_inline_completion(&Default::default(), cx);
926 });
927
928 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
929 assert!(copilot_requests.try_next().is_ok());
930 }
931
932 fn handle_copilot_completion_request(
933 lsp: &lsp::FakeLanguageServer,
934 completions: Vec<copilot::request::Completion>,
935 completions_cycling: Vec<copilot::request::Completion>,
936 ) {
937 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
938 let completions = completions.clone();
939 async move {
940 Ok(copilot::request::GetCompletionsResult {
941 completions: completions.clone(),
942 })
943 }
944 });
945 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
946 let completions_cycling = completions_cycling.clone();
947 async move {
948 Ok(copilot::request::GetCompletionsResult {
949 completions: completions_cycling.clone(),
950 })
951 }
952 });
953 }
954
955 fn handle_completion_request(
956 cx: &mut EditorLspTestContext,
957 marked_string: &str,
958 completions: Vec<&'static str>,
959 ) -> impl Future<Output = ()> {
960 let complete_from_marker: TextRangeMarker = '|'.into();
961 let replace_range_marker: TextRangeMarker = ('<', '>').into();
962 let (_, mut marked_ranges) = marked_text_ranges_by(
963 marked_string,
964 vec![complete_from_marker.clone(), replace_range_marker.clone()],
965 );
966
967 let complete_from_position =
968 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
969 let replace_range =
970 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
971
972 let mut request =
973 cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
974 let completions = completions.clone();
975 async move {
976 assert_eq!(params.text_document_position.text_document.uri, url.clone());
977 assert_eq!(
978 params.text_document_position.position,
979 complete_from_position
980 );
981 Ok(Some(lsp::CompletionResponse::Array(
982 completions
983 .iter()
984 .map(|completion_text| lsp::CompletionItem {
985 label: completion_text.to_string(),
986 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
987 range: replace_range,
988 new_text: completion_text.to_string(),
989 })),
990 ..Default::default()
991 })
992 .collect(),
993 )))
994 }
995 });
996
997 async move {
998 request.next().await;
999 }
1000 }
1001
1002 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1003 _ = cx.update(|cx| {
1004 let store = SettingsStore::test(cx);
1005 cx.set_global(store);
1006 theme::init(theme::LoadThemes::JustBase, cx);
1007 client::init_settings(cx);
1008 language::init(cx);
1009 editor::init_settings(cx);
1010 Project::init_settings(cx);
1011 workspace::init_settings(cx);
1012 cx.update_global(|store: &mut SettingsStore, cx| {
1013 store.update_user_settings::<AllLanguageSettings>(cx, f);
1014 });
1015 });
1016 }
1017}