1use crate::{Completion, Copilot};
2use anyhow::Result;
3use gpui::{App, Context, Entity, EntityId, Task};
4use inline_completion::{Direction, EditPredictionProvider, InlineCompletion};
5use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
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(async move |this, cx| {
87 if debounce {
88 cx.background_executor()
89 .timer(COPILOT_DEBOUNCE_TIMEOUT)
90 .await;
91 }
92
93 let completions = copilot
94 .update(cx, |copilot, cx| {
95 copilot.completions(&buffer, cursor_position, cx)
96 })?
97 .await?;
98
99 this.update(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(async move |this, cx| {
157 let completions = copilot
158 .update(cx, |copilot, cx| {
159 copilot.completions_cycling(&buffer, cursor_position, cx)
160 })?
161 .await?;
162
163 this.update(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 Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
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 Point,
275 language_settings::{
276 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
277 WordsCompletionMode,
278 },
279 };
280 use project::Project;
281 use serde_json::json;
282 use settings::SettingsStore;
283 use std::future::Future;
284 use util::{
285 path,
286 test::{TextRangeMarker, marked_text_ranges_by},
287 };
288
289 #[gpui::test(iterations = 10)]
290 async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
291 // flaky
292 init_test(cx, |settings| {
293 settings.defaults.completions = Some(CompletionSettings {
294 words: WordsCompletionMode::Disabled,
295 lsp: true,
296 lsp_fetch_timeout_ms: 0,
297 });
298 });
299
300 let (copilot, copilot_lsp) = Copilot::fake(cx);
301 let mut cx = EditorLspTestContext::new_rust(
302 lsp::ServerCapabilities {
303 completion_provider: Some(lsp::CompletionOptions {
304 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
305 ..Default::default()
306 }),
307 ..Default::default()
308 },
309 cx,
310 )
311 .await;
312 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
313 cx.update_editor(|editor, window, cx| {
314 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
315 });
316
317 cx.set_state(indoc! {"
318 oneˇ
319 two
320 three
321 "});
322 cx.simulate_keystroke(".");
323 drop(handle_completion_request(
324 &mut cx,
325 indoc! {"
326 one.|<>
327 two
328 three
329 "},
330 vec!["completion_a", "completion_b"],
331 ));
332 handle_copilot_completion_request(
333 &copilot_lsp,
334 vec![crate::request::Completion {
335 text: "one.copilot1".into(),
336 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
337 ..Default::default()
338 }],
339 vec![],
340 );
341 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
342 cx.update_editor(|editor, window, cx| {
343 assert!(editor.context_menu_visible());
344 assert!(!editor.has_active_inline_completion());
345 // Since we have both, the copilot suggestion is not shown inline
346 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
347 assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
348
349 // Confirming a non-copilot completion inserts it and hides the context menu, without showing
350 // the copilot suggestion afterwards.
351 editor
352 .confirm_completion(&Default::default(), window, cx)
353 .unwrap()
354 .detach();
355 assert!(!editor.context_menu_visible());
356 assert!(!editor.has_active_inline_completion());
357 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
358 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
359 });
360
361 // Reset editor and only return copilot suggestions
362 cx.set_state(indoc! {"
363 oneˇ
364 two
365 three
366 "});
367 cx.simulate_keystroke(".");
368
369 drop(handle_completion_request(
370 &mut cx,
371 indoc! {"
372 one.|<>
373 two
374 three
375 "},
376 vec![],
377 ));
378 handle_copilot_completion_request(
379 &copilot_lsp,
380 vec![crate::request::Completion {
381 text: "one.copilot1".into(),
382 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
383 ..Default::default()
384 }],
385 vec![],
386 );
387 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
388 cx.update_editor(|editor, _, cx| {
389 assert!(!editor.context_menu_visible());
390 assert!(editor.has_active_inline_completion());
391 // Since only the copilot is available, it's shown inline
392 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
393 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
394 });
395
396 // Ensure existing edit prediction is interpolated when inserting again.
397 cx.simulate_keystroke("c");
398 executor.run_until_parked();
399 cx.update_editor(|editor, _, cx| {
400 assert!(!editor.context_menu_visible());
401 assert!(editor.has_active_inline_completion());
402 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
403 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
404 });
405
406 // After debouncing, new Copilot completions should be requested.
407 handle_copilot_completion_request(
408 &copilot_lsp,
409 vec![crate::request::Completion {
410 text: "one.copilot2".into(),
411 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
412 ..Default::default()
413 }],
414 vec![],
415 );
416 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
417 cx.update_editor(|editor, window, cx| {
418 assert!(!editor.context_menu_visible());
419 assert!(editor.has_active_inline_completion());
420 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
421 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
422
423 // Canceling should remove the active Copilot suggestion.
424 editor.cancel(&Default::default(), window, cx);
425 assert!(!editor.has_active_inline_completion());
426 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
427 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
428
429 // After canceling, tabbing shouldn't insert the previously shown suggestion.
430 editor.tab(&Default::default(), window, cx);
431 assert!(!editor.has_active_inline_completion());
432 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
433 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
434
435 // When undoing the previously active suggestion is shown again.
436 editor.undo(&Default::default(), window, cx);
437 assert!(editor.has_active_inline_completion());
438 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
439 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
440 });
441
442 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
443 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
444 cx.update_editor(|editor, window, cx| {
445 assert!(editor.has_active_inline_completion());
446 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
447 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
448
449 // AcceptEditPrediction when there is an active suggestion inserts it.
450 editor.accept_edit_prediction(&Default::default(), window, cx);
451 assert!(!editor.has_active_inline_completion());
452 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
453 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
454
455 // When undoing the previously active suggestion is shown again.
456 editor.undo(&Default::default(), window, cx);
457 assert!(editor.has_active_inline_completion());
458 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
459 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
460
461 // Hide suggestion.
462 editor.cancel(&Default::default(), window, cx);
463 assert!(!editor.has_active_inline_completion());
464 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
465 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
466 });
467
468 // If an edit occurs outside of this editor but no suggestion is being shown,
469 // we won't make it visible.
470 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
471 cx.update_editor(|editor, _, cx| {
472 assert!(!editor.has_active_inline_completion());
473 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
474 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
475 });
476
477 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
478 cx.update_editor(|editor, window, cx| {
479 editor.set_text("fn foo() {\n \n}", window, cx);
480 editor.change_selections(None, window, cx, |s| {
481 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
482 });
483 });
484 handle_copilot_completion_request(
485 &copilot_lsp,
486 vec![crate::request::Completion {
487 text: " let x = 4;".into(),
488 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
489 ..Default::default()
490 }],
491 vec![],
492 );
493
494 cx.update_editor(|editor, window, cx| {
495 editor.next_edit_prediction(&Default::default(), window, cx)
496 });
497 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
498 cx.update_editor(|editor, window, cx| {
499 assert!(editor.has_active_inline_completion());
500 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
501 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
502
503 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
504 editor.tab(&Default::default(), window, cx);
505 assert!(editor.has_active_inline_completion());
506 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
507 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
508
509 // Using AcceptEditPrediction again accepts the suggestion.
510 editor.accept_edit_prediction(&Default::default(), window, cx);
511 assert!(!editor.has_active_inline_completion());
512 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
513 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
514 });
515 }
516
517 #[gpui::test(iterations = 10)]
518 async fn test_accept_partial_copilot_suggestion(
519 executor: BackgroundExecutor,
520 cx: &mut TestAppContext,
521 ) {
522 // flaky
523 init_test(cx, |settings| {
524 settings.defaults.completions = Some(CompletionSettings {
525 words: WordsCompletionMode::Disabled,
526 lsp: true,
527 lsp_fetch_timeout_ms: 0,
528 });
529 });
530
531 let (copilot, copilot_lsp) = Copilot::fake(cx);
532 let mut cx = EditorLspTestContext::new_rust(
533 lsp::ServerCapabilities {
534 completion_provider: Some(lsp::CompletionOptions {
535 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
536 ..Default::default()
537 }),
538 ..Default::default()
539 },
540 cx,
541 )
542 .await;
543 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
544 cx.update_editor(|editor, window, cx| {
545 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
546 });
547
548 // Setup the editor with a completion request.
549 cx.set_state(indoc! {"
550 oneˇ
551 two
552 three
553 "});
554 cx.simulate_keystroke(".");
555 drop(handle_completion_request(
556 &mut cx,
557 indoc! {"
558 one.|<>
559 two
560 three
561 "},
562 vec![],
563 ));
564 handle_copilot_completion_request(
565 &copilot_lsp,
566 vec![crate::request::Completion {
567 text: "one.copilot1".into(),
568 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
569 ..Default::default()
570 }],
571 vec![],
572 );
573 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
574 cx.update_editor(|editor, window, cx| {
575 assert!(editor.has_active_inline_completion());
576
577 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
578 editor.accept_partial_inline_completion(&Default::default(), window, cx);
579 assert!(editor.has_active_inline_completion());
580 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
581 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
582
583 // Accepting next word should accept the non-word and copilot suggestion should be gone
584 editor.accept_partial_inline_completion(&Default::default(), window, cx);
585 assert!(!editor.has_active_inline_completion());
586 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
587 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
588 });
589
590 // Reset the editor and check non-word and whitespace completion
591 cx.set_state(indoc! {"
592 oneˇ
593 two
594 three
595 "});
596 cx.simulate_keystroke(".");
597 drop(handle_completion_request(
598 &mut cx,
599 indoc! {"
600 one.|<>
601 two
602 three
603 "},
604 vec![],
605 ));
606 handle_copilot_completion_request(
607 &copilot_lsp,
608 vec![crate::request::Completion {
609 text: "one.123. copilot\n 456".into(),
610 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
611 ..Default::default()
612 }],
613 vec![],
614 );
615 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
616 cx.update_editor(|editor, window, cx| {
617 assert!(editor.has_active_inline_completion());
618
619 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
620 editor.accept_partial_inline_completion(&Default::default(), window, cx);
621 assert!(editor.has_active_inline_completion());
622 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
623 assert_eq!(
624 editor.display_text(cx),
625 "one.123. copilot\n 456\ntwo\nthree\n"
626 );
627
628 // Accepting next word should accept the next word and copilot suggestion should still exist
629 editor.accept_partial_inline_completion(&Default::default(), window, cx);
630 assert!(editor.has_active_inline_completion());
631 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
632 assert_eq!(
633 editor.display_text(cx),
634 "one.123. copilot\n 456\ntwo\nthree\n"
635 );
636
637 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
638 editor.accept_partial_inline_completion(&Default::default(), window, cx);
639 assert!(!editor.has_active_inline_completion());
640 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
641 assert_eq!(
642 editor.display_text(cx),
643 "one.123. copilot\n 456\ntwo\nthree\n"
644 );
645 });
646 }
647
648 #[gpui::test]
649 async fn test_copilot_completion_invalidation(
650 executor: BackgroundExecutor,
651 cx: &mut TestAppContext,
652 ) {
653 init_test(cx, |_| {});
654
655 let (copilot, copilot_lsp) = Copilot::fake(cx);
656 let mut cx = EditorLspTestContext::new_rust(
657 lsp::ServerCapabilities {
658 completion_provider: Some(lsp::CompletionOptions {
659 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
660 ..Default::default()
661 }),
662 ..Default::default()
663 },
664 cx,
665 )
666 .await;
667 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
668 cx.update_editor(|editor, window, cx| {
669 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
670 });
671
672 cx.set_state(indoc! {"
673 one
674 twˇ
675 three
676 "});
677
678 handle_copilot_completion_request(
679 &copilot_lsp,
680 vec![crate::request::Completion {
681 text: "two.foo()".into(),
682 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
683 ..Default::default()
684 }],
685 vec![],
686 );
687 cx.update_editor(|editor, window, cx| {
688 editor.next_edit_prediction(&Default::default(), window, cx)
689 });
690 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
691 cx.update_editor(|editor, window, cx| {
692 assert!(editor.has_active_inline_completion());
693 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
694 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
695
696 editor.backspace(&Default::default(), window, cx);
697 assert!(editor.has_active_inline_completion());
698 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
699 assert_eq!(editor.text(cx), "one\nt\nthree\n");
700
701 editor.backspace(&Default::default(), window, cx);
702 assert!(editor.has_active_inline_completion());
703 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
704 assert_eq!(editor.text(cx), "one\n\nthree\n");
705
706 // Deleting across the original suggestion range invalidates it.
707 editor.backspace(&Default::default(), window, cx);
708 assert!(!editor.has_active_inline_completion());
709 assert_eq!(editor.display_text(cx), "one\nthree\n");
710 assert_eq!(editor.text(cx), "one\nthree\n");
711
712 // Undoing the deletion restores the suggestion.
713 editor.undo(&Default::default(), window, cx);
714 assert!(editor.has_active_inline_completion());
715 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
716 assert_eq!(editor.text(cx), "one\n\nthree\n");
717 });
718 }
719
720 #[gpui::test]
721 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
722 init_test(cx, |_| {});
723
724 let (copilot, copilot_lsp) = Copilot::fake(cx);
725
726 let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx));
727 let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx));
728 let multibuffer = cx.new(|cx| {
729 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
730 multibuffer.push_excerpts(
731 buffer_1.clone(),
732 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
733 cx,
734 );
735 multibuffer.push_excerpts(
736 buffer_2.clone(),
737 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
738 cx,
739 );
740 multibuffer
741 });
742 let editor =
743 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
744 editor
745 .update(cx, |editor, window, cx| {
746 use gpui::Focusable;
747 window.focus(&editor.focus_handle(cx));
748 })
749 .unwrap();
750 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
751 editor
752 .update(cx, |editor, window, cx| {
753 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
754 })
755 .unwrap();
756
757 handle_copilot_completion_request(
758 &copilot_lsp,
759 vec![crate::request::Completion {
760 text: "b = 2 + a".into(),
761 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
762 ..Default::default()
763 }],
764 vec![],
765 );
766 _ = editor.update(cx, |editor, window, cx| {
767 // Ensure copilot suggestions are shown for the first excerpt.
768 editor.change_selections(None, window, cx, |s| {
769 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
770 });
771 editor.next_edit_prediction(&Default::default(), window, cx);
772 });
773 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
774 _ = editor.update(cx, |editor, _, cx| {
775 assert!(editor.has_active_inline_completion());
776 assert_eq!(
777 editor.display_text(cx),
778 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
779 );
780 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
781 });
782
783 handle_copilot_completion_request(
784 &copilot_lsp,
785 vec![crate::request::Completion {
786 text: "d = 4 + c".into(),
787 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
788 ..Default::default()
789 }],
790 vec![],
791 );
792 _ = editor.update(cx, |editor, window, cx| {
793 // Move to another excerpt, ensuring the suggestion gets cleared.
794 editor.change_selections(None, window, cx, |s| {
795 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
796 });
797 assert!(!editor.has_active_inline_completion());
798 assert_eq!(
799 editor.display_text(cx),
800 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
801 );
802 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
803
804 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
805 editor.handle_input(" ", window, cx);
806 assert!(!editor.has_active_inline_completion());
807 assert_eq!(
808 editor.display_text(cx),
809 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
810 );
811 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
812 });
813
814 // Ensure the new suggestion is displayed when the debounce timeout expires.
815 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
816 _ = editor.update(cx, |editor, _, cx| {
817 assert!(editor.has_active_inline_completion());
818 assert_eq!(
819 editor.display_text(cx),
820 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
821 );
822 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
823 });
824 }
825
826 #[gpui::test]
827 async fn test_copilot_does_not_prevent_completion_triggers(
828 executor: BackgroundExecutor,
829 cx: &mut TestAppContext,
830 ) {
831 init_test(cx, |_| {});
832
833 let (copilot, copilot_lsp) = Copilot::fake(cx);
834 let mut cx = EditorLspTestContext::new_rust(
835 lsp::ServerCapabilities {
836 completion_provider: Some(lsp::CompletionOptions {
837 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
838 ..lsp::CompletionOptions::default()
839 }),
840 ..lsp::ServerCapabilities::default()
841 },
842 cx,
843 )
844 .await;
845 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
846 cx.update_editor(|editor, window, cx| {
847 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
848 });
849
850 cx.set_state(indoc! {"
851 one
852 twˇ
853 three
854 "});
855
856 drop(handle_completion_request(
857 &mut cx,
858 indoc! {"
859 one
860 tw|<>
861 three
862 "},
863 vec!["completion_a", "completion_b"],
864 ));
865 handle_copilot_completion_request(
866 &copilot_lsp,
867 vec![crate::request::Completion {
868 text: "two.foo()".into(),
869 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
870 ..Default::default()
871 }],
872 vec![],
873 );
874 cx.update_editor(|editor, window, cx| {
875 editor.next_edit_prediction(&Default::default(), window, cx)
876 });
877 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
878 cx.update_editor(|editor, _, cx| {
879 assert!(!editor.context_menu_visible());
880 assert!(editor.has_active_inline_completion());
881 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
882 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
883 });
884
885 cx.simulate_keystroke("o");
886 drop(handle_completion_request(
887 &mut cx,
888 indoc! {"
889 one
890 two|<>
891 three
892 "},
893 vec!["completion_a_2", "completion_b_2"],
894 ));
895 handle_copilot_completion_request(
896 &copilot_lsp,
897 vec![crate::request::Completion {
898 text: "two.foo()".into(),
899 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
900 ..Default::default()
901 }],
902 vec![],
903 );
904 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
905 cx.update_editor(|editor, _, cx| {
906 assert!(!editor.context_menu_visible());
907 assert!(editor.has_active_inline_completion());
908 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
909 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
910 });
911
912 cx.simulate_keystroke(".");
913 drop(handle_completion_request(
914 &mut cx,
915 indoc! {"
916 one
917 two.|<>
918 three
919 "},
920 vec!["something_else()"],
921 ));
922 handle_copilot_completion_request(
923 &copilot_lsp,
924 vec![crate::request::Completion {
925 text: "two.foo()".into(),
926 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
927 ..Default::default()
928 }],
929 vec![],
930 );
931 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
932 cx.update_editor(|editor, _, cx| {
933 assert!(editor.context_menu_visible());
934 assert!(!editor.has_active_inline_completion(),);
935 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
936 });
937 }
938
939 #[gpui::test]
940 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
941 init_test(cx, |settings| {
942 settings
943 .edit_predictions
944 .get_or_insert(Default::default())
945 .disabled_globs = Some(vec![".env*".to_string()]);
946 });
947
948 let (copilot, copilot_lsp) = Copilot::fake(cx);
949
950 let fs = FakeFs::new(cx.executor());
951 fs.insert_tree(
952 path!("/test"),
953 json!({
954 ".env": "SECRET=something\n",
955 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
956 }),
957 )
958 .await;
959 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
960
961 let private_buffer = project
962 .update(cx, |project, cx| {
963 project.open_local_buffer(path!("/test/.env"), cx)
964 })
965 .await
966 .unwrap();
967 let public_buffer = project
968 .update(cx, |project, cx| {
969 project.open_local_buffer(path!("/test/README.md"), cx)
970 })
971 .await
972 .unwrap();
973
974 let multibuffer = cx.new(|cx| {
975 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
976 multibuffer.push_excerpts(
977 private_buffer.clone(),
978 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
979 cx,
980 );
981 multibuffer.push_excerpts(
982 public_buffer.clone(),
983 [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
984 cx,
985 );
986 multibuffer
987 });
988 let editor =
989 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
990 editor
991 .update(cx, |editor, window, cx| {
992 use gpui::Focusable;
993 window.focus(&editor.focus_handle(cx))
994 })
995 .unwrap();
996 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
997 editor
998 .update(cx, |editor, window, cx| {
999 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1000 })
1001 .unwrap();
1002
1003 let mut copilot_requests = copilot_lsp
1004 .set_request_handler::<crate::request::GetCompletions, _, _>(
1005 move |_params, _cx| async move {
1006 Ok(crate::request::GetCompletionsResult {
1007 completions: vec![crate::request::Completion {
1008 text: "next line".into(),
1009 range: lsp::Range::new(
1010 lsp::Position::new(1, 0),
1011 lsp::Position::new(1, 0),
1012 ),
1013 ..Default::default()
1014 }],
1015 })
1016 },
1017 );
1018
1019 _ = editor.update(cx, |editor, window, cx| {
1020 editor.change_selections(None, window, cx, |selections| {
1021 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1022 });
1023 editor.refresh_inline_completion(true, false, window, cx);
1024 });
1025
1026 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1027 assert!(copilot_requests.try_next().is_err());
1028
1029 _ = editor.update(cx, |editor, window, cx| {
1030 editor.change_selections(None, window, cx, |s| {
1031 s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1032 });
1033 editor.refresh_inline_completion(true, false, window, cx);
1034 });
1035
1036 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1037 assert!(copilot_requests.try_next().is_ok());
1038 }
1039
1040 fn handle_copilot_completion_request(
1041 lsp: &lsp::FakeLanguageServer,
1042 completions: Vec<crate::request::Completion>,
1043 completions_cycling: Vec<crate::request::Completion>,
1044 ) {
1045 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1046 let completions = completions.clone();
1047 async move {
1048 Ok(crate::request::GetCompletionsResult {
1049 completions: completions.clone(),
1050 })
1051 }
1052 });
1053 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1054 move |_params, _cx| {
1055 let completions_cycling = completions_cycling.clone();
1056 async move {
1057 Ok(crate::request::GetCompletionsResult {
1058 completions: completions_cycling.clone(),
1059 })
1060 }
1061 },
1062 );
1063 }
1064
1065 fn handle_completion_request(
1066 cx: &mut EditorLspTestContext,
1067 marked_string: &str,
1068 completions: Vec<&'static str>,
1069 ) -> impl Future<Output = ()> {
1070 let complete_from_marker: TextRangeMarker = '|'.into();
1071 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1072 let (_, mut marked_ranges) = marked_text_ranges_by(
1073 marked_string,
1074 vec![complete_from_marker.clone(), replace_range_marker.clone()],
1075 );
1076
1077 let complete_from_position =
1078 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
1079 let replace_range =
1080 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1081
1082 let mut request =
1083 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1084 let completions = completions.clone();
1085 async move {
1086 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1087 assert_eq!(
1088 params.text_document_position.position,
1089 complete_from_position
1090 );
1091 Ok(Some(lsp::CompletionResponse::Array(
1092 completions
1093 .iter()
1094 .map(|completion_text| lsp::CompletionItem {
1095 label: completion_text.to_string(),
1096 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1097 range: replace_range,
1098 new_text: completion_text.to_string(),
1099 })),
1100 ..Default::default()
1101 })
1102 .collect(),
1103 )))
1104 }
1105 });
1106
1107 async move {
1108 request.next().await;
1109 }
1110 }
1111
1112 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1113 cx.update(|cx| {
1114 let store = SettingsStore::test(cx);
1115 cx.set_global(store);
1116 theme::init(theme::LoadThemes::JustBase, cx);
1117 client::init_settings(cx);
1118 language::init(cx);
1119 editor::init_settings(cx);
1120 Project::init_settings(cx);
1121 workspace::init_settings(cx);
1122 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1123 store.update_user_settings::<AllLanguageSettings>(cx, f);
1124 });
1125 });
1126 }
1127}