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 BufferId, 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| {
733 Buffer::new(
734 0,
735 BufferId::new(cx.entity_id().as_u64()).unwrap(),
736 "a = 1\nb = 2\n",
737 )
738 });
739 let buffer_2 = cx.new_model(|cx| {
740 Buffer::new(
741 0,
742 BufferId::new(cx.entity_id().as_u64()).unwrap(),
743 "c = 3\nd = 4\n",
744 )
745 });
746 let multibuffer = cx.new_model(|cx| {
747 let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
748 multibuffer.push_excerpts(
749 buffer_1.clone(),
750 [ExcerptRange {
751 context: Point::new(0, 0)..Point::new(2, 0),
752 primary: None,
753 }],
754 cx,
755 );
756 multibuffer.push_excerpts(
757 buffer_2.clone(),
758 [ExcerptRange {
759 context: Point::new(0, 0)..Point::new(2, 0),
760 primary: None,
761 }],
762 cx,
763 );
764 multibuffer
765 });
766 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
767 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
768 editor
769 .update(cx, |editor, cx| {
770 editor.set_inline_completion_provider(copilot_provider, cx)
771 })
772 .unwrap();
773
774 handle_copilot_completion_request(
775 &copilot_lsp,
776 vec![copilot::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![copilot::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_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
845 init_test(cx, |settings| {
846 settings
847 .copilot
848 .get_or_insert(Default::default())
849 .disabled_globs = Some(vec![".env*".to_string()]);
850 });
851
852 let (copilot, copilot_lsp) = Copilot::fake(cx);
853
854 let fs = FakeFs::new(cx.executor());
855 fs.insert_tree(
856 "/test",
857 json!({
858 ".env": "SECRET=something\n",
859 "README.md": "hello\n"
860 }),
861 )
862 .await;
863 let project = Project::test(fs, ["/test".as_ref()], cx).await;
864
865 let private_buffer = project
866 .update(cx, |project, cx| {
867 project.open_local_buffer("/test/.env", cx)
868 })
869 .await
870 .unwrap();
871 let public_buffer = project
872 .update(cx, |project, cx| {
873 project.open_local_buffer("/test/README.md", cx)
874 })
875 .await
876 .unwrap();
877
878 let multibuffer = cx.new_model(|cx| {
879 let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
880 multibuffer.push_excerpts(
881 private_buffer.clone(),
882 [ExcerptRange {
883 context: Point::new(0, 0)..Point::new(1, 0),
884 primary: None,
885 }],
886 cx,
887 );
888 multibuffer.push_excerpts(
889 public_buffer.clone(),
890 [ExcerptRange {
891 context: Point::new(0, 0)..Point::new(1, 0),
892 primary: None,
893 }],
894 cx,
895 );
896 multibuffer
897 });
898 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
899 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
900 editor
901 .update(cx, |editor, cx| {
902 editor.set_inline_completion_provider(copilot_provider, cx)
903 })
904 .unwrap();
905
906 let mut copilot_requests = copilot_lsp
907 .handle_request::<copilot::request::GetCompletions, _, _>(
908 move |_params, _cx| async move {
909 Ok(copilot::request::GetCompletionsResult {
910 completions: vec![copilot::request::Completion {
911 text: "next line".into(),
912 range: lsp::Range::new(
913 lsp::Position::new(1, 0),
914 lsp::Position::new(1, 0),
915 ),
916 ..Default::default()
917 }],
918 })
919 },
920 );
921
922 _ = editor.update(cx, |editor, cx| {
923 editor.change_selections(None, cx, |selections| {
924 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
925 });
926 editor.next_inline_completion(&Default::default(), cx);
927 });
928
929 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
930 assert!(copilot_requests.try_next().is_err());
931
932 _ = editor.update(cx, |editor, cx| {
933 editor.change_selections(None, cx, |s| {
934 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
935 });
936 editor.next_inline_completion(&Default::default(), cx);
937 });
938
939 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
940 assert!(copilot_requests.try_next().is_ok());
941 }
942
943 fn handle_copilot_completion_request(
944 lsp: &lsp::FakeLanguageServer,
945 completions: Vec<copilot::request::Completion>,
946 completions_cycling: Vec<copilot::request::Completion>,
947 ) {
948 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
949 let completions = completions.clone();
950 async move {
951 Ok(copilot::request::GetCompletionsResult {
952 completions: completions.clone(),
953 })
954 }
955 });
956 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
957 let completions_cycling = completions_cycling.clone();
958 async move {
959 Ok(copilot::request::GetCompletionsResult {
960 completions: completions_cycling.clone(),
961 })
962 }
963 });
964 }
965
966 fn handle_completion_request(
967 cx: &mut EditorLspTestContext,
968 marked_string: &str,
969 completions: Vec<&'static str>,
970 ) -> impl Future<Output = ()> {
971 let complete_from_marker: TextRangeMarker = '|'.into();
972 let replace_range_marker: TextRangeMarker = ('<', '>').into();
973 let (_, mut marked_ranges) = marked_text_ranges_by(
974 marked_string,
975 vec![complete_from_marker.clone(), replace_range_marker.clone()],
976 );
977
978 let complete_from_position =
979 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
980 let replace_range =
981 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
982
983 let mut request =
984 cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
985 let completions = completions.clone();
986 async move {
987 assert_eq!(params.text_document_position.text_document.uri, url.clone());
988 assert_eq!(
989 params.text_document_position.position,
990 complete_from_position
991 );
992 Ok(Some(lsp::CompletionResponse::Array(
993 completions
994 .iter()
995 .map(|completion_text| lsp::CompletionItem {
996 label: completion_text.to_string(),
997 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
998 range: replace_range,
999 new_text: completion_text.to_string(),
1000 })),
1001 ..Default::default()
1002 })
1003 .collect(),
1004 )))
1005 }
1006 });
1007
1008 async move {
1009 request.next().await;
1010 }
1011 }
1012
1013 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1014 _ = cx.update(|cx| {
1015 let store = SettingsStore::test(cx);
1016 cx.set_global(store);
1017 theme::init(theme::LoadThemes::JustBase, cx);
1018 client::init_settings(cx);
1019 language::init(cx);
1020 editor::init_settings(cx);
1021 Project::init_settings(cx);
1022 workspace::init_settings(cx);
1023 cx.update_global(|store: &mut SettingsStore, cx| {
1024 store.update_user_settings::<AllLanguageSettings>(cx, f);
1025 });
1026 });
1027 }
1028}