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