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