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