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