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, LspInsertMode,
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 lsp_insert_mode: LspInsertMode::Insert,
298 });
299 });
300
301 let (copilot, copilot_lsp) = Copilot::fake(cx);
302 let mut cx = EditorLspTestContext::new_rust(
303 lsp::ServerCapabilities {
304 completion_provider: Some(lsp::CompletionOptions {
305 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
306 ..Default::default()
307 }),
308 ..Default::default()
309 },
310 cx,
311 )
312 .await;
313 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
314 cx.update_editor(|editor, window, cx| {
315 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
316 });
317
318 cx.set_state(indoc! {"
319 oneˇ
320 two
321 three
322 "});
323 cx.simulate_keystroke(".");
324 drop(handle_completion_request(
325 &mut cx,
326 indoc! {"
327 one.|<>
328 two
329 three
330 "},
331 vec!["completion_a", "completion_b"],
332 ));
333 handle_copilot_completion_request(
334 &copilot_lsp,
335 vec![crate::request::Completion {
336 text: "one.copilot1".into(),
337 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
338 ..Default::default()
339 }],
340 vec![],
341 );
342 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
343 cx.update_editor(|editor, window, cx| {
344 assert!(editor.context_menu_visible());
345 assert!(!editor.has_active_inline_completion());
346 // Since we have both, the copilot suggestion is not shown inline
347 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
348 assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
349
350 // Confirming a non-copilot completion inserts it and hides the context menu, without showing
351 // the copilot suggestion afterwards.
352 editor
353 .confirm_completion(&Default::default(), window, cx)
354 .unwrap()
355 .detach();
356 assert!(!editor.context_menu_visible());
357 assert!(!editor.has_active_inline_completion());
358 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
359 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
360 });
361
362 // Reset editor and only return copilot suggestions
363 cx.set_state(indoc! {"
364 oneˇ
365 two
366 three
367 "});
368 cx.simulate_keystroke(".");
369
370 drop(handle_completion_request(
371 &mut cx,
372 indoc! {"
373 one.|<>
374 two
375 three
376 "},
377 vec![],
378 ));
379 handle_copilot_completion_request(
380 &copilot_lsp,
381 vec![crate::request::Completion {
382 text: "one.copilot1".into(),
383 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
384 ..Default::default()
385 }],
386 vec![],
387 );
388 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
389 cx.update_editor(|editor, _, cx| {
390 assert!(!editor.context_menu_visible());
391 assert!(editor.has_active_inline_completion());
392 // Since only the copilot is available, it's shown inline
393 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
394 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
395 });
396
397 // Ensure existing edit prediction is interpolated when inserting again.
398 cx.simulate_keystroke("c");
399 executor.run_until_parked();
400 cx.update_editor(|editor, _, cx| {
401 assert!(!editor.context_menu_visible());
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_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
422 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
423
424 // Canceling should remove the active Copilot suggestion.
425 editor.cancel(&Default::default(), window, cx);
426 assert!(!editor.has_active_inline_completion());
427 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
428 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
429
430 // After canceling, tabbing shouldn't insert the previously shown suggestion.
431 editor.tab(&Default::default(), window, cx);
432 assert!(!editor.has_active_inline_completion());
433 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
434 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
435
436 // When undoing the previously active suggestion is shown again.
437 editor.undo(&Default::default(), window, cx);
438 assert!(editor.has_active_inline_completion());
439 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
440 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
441 });
442
443 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
444 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
445 cx.update_editor(|editor, window, cx| {
446 assert!(editor.has_active_inline_completion());
447 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
448 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
449
450 // AcceptEditPrediction when there is an active suggestion inserts it.
451 editor.accept_edit_prediction(&Default::default(), window, cx);
452 assert!(!editor.has_active_inline_completion());
453 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
454 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
455
456 // When undoing the previously active suggestion is shown again.
457 editor.undo(&Default::default(), window, cx);
458 assert!(editor.has_active_inline_completion());
459 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
460 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
461
462 // Hide suggestion.
463 editor.cancel(&Default::default(), window, cx);
464 assert!(!editor.has_active_inline_completion());
465 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
466 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
467 });
468
469 // If an edit occurs outside of this editor but no suggestion is being shown,
470 // we won't make it visible.
471 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
472 cx.update_editor(|editor, _, cx| {
473 assert!(!editor.has_active_inline_completion());
474 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
475 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
476 });
477
478 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
479 cx.update_editor(|editor, window, cx| {
480 editor.set_text("fn foo() {\n \n}", window, cx);
481 editor.change_selections(None, window, cx, |s| {
482 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
483 });
484 });
485 handle_copilot_completion_request(
486 &copilot_lsp,
487 vec![crate::request::Completion {
488 text: " let x = 4;".into(),
489 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
490 ..Default::default()
491 }],
492 vec![],
493 );
494
495 cx.update_editor(|editor, window, cx| {
496 editor.next_edit_prediction(&Default::default(), window, cx)
497 });
498 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
499 cx.update_editor(|editor, window, cx| {
500 assert!(editor.has_active_inline_completion());
501 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
502 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
503
504 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
505 editor.tab(&Default::default(), window, cx);
506 assert!(editor.has_active_inline_completion());
507 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
508 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
509
510 // Using AcceptEditPrediction again accepts the suggestion.
511 editor.accept_edit_prediction(&Default::default(), window, cx);
512 assert!(!editor.has_active_inline_completion());
513 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
514 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
515 });
516 }
517
518 #[gpui::test(iterations = 10)]
519 async fn test_accept_partial_copilot_suggestion(
520 executor: BackgroundExecutor,
521 cx: &mut TestAppContext,
522 ) {
523 // flaky
524 init_test(cx, |settings| {
525 settings.defaults.completions = Some(CompletionSettings {
526 words: WordsCompletionMode::Disabled,
527 lsp: true,
528 lsp_fetch_timeout_ms: 0,
529 lsp_insert_mode: LspInsertMode::Insert,
530 });
531 });
532
533 let (copilot, copilot_lsp) = Copilot::fake(cx);
534 let mut cx = EditorLspTestContext::new_rust(
535 lsp::ServerCapabilities {
536 completion_provider: Some(lsp::CompletionOptions {
537 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
538 ..Default::default()
539 }),
540 ..Default::default()
541 },
542 cx,
543 )
544 .await;
545 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
546 cx.update_editor(|editor, window, cx| {
547 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
548 });
549
550 // Setup the editor with a completion request.
551 cx.set_state(indoc! {"
552 oneˇ
553 two
554 three
555 "});
556 cx.simulate_keystroke(".");
557 drop(handle_completion_request(
558 &mut cx,
559 indoc! {"
560 one.|<>
561 two
562 three
563 "},
564 vec![],
565 ));
566 handle_copilot_completion_request(
567 &copilot_lsp,
568 vec![crate::request::Completion {
569 text: "one.copilot1".into(),
570 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
571 ..Default::default()
572 }],
573 vec![],
574 );
575 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
576 cx.update_editor(|editor, window, cx| {
577 assert!(editor.has_active_inline_completion());
578
579 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
580 editor.accept_partial_inline_completion(&Default::default(), window, cx);
581 assert!(editor.has_active_inline_completion());
582 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
583 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
584
585 // Accepting next word should accept the non-word and copilot suggestion should be gone
586 editor.accept_partial_inline_completion(&Default::default(), window, cx);
587 assert!(!editor.has_active_inline_completion());
588 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
589 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
590 });
591
592 // Reset the editor and check non-word and whitespace completion
593 cx.set_state(indoc! {"
594 oneˇ
595 two
596 three
597 "});
598 cx.simulate_keystroke(".");
599 drop(handle_completion_request(
600 &mut cx,
601 indoc! {"
602 one.|<>
603 two
604 three
605 "},
606 vec![],
607 ));
608 handle_copilot_completion_request(
609 &copilot_lsp,
610 vec![crate::request::Completion {
611 text: "one.123. copilot\n 456".into(),
612 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
613 ..Default::default()
614 }],
615 vec![],
616 );
617 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
618 cx.update_editor(|editor, window, cx| {
619 assert!(editor.has_active_inline_completion());
620
621 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
622 editor.accept_partial_inline_completion(&Default::default(), window, cx);
623 assert!(editor.has_active_inline_completion());
624 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
625 assert_eq!(
626 editor.display_text(cx),
627 "one.123. copilot\n 456\ntwo\nthree\n"
628 );
629
630 // Accepting next word should accept the next word and copilot suggestion should still exist
631 editor.accept_partial_inline_completion(&Default::default(), window, cx);
632 assert!(editor.has_active_inline_completion());
633 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
634 assert_eq!(
635 editor.display_text(cx),
636 "one.123. copilot\n 456\ntwo\nthree\n"
637 );
638
639 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
640 editor.accept_partial_inline_completion(&Default::default(), window, cx);
641 assert!(!editor.has_active_inline_completion());
642 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
643 assert_eq!(
644 editor.display_text(cx),
645 "one.123. copilot\n 456\ntwo\nthree\n"
646 );
647 });
648 }
649
650 #[gpui::test]
651 async fn test_copilot_completion_invalidation(
652 executor: BackgroundExecutor,
653 cx: &mut TestAppContext,
654 ) {
655 init_test(cx, |_| {});
656
657 let (copilot, copilot_lsp) = Copilot::fake(cx);
658 let mut cx = EditorLspTestContext::new_rust(
659 lsp::ServerCapabilities {
660 completion_provider: Some(lsp::CompletionOptions {
661 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
662 ..Default::default()
663 }),
664 ..Default::default()
665 },
666 cx,
667 )
668 .await;
669 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
670 cx.update_editor(|editor, window, cx| {
671 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
672 });
673
674 cx.set_state(indoc! {"
675 one
676 twˇ
677 three
678 "});
679
680 handle_copilot_completion_request(
681 &copilot_lsp,
682 vec![crate::request::Completion {
683 text: "two.foo()".into(),
684 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
685 ..Default::default()
686 }],
687 vec![],
688 );
689 cx.update_editor(|editor, window, cx| {
690 editor.next_edit_prediction(&Default::default(), window, cx)
691 });
692 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
693 cx.update_editor(|editor, window, cx| {
694 assert!(editor.has_active_inline_completion());
695 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
696 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
697
698 editor.backspace(&Default::default(), window, cx);
699 assert!(editor.has_active_inline_completion());
700 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
701 assert_eq!(editor.text(cx), "one\nt\nthree\n");
702
703 editor.backspace(&Default::default(), window, cx);
704 assert!(editor.has_active_inline_completion());
705 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
706 assert_eq!(editor.text(cx), "one\n\nthree\n");
707
708 // Deleting across the original suggestion range invalidates it.
709 editor.backspace(&Default::default(), window, cx);
710 assert!(!editor.has_active_inline_completion());
711 assert_eq!(editor.display_text(cx), "one\nthree\n");
712 assert_eq!(editor.text(cx), "one\nthree\n");
713
714 // Undoing the deletion restores the suggestion.
715 editor.undo(&Default::default(), window, cx);
716 assert!(editor.has_active_inline_completion());
717 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
718 assert_eq!(editor.text(cx), "one\n\nthree\n");
719 });
720 }
721
722 #[gpui::test]
723 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
724 init_test(cx, |_| {});
725
726 let (copilot, copilot_lsp) = Copilot::fake(cx);
727
728 let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx));
729 let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx));
730 let multibuffer = cx.new(|cx| {
731 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
732 multibuffer.push_excerpts(
733 buffer_1.clone(),
734 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
735 cx,
736 );
737 multibuffer.push_excerpts(
738 buffer_2.clone(),
739 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
740 cx,
741 );
742 multibuffer
743 });
744 let editor =
745 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, 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_edit_prediction_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_edit_prediction(&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\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\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\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\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\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \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\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\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_edit_prediction_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_edit_prediction(&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.has_active_inline_completion(),);
937 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
938 });
939 }
940
941 #[gpui::test]
942 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
943 init_test(cx, |settings| {
944 settings
945 .edit_predictions
946 .get_or_insert(Default::default())
947 .disabled_globs = Some(vec![".env*".to_string()]);
948 });
949
950 let (copilot, copilot_lsp) = Copilot::fake(cx);
951
952 let fs = FakeFs::new(cx.executor());
953 fs.insert_tree(
954 path!("/test"),
955 json!({
956 ".env": "SECRET=something\n",
957 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
958 }),
959 )
960 .await;
961 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
962
963 let private_buffer = project
964 .update(cx, |project, cx| {
965 project.open_local_buffer(path!("/test/.env"), cx)
966 })
967 .await
968 .unwrap();
969 let public_buffer = project
970 .update(cx, |project, cx| {
971 project.open_local_buffer(path!("/test/README.md"), cx)
972 })
973 .await
974 .unwrap();
975
976 let multibuffer = cx.new(|cx| {
977 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
978 multibuffer.push_excerpts(
979 private_buffer.clone(),
980 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
981 cx,
982 );
983 multibuffer.push_excerpts(
984 public_buffer.clone(),
985 [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
986 cx,
987 );
988 multibuffer
989 });
990 let editor =
991 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
992 editor
993 .update(cx, |editor, window, cx| {
994 use gpui::Focusable;
995 window.focus(&editor.focus_handle(cx))
996 })
997 .unwrap();
998 let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
999 editor
1000 .update(cx, |editor, window, cx| {
1001 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1002 })
1003 .unwrap();
1004
1005 let mut copilot_requests = copilot_lsp
1006 .set_request_handler::<crate::request::GetCompletions, _, _>(
1007 move |_params, _cx| async move {
1008 Ok(crate::request::GetCompletionsResult {
1009 completions: vec![crate::request::Completion {
1010 text: "next line".into(),
1011 range: lsp::Range::new(
1012 lsp::Position::new(1, 0),
1013 lsp::Position::new(1, 0),
1014 ),
1015 ..Default::default()
1016 }],
1017 })
1018 },
1019 );
1020
1021 _ = editor.update(cx, |editor, window, cx| {
1022 editor.change_selections(None, window, cx, |selections| {
1023 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1024 });
1025 editor.refresh_inline_completion(true, false, window, cx);
1026 });
1027
1028 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1029 assert!(copilot_requests.try_next().is_err());
1030
1031 _ = editor.update(cx, |editor, window, cx| {
1032 editor.change_selections(None, window, cx, |s| {
1033 s.select_ranges([Point::new(5, 0)..Point::new(5, 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_ok());
1040 }
1041
1042 fn handle_copilot_completion_request(
1043 lsp: &lsp::FakeLanguageServer,
1044 completions: Vec<crate::request::Completion>,
1045 completions_cycling: Vec<crate::request::Completion>,
1046 ) {
1047 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1048 let completions = completions.clone();
1049 async move {
1050 Ok(crate::request::GetCompletionsResult {
1051 completions: completions.clone(),
1052 })
1053 }
1054 });
1055 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1056 move |_params, _cx| {
1057 let completions_cycling = completions_cycling.clone();
1058 async move {
1059 Ok(crate::request::GetCompletionsResult {
1060 completions: completions_cycling.clone(),
1061 })
1062 }
1063 },
1064 );
1065 }
1066
1067 fn handle_completion_request(
1068 cx: &mut EditorLspTestContext,
1069 marked_string: &str,
1070 completions: Vec<&'static str>,
1071 ) -> impl Future<Output = ()> {
1072 let complete_from_marker: TextRangeMarker = '|'.into();
1073 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1074 let (_, mut marked_ranges) = marked_text_ranges_by(
1075 marked_string,
1076 vec![complete_from_marker.clone(), replace_range_marker.clone()],
1077 );
1078
1079 let complete_from_position =
1080 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
1081 let replace_range =
1082 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1083
1084 let mut request =
1085 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1086 let completions = completions.clone();
1087 async move {
1088 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1089 assert_eq!(
1090 params.text_document_position.position,
1091 complete_from_position
1092 );
1093 Ok(Some(lsp::CompletionResponse::Array(
1094 completions
1095 .iter()
1096 .map(|completion_text| lsp::CompletionItem {
1097 label: completion_text.to_string(),
1098 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1099 range: replace_range,
1100 new_text: completion_text.to_string(),
1101 })),
1102 ..Default::default()
1103 })
1104 .collect(),
1105 )))
1106 }
1107 });
1108
1109 async move {
1110 request.next().await;
1111 }
1112 }
1113
1114 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1115 cx.update(|cx| {
1116 let store = SettingsStore::test(cx);
1117 cx.set_global(store);
1118 theme::init(theme::LoadThemes::JustBase, cx);
1119 client::init_settings(cx);
1120 language::init(cx);
1121 editor::init_settings(cx);
1122 Project::init_settings(cx);
1123 workspace::init_settings(cx);
1124 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1125 store.update_user_settings::<AllLanguageSettings>(cx, f);
1126 });
1127 });
1128 }
1129}