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