1use crate::{Completion, Copilot};
2use anyhow::Result;
3use client::telemetry::Telemetry;
4use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
5use gpui::{AppContext, EntityId, Model, ModelContext, Task};
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()))
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);
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.confirm_completion(&Default::default(), cx).unwrap()
367 })
368 .await
369 .unwrap();
370
371 cx.update_editor(|editor, cx| {
372 assert!(!editor.context_menu_visible());
373 assert!(!editor.has_active_inline_completion(cx));
374 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
375 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
376 });
377
378 // Ensure Copilot suggestions are shown right away if no autocompletion is available.
379 cx.set_state(indoc! {"
380 oneˇ
381 two
382 three
383 "});
384 cx.simulate_keystroke(".");
385 drop(handle_completion_request(
386 &mut cx,
387 indoc! {"
388 one.|<>
389 two
390 three
391 "},
392 vec![],
393 ));
394 handle_copilot_completion_request(
395 &copilot_lsp,
396 vec![crate::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 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
408 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
409 });
410
411 // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
412 cx.set_state(indoc! {"
413 oneˇ
414 two
415 three
416 "});
417 cx.simulate_keystroke(".");
418 drop(handle_completion_request(
419 &mut cx,
420 indoc! {"
421 one.|<>
422 two
423 three
424 "},
425 vec!["completion_a", "completion_b"],
426 ));
427 handle_copilot_completion_request(
428 &copilot_lsp,
429 vec![crate::request::Completion {
430 text: "one.copilot1".into(),
431 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
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
441 // When hiding the context menu, the Copilot suggestion becomes visible.
442 editor.cancel(&Default::default(), cx);
443 assert!(!editor.context_menu_visible());
444 assert!(editor.has_active_inline_completion(cx));
445 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
446 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
447 });
448
449 // Ensure existing completion is interpolated when inserting again.
450 cx.simulate_keystroke("c");
451 executor.run_until_parked();
452 cx.update_editor(|editor, cx| {
453 assert!(!editor.context_menu_visible());
454 assert!(editor.has_active_inline_completion(cx));
455 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
456 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
457 });
458
459 // After debouncing, new Copilot completions should be requested.
460 handle_copilot_completion_request(
461 &copilot_lsp,
462 vec![crate::request::Completion {
463 text: "one.copilot2".into(),
464 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
465 ..Default::default()
466 }],
467 vec![],
468 );
469 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
470 cx.update_editor(|editor, cx| {
471 assert!(!editor.context_menu_visible());
472 assert!(editor.has_active_inline_completion(cx));
473 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
474 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
475
476 // Canceling should remove the active Copilot suggestion.
477 editor.cancel(&Default::default(), cx);
478 assert!(!editor.has_active_inline_completion(cx));
479 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
480 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
481
482 // After canceling, tabbing shouldn't insert the previously shown suggestion.
483 editor.tab(&Default::default(), cx);
484 assert!(!editor.has_active_inline_completion(cx));
485 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
486 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
487
488 // When undoing the previously active suggestion is shown again.
489 editor.undo(&Default::default(), cx);
490 assert!(editor.has_active_inline_completion(cx));
491 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
492 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
493 });
494
495 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
496 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
497 cx.update_editor(|editor, cx| {
498 assert!(editor.has_active_inline_completion(cx));
499 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
500 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
501
502 // AcceptInlineCompletion when there is an active suggestion inserts it.
503 editor.accept_inline_completion(&Default::default(), cx);
504 assert!(!editor.has_active_inline_completion(cx));
505 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
506 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
507
508 // When undoing the previously active suggestion is shown again.
509 editor.undo(&Default::default(), cx);
510 assert!(editor.has_active_inline_completion(cx));
511 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
512 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
513
514 // Hide suggestion.
515 editor.cancel(&Default::default(), cx);
516 assert!(!editor.has_active_inline_completion(cx));
517 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
518 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
519 });
520
521 // If an edit occurs outside of this editor but no suggestion is being shown,
522 // we won't make it visible.
523 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
524 cx.update_editor(|editor, cx| {
525 assert!(!editor.has_active_inline_completion(cx));
526 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
527 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
528 });
529
530 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
531 cx.update_editor(|editor, cx| {
532 editor.set_text("fn foo() {\n \n}", cx);
533 editor.change_selections(None, cx, |s| {
534 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
535 });
536 });
537 handle_copilot_completion_request(
538 &copilot_lsp,
539 vec![crate::request::Completion {
540 text: " let x = 4;".into(),
541 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
542 ..Default::default()
543 }],
544 vec![],
545 );
546
547 cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
548 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
549 cx.update_editor(|editor, cx| {
550 assert!(editor.has_active_inline_completion(cx));
551 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
552 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
553
554 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
555 editor.tab(&Default::default(), cx);
556 assert!(editor.has_active_inline_completion(cx));
557 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
558 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
559
560 // Using AcceptInlineCompletion again accepts the suggestion.
561 editor.accept_inline_completion(&Default::default(), cx);
562 assert!(!editor.has_active_inline_completion(cx));
563 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
564 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
565 });
566 }
567
568 #[gpui::test(iterations = 10)]
569 async fn test_accept_partial_copilot_suggestion(
570 executor: BackgroundExecutor,
571 cx: &mut TestAppContext,
572 ) {
573 // flaky
574 init_test(cx, |_| {});
575
576 let (copilot, copilot_lsp) = Copilot::fake(cx);
577 let mut cx = EditorLspTestContext::new_rust(
578 lsp::ServerCapabilities {
579 completion_provider: Some(lsp::CompletionOptions {
580 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
581 ..Default::default()
582 }),
583 ..Default::default()
584 },
585 cx,
586 )
587 .await;
588 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
589 cx.update_editor(|editor, cx| {
590 editor.set_inline_completion_provider(Some(copilot_provider), cx)
591 });
592
593 // Setup the editor with a completion request.
594 cx.set_state(indoc! {"
595 oneˇ
596 two
597 three
598 "});
599 cx.simulate_keystroke(".");
600 drop(handle_completion_request(
601 &mut cx,
602 indoc! {"
603 one.|<>
604 two
605 three
606 "},
607 vec![],
608 ));
609 handle_copilot_completion_request(
610 &copilot_lsp,
611 vec![crate::request::Completion {
612 text: "one.copilot1".into(),
613 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
614 ..Default::default()
615 }],
616 vec![],
617 );
618 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
619 cx.update_editor(|editor, cx| {
620 assert!(editor.has_active_inline_completion(cx));
621
622 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
623 editor.accept_partial_inline_completion(&Default::default(), cx);
624 assert!(editor.has_active_inline_completion(cx));
625 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
626 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
627
628 // Accepting next word should accept the non-word and copilot suggestion should be gone
629 editor.accept_partial_inline_completion(&Default::default(), cx);
630 assert!(!editor.has_active_inline_completion(cx));
631 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
632 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
633 });
634
635 // Reset the editor and check non-word and whitespace completion
636 cx.set_state(indoc! {"
637 oneˇ
638 two
639 three
640 "});
641 cx.simulate_keystroke(".");
642 drop(handle_completion_request(
643 &mut cx,
644 indoc! {"
645 one.|<>
646 two
647 three
648 "},
649 vec![],
650 ));
651 handle_copilot_completion_request(
652 &copilot_lsp,
653 vec![crate::request::Completion {
654 text: "one.123. copilot\n 456".into(),
655 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
656 ..Default::default()
657 }],
658 vec![],
659 );
660 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
661 cx.update_editor(|editor, cx| {
662 assert!(editor.has_active_inline_completion(cx));
663
664 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
665 editor.accept_partial_inline_completion(&Default::default(), cx);
666 assert!(editor.has_active_inline_completion(cx));
667 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
668 assert_eq!(
669 editor.display_text(cx),
670 "one.123. copilot\n 456\ntwo\nthree\n"
671 );
672
673 // Accepting next word should accept the next word and copilot suggestion should still exist
674 editor.accept_partial_inline_completion(&Default::default(), cx);
675 assert!(editor.has_active_inline_completion(cx));
676 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
677 assert_eq!(
678 editor.display_text(cx),
679 "one.123. copilot\n 456\ntwo\nthree\n"
680 );
681
682 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
683 editor.accept_partial_inline_completion(&Default::default(), cx);
684 assert!(!editor.has_active_inline_completion(cx));
685 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
686 assert_eq!(
687 editor.display_text(cx),
688 "one.123. copilot\n 456\ntwo\nthree\n"
689 );
690 });
691 }
692
693 #[gpui::test]
694 async fn test_copilot_completion_invalidation(
695 executor: BackgroundExecutor,
696 cx: &mut TestAppContext,
697 ) {
698 init_test(cx, |_| {});
699
700 let (copilot, copilot_lsp) = Copilot::fake(cx);
701 let mut cx = EditorLspTestContext::new_rust(
702 lsp::ServerCapabilities {
703 completion_provider: Some(lsp::CompletionOptions {
704 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
705 ..Default::default()
706 }),
707 ..Default::default()
708 },
709 cx,
710 )
711 .await;
712 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
713 cx.update_editor(|editor, cx| {
714 editor.set_inline_completion_provider(Some(copilot_provider), cx)
715 });
716
717 cx.set_state(indoc! {"
718 one
719 twˇ
720 three
721 "});
722
723 handle_copilot_completion_request(
724 &copilot_lsp,
725 vec![crate::request::Completion {
726 text: "two.foo()".into(),
727 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
728 ..Default::default()
729 }],
730 vec![],
731 );
732 cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
733 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
734 cx.update_editor(|editor, cx| {
735 assert!(editor.has_active_inline_completion(cx));
736 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
737 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
738
739 editor.backspace(&Default::default(), cx);
740 assert!(editor.has_active_inline_completion(cx));
741 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
742 assert_eq!(editor.text(cx), "one\nt\nthree\n");
743
744 editor.backspace(&Default::default(), cx);
745 assert!(editor.has_active_inline_completion(cx));
746 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
747 assert_eq!(editor.text(cx), "one\n\nthree\n");
748
749 // Deleting across the original suggestion range invalidates it.
750 editor.backspace(&Default::default(), cx);
751 assert!(!editor.has_active_inline_completion(cx));
752 assert_eq!(editor.display_text(cx), "one\nthree\n");
753 assert_eq!(editor.text(cx), "one\nthree\n");
754
755 // Undoing the deletion restores the suggestion.
756 editor.undo(&Default::default(), cx);
757 assert!(editor.has_active_inline_completion(cx));
758 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
759 assert_eq!(editor.text(cx), "one\n\nthree\n");
760 });
761 }
762
763 #[gpui::test]
764 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
765 init_test(cx, |_| {});
766
767 let (copilot, copilot_lsp) = Copilot::fake(cx);
768
769 let buffer_1 = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx));
770 let buffer_2 = cx.new_model(|cx| Buffer::local("c = 3\nd = 4\n", cx));
771 let multibuffer = cx.new_model(|cx| {
772 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
773 multibuffer.push_excerpts(
774 buffer_1.clone(),
775 [ExcerptRange {
776 context: Point::new(0, 0)..Point::new(2, 0),
777 primary: None,
778 }],
779 cx,
780 );
781 multibuffer.push_excerpts(
782 buffer_2.clone(),
783 [ExcerptRange {
784 context: Point::new(0, 0)..Point::new(2, 0),
785 primary: None,
786 }],
787 cx,
788 );
789 multibuffer
790 });
791 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
792 editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
793 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
794 editor
795 .update(cx, |editor, cx| {
796 editor.set_inline_completion_provider(Some(copilot_provider), cx)
797 })
798 .unwrap();
799
800 handle_copilot_completion_request(
801 &copilot_lsp,
802 vec![crate::request::Completion {
803 text: "b = 2 + a".into(),
804 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
805 ..Default::default()
806 }],
807 vec![],
808 );
809 _ = editor.update(cx, |editor, cx| {
810 // Ensure copilot suggestions are shown for the first excerpt.
811 editor.change_selections(None, cx, |s| {
812 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
813 });
814 editor.next_inline_completion(&Default::default(), cx);
815 });
816 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
817 _ = editor.update(cx, |editor, cx| {
818 assert!(editor.has_active_inline_completion(cx));
819 assert_eq!(
820 editor.display_text(cx),
821 "\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
822 );
823 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
824 });
825
826 handle_copilot_completion_request(
827 &copilot_lsp,
828 vec![crate::request::Completion {
829 text: "d = 4 + c".into(),
830 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
831 ..Default::default()
832 }],
833 vec![],
834 );
835 _ = editor.update(cx, |editor, cx| {
836 // Move to another excerpt, ensuring the suggestion gets cleared.
837 editor.change_selections(None, cx, |s| {
838 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
839 });
840 assert!(!editor.has_active_inline_completion(cx));
841 assert_eq!(
842 editor.display_text(cx),
843 "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
844 );
845 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
846
847 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
848 editor.handle_input(" ", cx);
849 assert!(!editor.has_active_inline_completion(cx));
850 assert_eq!(
851 editor.display_text(cx),
852 "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
853 );
854 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
855 });
856
857 // Ensure the new suggestion is displayed when the debounce timeout expires.
858 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
859 _ = editor.update(cx, |editor, cx| {
860 assert!(editor.has_active_inline_completion(cx));
861 assert_eq!(
862 editor.display_text(cx),
863 "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
864 );
865 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
866 });
867 }
868
869 #[gpui::test]
870 async fn test_copilot_does_not_prevent_completion_triggers(
871 executor: BackgroundExecutor,
872 cx: &mut TestAppContext,
873 ) {
874 init_test(cx, |_| {});
875
876 let (copilot, copilot_lsp) = Copilot::fake(cx);
877 let mut cx = EditorLspTestContext::new_rust(
878 lsp::ServerCapabilities {
879 completion_provider: Some(lsp::CompletionOptions {
880 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
881 ..lsp::CompletionOptions::default()
882 }),
883 ..lsp::ServerCapabilities::default()
884 },
885 cx,
886 )
887 .await;
888 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
889 cx.update_editor(|editor, cx| {
890 editor.set_inline_completion_provider(Some(copilot_provider), cx)
891 });
892
893 cx.set_state(indoc! {"
894 one
895 twˇ
896 three
897 "});
898
899 drop(handle_completion_request(
900 &mut cx,
901 indoc! {"
902 one
903 tw|<>
904 three
905 "},
906 vec!["completion_a", "completion_b"],
907 ));
908 handle_copilot_completion_request(
909 &copilot_lsp,
910 vec![crate::request::Completion {
911 text: "two.foo()".into(),
912 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
913 ..Default::default()
914 }],
915 vec![],
916 );
917 cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
918 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
919 cx.update_editor(|editor, cx| {
920 assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
921 assert!(editor.has_active_inline_completion(cx));
922 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
923 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
924 });
925
926 cx.simulate_keystroke("o");
927 drop(handle_completion_request(
928 &mut cx,
929 indoc! {"
930 one
931 two|<>
932 three
933 "},
934 vec!["completion_a_2", "completion_b_2"],
935 ));
936 handle_copilot_completion_request(
937 &copilot_lsp,
938 vec![crate::request::Completion {
939 text: "two.foo()".into(),
940 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
941 ..Default::default()
942 }],
943 vec![],
944 );
945 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
946 cx.update_editor(|editor, cx| {
947 assert!(!editor.context_menu_visible());
948 assert!(editor.has_active_inline_completion(cx));
949 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
950 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
951 });
952
953 cx.simulate_keystroke(".");
954 drop(handle_completion_request(
955 &mut cx,
956 indoc! {"
957 one
958 two.|<>
959 three
960 "},
961 vec!["something_else()"],
962 ));
963 handle_copilot_completion_request(
964 &copilot_lsp,
965 vec![crate::request::Completion {
966 text: "two.foo()".into(),
967 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
968 ..Default::default()
969 }],
970 vec![],
971 );
972 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
973 cx.update_editor(|editor, cx| {
974 assert!(
975 editor.context_menu_visible(),
976 "On completion trigger input, the completions should be fetched and visible"
977 );
978 assert!(
979 !editor.has_active_inline_completion(cx),
980 "On completion trigger input, copilot suggestion should be dismissed"
981 );
982 assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
983 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
984 });
985 }
986
987 #[gpui::test]
988 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
989 init_test(cx, |settings| {
990 settings
991 .inline_completions
992 .get_or_insert(Default::default())
993 .disabled_globs = Some(vec![".env*".to_string()]);
994 });
995
996 let (copilot, copilot_lsp) = Copilot::fake(cx);
997
998 let fs = FakeFs::new(cx.executor());
999 fs.insert_tree(
1000 "/test",
1001 json!({
1002 ".env": "SECRET=something\n",
1003 "README.md": "hello\n"
1004 }),
1005 )
1006 .await;
1007 let project = Project::test(fs, ["/test".as_ref()], cx).await;
1008
1009 let private_buffer = project
1010 .update(cx, |project, cx| {
1011 project.open_local_buffer("/test/.env", cx)
1012 })
1013 .await
1014 .unwrap();
1015 let public_buffer = project
1016 .update(cx, |project, cx| {
1017 project.open_local_buffer("/test/README.md", cx)
1018 })
1019 .await
1020 .unwrap();
1021
1022 let multibuffer = cx.new_model(|cx| {
1023 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
1024 multibuffer.push_excerpts(
1025 private_buffer.clone(),
1026 [ExcerptRange {
1027 context: Point::new(0, 0)..Point::new(1, 0),
1028 primary: None,
1029 }],
1030 cx,
1031 );
1032 multibuffer.push_excerpts(
1033 public_buffer.clone(),
1034 [ExcerptRange {
1035 context: Point::new(0, 0)..Point::new(1, 0),
1036 primary: None,
1037 }],
1038 cx,
1039 );
1040 multibuffer
1041 });
1042 let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
1043 let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
1044 editor
1045 .update(cx, |editor, cx| {
1046 editor.set_inline_completion_provider(Some(copilot_provider), cx)
1047 })
1048 .unwrap();
1049
1050 let mut copilot_requests = copilot_lsp
1051 .handle_request::<crate::request::GetCompletions, _, _>(
1052 move |_params, _cx| async move {
1053 Ok(crate::request::GetCompletionsResult {
1054 completions: vec![crate::request::Completion {
1055 text: "next line".into(),
1056 range: lsp::Range::new(
1057 lsp::Position::new(1, 0),
1058 lsp::Position::new(1, 0),
1059 ),
1060 ..Default::default()
1061 }],
1062 })
1063 },
1064 );
1065
1066 _ = editor.update(cx, |editor, cx| {
1067 editor.change_selections(None, cx, |selections| {
1068 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1069 });
1070 editor.refresh_inline_completion(true, false, cx);
1071 });
1072
1073 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1074 assert!(copilot_requests.try_next().is_err());
1075
1076 _ = editor.update(cx, |editor, cx| {
1077 editor.change_selections(None, cx, |s| {
1078 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
1079 });
1080 editor.refresh_inline_completion(true, false, cx);
1081 });
1082
1083 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1084 assert!(copilot_requests.try_next().is_ok());
1085 }
1086
1087 fn handle_copilot_completion_request(
1088 lsp: &lsp::FakeLanguageServer,
1089 completions: Vec<crate::request::Completion>,
1090 completions_cycling: Vec<crate::request::Completion>,
1091 ) {
1092 lsp.handle_request::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1093 let completions = completions.clone();
1094 async move {
1095 Ok(crate::request::GetCompletionsResult {
1096 completions: completions.clone(),
1097 })
1098 }
1099 });
1100 lsp.handle_request::<crate::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
1101 let completions_cycling = completions_cycling.clone();
1102 async move {
1103 Ok(crate::request::GetCompletionsResult {
1104 completions: completions_cycling.clone(),
1105 })
1106 }
1107 });
1108 }
1109
1110 fn handle_completion_request(
1111 cx: &mut EditorLspTestContext,
1112 marked_string: &str,
1113 completions: Vec<&'static str>,
1114 ) -> impl Future<Output = ()> {
1115 let complete_from_marker: TextRangeMarker = '|'.into();
1116 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1117 let (_, mut marked_ranges) = marked_text_ranges_by(
1118 marked_string,
1119 vec![complete_from_marker.clone(), replace_range_marker.clone()],
1120 );
1121
1122 let complete_from_position =
1123 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
1124 let replace_range =
1125 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1126
1127 let mut request =
1128 cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
1129 let completions = completions.clone();
1130 async move {
1131 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1132 assert_eq!(
1133 params.text_document_position.position,
1134 complete_from_position
1135 );
1136 Ok(Some(lsp::CompletionResponse::Array(
1137 completions
1138 .iter()
1139 .map(|completion_text| lsp::CompletionItem {
1140 label: completion_text.to_string(),
1141 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1142 range: replace_range,
1143 new_text: completion_text.to_string(),
1144 })),
1145 ..Default::default()
1146 })
1147 .collect(),
1148 )))
1149 }
1150 });
1151
1152 async move {
1153 request.next().await;
1154 }
1155 }
1156
1157 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1158 cx.update(|cx| {
1159 let store = SettingsStore::test(cx);
1160 cx.set_global(store);
1161 theme::init(theme::LoadThemes::JustBase, cx);
1162 client::init_settings(cx);
1163 language::init(cx);
1164 editor::init_settings(cx);
1165 Project::init_settings(cx);
1166 workspace::init_settings(cx);
1167 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1168 store.update_user_settings::<AllLanguageSettings>(cx, f);
1169 });
1170 });
1171 }
1172}