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