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