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