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