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