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