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