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 editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
768 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
769 editor
770 .update(cx, |editor, cx| {
771 editor.set_inline_completion_provider(copilot_provider, cx)
772 })
773 .unwrap();
774
775 handle_copilot_completion_request(
776 &copilot_lsp,
777 vec![copilot::request::Completion {
778 text: "b = 2 + a".into(),
779 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
780 ..Default::default()
781 }],
782 vec![],
783 );
784 _ = editor.update(cx, |editor, cx| {
785 // Ensure copilot suggestions are shown for the first excerpt.
786 editor.change_selections(None, cx, |s| {
787 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
788 });
789 editor.next_inline_completion(&Default::default(), cx);
790 });
791 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
792 _ = editor.update(cx, |editor, cx| {
793 assert!(editor.has_active_inline_completion(cx));
794 assert_eq!(
795 editor.display_text(cx),
796 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
797 );
798 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
799 });
800
801 handle_copilot_completion_request(
802 &copilot_lsp,
803 vec![copilot::request::Completion {
804 text: "d = 4 + c".into(),
805 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
806 ..Default::default()
807 }],
808 vec![],
809 );
810 _ = editor.update(cx, |editor, cx| {
811 // Move to another excerpt, ensuring the suggestion gets cleared.
812 editor.change_selections(None, cx, |s| {
813 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
814 });
815 assert!(!editor.has_active_inline_completion(cx));
816 assert_eq!(
817 editor.display_text(cx),
818 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
819 );
820 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
821
822 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
823 editor.handle_input(" ", cx);
824 assert!(!editor.has_active_inline_completion(cx));
825 assert_eq!(
826 editor.display_text(cx),
827 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
828 );
829 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
830 });
831
832 // Ensure the new suggestion is displayed when the debounce timeout expires.
833 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
834 _ = editor.update(cx, |editor, cx| {
835 assert!(editor.has_active_inline_completion(cx));
836 assert_eq!(
837 editor.display_text(cx),
838 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
839 );
840 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
841 });
842 }
843
844 #[gpui::test]
845 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
846 init_test(cx, |settings| {
847 settings
848 .copilot
849 .get_or_insert(Default::default())
850 .disabled_globs = Some(vec![".env*".to_string()]);
851 });
852
853 let (copilot, copilot_lsp) = Copilot::fake(cx);
854
855 let fs = FakeFs::new(cx.executor());
856 fs.insert_tree(
857 "/test",
858 json!({
859 ".env": "SECRET=something\n",
860 "README.md": "hello\n"
861 }),
862 )
863 .await;
864 let project = Project::test(fs, ["/test".as_ref()], cx).await;
865
866 let private_buffer = project
867 .update(cx, |project, cx| {
868 project.open_local_buffer("/test/.env", cx)
869 })
870 .await
871 .unwrap();
872 let public_buffer = project
873 .update(cx, |project, cx| {
874 project.open_local_buffer("/test/README.md", cx)
875 })
876 .await
877 .unwrap();
878
879 let multibuffer = cx.new_model(|cx| {
880 let mut multibuffer = MultiBuffer::new(0, language::Capability::ReadWrite);
881 multibuffer.push_excerpts(
882 private_buffer.clone(),
883 [ExcerptRange {
884 context: Point::new(0, 0)..Point::new(1, 0),
885 primary: None,
886 }],
887 cx,
888 );
889 multibuffer.push_excerpts(
890 public_buffer.clone(),
891 [ExcerptRange {
892 context: Point::new(0, 0)..Point::new(1, 0),
893 primary: None,
894 }],
895 cx,
896 );
897 multibuffer
898 });
899 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
900 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
901 editor
902 .update(cx, |editor, cx| {
903 editor.set_inline_completion_provider(copilot_provider, cx)
904 })
905 .unwrap();
906
907 let mut copilot_requests = copilot_lsp
908 .handle_request::<copilot::request::GetCompletions, _, _>(
909 move |_params, _cx| async move {
910 Ok(copilot::request::GetCompletionsResult {
911 completions: vec![copilot::request::Completion {
912 text: "next line".into(),
913 range: lsp::Range::new(
914 lsp::Position::new(1, 0),
915 lsp::Position::new(1, 0),
916 ),
917 ..Default::default()
918 }],
919 })
920 },
921 );
922
923 _ = editor.update(cx, |editor, cx| {
924 editor.change_selections(None, cx, |selections| {
925 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
926 });
927 editor.next_inline_completion(&Default::default(), cx);
928 });
929
930 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
931 assert!(copilot_requests.try_next().is_err());
932
933 _ = editor.update(cx, |editor, cx| {
934 editor.change_selections(None, cx, |s| {
935 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
936 });
937 editor.next_inline_completion(&Default::default(), cx);
938 });
939
940 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
941 assert!(copilot_requests.try_next().is_ok());
942 }
943
944 fn handle_copilot_completion_request(
945 lsp: &lsp::FakeLanguageServer,
946 completions: Vec<copilot::request::Completion>,
947 completions_cycling: Vec<copilot::request::Completion>,
948 ) {
949 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
950 let completions = completions.clone();
951 async move {
952 Ok(copilot::request::GetCompletionsResult {
953 completions: completions.clone(),
954 })
955 }
956 });
957 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
958 let completions_cycling = completions_cycling.clone();
959 async move {
960 Ok(copilot::request::GetCompletionsResult {
961 completions: completions_cycling.clone(),
962 })
963 }
964 });
965 }
966
967 fn handle_completion_request(
968 cx: &mut EditorLspTestContext,
969 marked_string: &str,
970 completions: Vec<&'static str>,
971 ) -> impl Future<Output = ()> {
972 let complete_from_marker: TextRangeMarker = '|'.into();
973 let replace_range_marker: TextRangeMarker = ('<', '>').into();
974 let (_, mut marked_ranges) = marked_text_ranges_by(
975 marked_string,
976 vec![complete_from_marker.clone(), replace_range_marker.clone()],
977 );
978
979 let complete_from_position =
980 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
981 let replace_range =
982 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
983
984 let mut request =
985 cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
986 let completions = completions.clone();
987 async move {
988 assert_eq!(params.text_document_position.text_document.uri, url.clone());
989 assert_eq!(
990 params.text_document_position.position,
991 complete_from_position
992 );
993 Ok(Some(lsp::CompletionResponse::Array(
994 completions
995 .iter()
996 .map(|completion_text| lsp::CompletionItem {
997 label: completion_text.to_string(),
998 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
999 range: replace_range,
1000 new_text: completion_text.to_string(),
1001 })),
1002 ..Default::default()
1003 })
1004 .collect(),
1005 )))
1006 }
1007 });
1008
1009 async move {
1010 request.next().await;
1011 }
1012 }
1013
1014 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1015 _ = cx.update(|cx| {
1016 let store = SettingsStore::test(cx);
1017 cx.set_global(store);
1018 theme::init(theme::LoadThemes::JustBase, cx);
1019 client::init_settings(cx);
1020 language::init(cx);
1021 editor::init_settings(cx);
1022 Project::init_settings(cx);
1023 workspace::init_settings(cx);
1024 cx.update_global(|store: &mut SettingsStore, cx| {
1025 store.update_user_settings::<AllLanguageSettings>(cx, f);
1026 });
1027 });
1028 }
1029}