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