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