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