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 {
733 context: Point::new(0, 0)..Point::new(2, 0),
734 primary: None,
735 }],
736 cx,
737 );
738 multibuffer.push_excerpts(
739 buffer_2.clone(),
740 [ExcerptRange {
741 context: Point::new(0, 0)..Point::new(2, 0),
742 primary: None,
743 }],
744 cx,
745 );
746 multibuffer
747 });
748 let editor =
749 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
750 editor
751 .update(cx, |editor, window, cx| {
752 use gpui::Focusable;
753 window.focus(&editor.focus_handle(cx));
754 })
755 .unwrap();
756 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
757 editor
758 .update(cx, |editor, window, cx| {
759 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
760 })
761 .unwrap();
762
763 handle_copilot_completion_request(
764 &copilot_lsp,
765 vec![crate::request::Completion {
766 text: "b = 2 + a".into(),
767 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
768 ..Default::default()
769 }],
770 vec![],
771 );
772 _ = editor.update(cx, |editor, window, cx| {
773 // Ensure copilot suggestions are shown for the first excerpt.
774 editor.change_selections(None, window, cx, |s| {
775 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
776 });
777 editor.next_edit_prediction(&Default::default(), window, cx);
778 });
779 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
780 _ = editor.update(cx, |editor, _, cx| {
781 assert!(editor.has_active_inline_completion());
782 assert_eq!(
783 editor.display_text(cx),
784 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
785 );
786 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
787 });
788
789 handle_copilot_completion_request(
790 &copilot_lsp,
791 vec![crate::request::Completion {
792 text: "d = 4 + c".into(),
793 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
794 ..Default::default()
795 }],
796 vec![],
797 );
798 _ = editor.update(cx, |editor, window, cx| {
799 // Move to another excerpt, ensuring the suggestion gets cleared.
800 editor.change_selections(None, window, cx, |s| {
801 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
802 });
803 assert!(!editor.has_active_inline_completion());
804 assert_eq!(
805 editor.display_text(cx),
806 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
807 );
808 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
809
810 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
811 editor.handle_input(" ", window, cx);
812 assert!(!editor.has_active_inline_completion());
813 assert_eq!(
814 editor.display_text(cx),
815 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
816 );
817 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
818 });
819
820 // Ensure the new suggestion is displayed when the debounce timeout expires.
821 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
822 _ = editor.update(cx, |editor, _, cx| {
823 assert!(editor.has_active_inline_completion());
824 assert_eq!(
825 editor.display_text(cx),
826 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
827 );
828 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
829 });
830 }
831
832 #[gpui::test]
833 async fn test_copilot_does_not_prevent_completion_triggers(
834 executor: BackgroundExecutor,
835 cx: &mut TestAppContext,
836 ) {
837 init_test(cx, |_| {});
838
839 let (copilot, copilot_lsp) = Copilot::fake(cx);
840 let mut cx = EditorLspTestContext::new_rust(
841 lsp::ServerCapabilities {
842 completion_provider: Some(lsp::CompletionOptions {
843 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
844 ..lsp::CompletionOptions::default()
845 }),
846 ..lsp::ServerCapabilities::default()
847 },
848 cx,
849 )
850 .await;
851 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
852 cx.update_editor(|editor, window, cx| {
853 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
854 });
855
856 cx.set_state(indoc! {"
857 one
858 twˇ
859 three
860 "});
861
862 drop(handle_completion_request(
863 &mut cx,
864 indoc! {"
865 one
866 tw|<>
867 three
868 "},
869 vec!["completion_a", "completion_b"],
870 ));
871 handle_copilot_completion_request(
872 &copilot_lsp,
873 vec![crate::request::Completion {
874 text: "two.foo()".into(),
875 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
876 ..Default::default()
877 }],
878 vec![],
879 );
880 cx.update_editor(|editor, window, cx| {
881 editor.next_edit_prediction(&Default::default(), window, cx)
882 });
883 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
884 cx.update_editor(|editor, _, cx| {
885 assert!(!editor.context_menu_visible());
886 assert!(editor.has_active_inline_completion());
887 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
888 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
889 });
890
891 cx.simulate_keystroke("o");
892 drop(handle_completion_request(
893 &mut cx,
894 indoc! {"
895 one
896 two|<>
897 three
898 "},
899 vec!["completion_a_2", "completion_b_2"],
900 ));
901 handle_copilot_completion_request(
902 &copilot_lsp,
903 vec![crate::request::Completion {
904 text: "two.foo()".into(),
905 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
906 ..Default::default()
907 }],
908 vec![],
909 );
910 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
911 cx.update_editor(|editor, _, cx| {
912 assert!(!editor.context_menu_visible());
913 assert!(editor.has_active_inline_completion());
914 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
915 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
916 });
917
918 cx.simulate_keystroke(".");
919 drop(handle_completion_request(
920 &mut cx,
921 indoc! {"
922 one
923 two.|<>
924 three
925 "},
926 vec!["something_else()"],
927 ));
928 handle_copilot_completion_request(
929 &copilot_lsp,
930 vec![crate::request::Completion {
931 text: "two.foo()".into(),
932 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
933 ..Default::default()
934 }],
935 vec![],
936 );
937 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
938 cx.update_editor(|editor, _, cx| {
939 assert!(editor.context_menu_visible());
940 assert!(!editor.has_active_inline_completion(),);
941 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
942 });
943 }
944
945 #[gpui::test]
946 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
947 init_test(cx, |settings| {
948 settings
949 .edit_predictions
950 .get_or_insert(Default::default())
951 .disabled_globs = Some(vec![".env*".to_string()]);
952 });
953
954 let (copilot, copilot_lsp) = Copilot::fake(cx);
955
956 let fs = FakeFs::new(cx.executor());
957 fs.insert_tree(
958 path!("/test"),
959 json!({
960 ".env": "SECRET=something\n",
961 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
962 }),
963 )
964 .await;
965 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
966
967 let private_buffer = project
968 .update(cx, |project, cx| {
969 project.open_local_buffer(path!("/test/.env"), cx)
970 })
971 .await
972 .unwrap();
973 let public_buffer = project
974 .update(cx, |project, cx| {
975 project.open_local_buffer(path!("/test/README.md"), cx)
976 })
977 .await
978 .unwrap();
979
980 let multibuffer = cx.new(|cx| {
981 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
982 multibuffer.push_excerpts(
983 private_buffer.clone(),
984 [ExcerptRange {
985 context: Point::new(0, 0)..Point::new(1, 0),
986 primary: None,
987 }],
988 cx,
989 );
990 multibuffer.push_excerpts(
991 public_buffer.clone(),
992 [ExcerptRange {
993 context: Point::new(0, 0)..Point::new(6, 0),
994 primary: None,
995 }],
996 cx,
997 );
998 multibuffer
999 });
1000 let editor =
1001 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
1002 editor
1003 .update(cx, |editor, window, cx| {
1004 use gpui::Focusable;
1005 window.focus(&editor.focus_handle(cx))
1006 })
1007 .unwrap();
1008 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
1009 editor
1010 .update(cx, |editor, window, cx| {
1011 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1012 })
1013 .unwrap();
1014
1015 let mut copilot_requests = copilot_lsp
1016 .set_request_handler::<crate::request::GetCompletions, _, _>(
1017 move |_params, _cx| async move {
1018 Ok(crate::request::GetCompletionsResult {
1019 completions: vec![crate::request::Completion {
1020 text: "next line".into(),
1021 range: lsp::Range::new(
1022 lsp::Position::new(1, 0),
1023 lsp::Position::new(1, 0),
1024 ),
1025 ..Default::default()
1026 }],
1027 })
1028 },
1029 );
1030
1031 _ = editor.update(cx, |editor, window, cx| {
1032 editor.change_selections(None, window, cx, |selections| {
1033 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1034 });
1035 editor.refresh_inline_completion(true, false, window, cx);
1036 });
1037
1038 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1039 assert!(copilot_requests.try_next().is_err());
1040
1041 _ = editor.update(cx, |editor, window, cx| {
1042 editor.change_selections(None, window, cx, |s| {
1043 s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1044 });
1045 editor.refresh_inline_completion(true, false, window, cx);
1046 });
1047
1048 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1049 assert!(copilot_requests.try_next().is_ok());
1050 }
1051
1052 fn handle_copilot_completion_request(
1053 lsp: &lsp::FakeLanguageServer,
1054 completions: Vec<crate::request::Completion>,
1055 completions_cycling: Vec<crate::request::Completion>,
1056 ) {
1057 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1058 let completions = completions.clone();
1059 async move {
1060 Ok(crate::request::GetCompletionsResult {
1061 completions: completions.clone(),
1062 })
1063 }
1064 });
1065 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1066 move |_params, _cx| {
1067 let completions_cycling = completions_cycling.clone();
1068 async move {
1069 Ok(crate::request::GetCompletionsResult {
1070 completions: completions_cycling.clone(),
1071 })
1072 }
1073 },
1074 );
1075 }
1076
1077 fn handle_completion_request(
1078 cx: &mut EditorLspTestContext,
1079 marked_string: &str,
1080 completions: Vec<&'static str>,
1081 ) -> impl Future<Output = ()> {
1082 let complete_from_marker: TextRangeMarker = '|'.into();
1083 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1084 let (_, mut marked_ranges) = marked_text_ranges_by(
1085 marked_string,
1086 vec![complete_from_marker.clone(), replace_range_marker.clone()],
1087 );
1088
1089 let complete_from_position =
1090 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
1091 let replace_range =
1092 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1093
1094 let mut request =
1095 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1096 let completions = completions.clone();
1097 async move {
1098 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1099 assert_eq!(
1100 params.text_document_position.position,
1101 complete_from_position
1102 );
1103 Ok(Some(lsp::CompletionResponse::Array(
1104 completions
1105 .iter()
1106 .map(|completion_text| lsp::CompletionItem {
1107 label: completion_text.to_string(),
1108 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1109 range: replace_range,
1110 new_text: completion_text.to_string(),
1111 })),
1112 ..Default::default()
1113 })
1114 .collect(),
1115 )))
1116 }
1117 });
1118
1119 async move {
1120 request.next().await;
1121 }
1122 }
1123
1124 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1125 cx.update(|cx| {
1126 let store = SettingsStore::test(cx);
1127 cx.set_global(store);
1128 theme::init(theme::LoadThemes::JustBase, cx);
1129 client::init_settings(cx);
1130 language::init(cx);
1131 editor::init_settings(cx);
1132 Project::init_settings(cx);
1133 workspace::init_settings(cx);
1134 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1135 store.update_user_settings::<AllLanguageSettings>(cx, f);
1136 });
1137 });
1138 }
1139}