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