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