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