1use crate::{Completion, Copilot};
2use anyhow::Result;
3use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
4use gpui::{App, Context, Entity, EntityId, Task};
5use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
6use settings::Settings;
7use std::{path::Path, time::Duration};
8
9pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
10
11pub struct CopilotEditPredictionDelegate {
12 cycled: bool,
13 buffer_id: Option<EntityId>,
14 completions: Vec<Completion>,
15 active_completion_index: usize,
16 file_extension: Option<String>,
17 pending_refresh: Option<Task<Result<()>>>,
18 pending_cycling_refresh: Option<Task<Result<()>>>,
19 copilot: Entity<Copilot>,
20}
21
22impl CopilotEditPredictionDelegate {
23 pub fn new(copilot: Entity<Copilot>) -> Self {
24 Self {
25 cycled: false,
26 buffer_id: None,
27 completions: Vec::new(),
28 active_completion_index: 0,
29 file_extension: None,
30 pending_refresh: None,
31 pending_cycling_refresh: None,
32 copilot,
33 }
34 }
35
36 fn active_completion(&self) -> Option<&Completion> {
37 self.completions.get(self.active_completion_index)
38 }
39
40 fn push_completion(&mut self, new_completion: Completion) {
41 for completion in &self.completions {
42 if completion.text == new_completion.text && completion.range == new_completion.range {
43 return;
44 }
45 }
46 self.completions.push(new_completion);
47 }
48}
49
50impl EditPredictionDelegate for CopilotEditPredictionDelegate {
51 fn name() -> &'static str {
52 "copilot"
53 }
54
55 fn display_name() -> &'static str {
56 "Copilot"
57 }
58
59 fn show_predictions_in_menu() -> bool {
60 true
61 }
62
63 fn show_tab_accept_marker() -> bool {
64 true
65 }
66
67 fn supports_jump_to_edit() -> bool {
68 false
69 }
70
71 fn is_refreshing(&self, _cx: &App) -> bool {
72 self.pending_refresh.is_some() && self.completions.is_empty()
73 }
74
75 fn is_enabled(
76 &self,
77 _buffer: &Entity<Buffer>,
78 _cursor_position: language::Anchor,
79 cx: &App,
80 ) -> bool {
81 self.copilot.read(cx).status().is_authorized()
82 }
83
84 fn refresh(
85 &mut self,
86 buffer: Entity<Buffer>,
87 cursor_position: language::Anchor,
88 debounce: bool,
89 cx: &mut Context<Self>,
90 ) {
91 let copilot = self.copilot.clone();
92 self.pending_refresh = Some(cx.spawn(async move |this, cx| {
93 if debounce {
94 cx.background_executor()
95 .timer(COPILOT_DEBOUNCE_TIMEOUT)
96 .await;
97 }
98
99 let completions = copilot
100 .update(cx, |copilot, cx| {
101 copilot.completions(&buffer, cursor_position, cx)
102 })?
103 .await?;
104
105 this.update(cx, |this, cx| {
106 if !completions.is_empty() {
107 this.cycled = false;
108 this.pending_refresh = None;
109 this.pending_cycling_refresh = None;
110 this.completions.clear();
111 this.active_completion_index = 0;
112 this.buffer_id = Some(buffer.entity_id());
113 this.file_extension = buffer.read(cx).file().and_then(|file| {
114 Some(
115 Path::new(file.file_name(cx))
116 .extension()?
117 .to_str()?
118 .to_string(),
119 )
120 });
121
122 for completion in completions {
123 this.push_completion(completion);
124 }
125 cx.notify();
126 }
127 })?;
128
129 Ok(())
130 }));
131 }
132
133 fn cycle(
134 &mut self,
135 buffer: Entity<Buffer>,
136 cursor_position: language::Anchor,
137 direction: Direction,
138 cx: &mut Context<Self>,
139 ) {
140 if self.cycled {
141 match direction {
142 Direction::Prev => {
143 self.active_completion_index = if self.active_completion_index == 0 {
144 self.completions.len().saturating_sub(1)
145 } else {
146 self.active_completion_index - 1
147 };
148 }
149 Direction::Next => {
150 if self.completions.is_empty() {
151 self.active_completion_index = 0
152 } else {
153 self.active_completion_index =
154 (self.active_completion_index + 1) % self.completions.len();
155 }
156 }
157 }
158
159 cx.notify();
160 } else {
161 let copilot = self.copilot.clone();
162 self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
163 let completions = copilot
164 .update(cx, |copilot, cx| {
165 copilot.completions_cycling(&buffer, cursor_position, cx)
166 })?
167 .await?;
168
169 this.update(cx, |this, cx| {
170 this.cycled = true;
171 this.file_extension = buffer.read(cx).file().and_then(|file| {
172 Some(
173 Path::new(file.file_name(cx))
174 .extension()?
175 .to_str()?
176 .to_string(),
177 )
178 });
179 for completion in completions {
180 this.push_completion(completion);
181 }
182 this.cycle(buffer, cursor_position, direction, cx);
183 })?;
184
185 Ok(())
186 }));
187 }
188 }
189
190 fn accept(&mut self, cx: &mut Context<Self>) {
191 if let Some(completion) = self.active_completion() {
192 self.copilot
193 .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
194 .detach_and_log_err(cx);
195 }
196 }
197
198 fn discard(&mut self, cx: &mut Context<Self>) {
199 let settings = AllLanguageSettings::get_global(cx);
200
201 let copilot_enabled = settings.show_edit_predictions(None, cx);
202
203 if !copilot_enabled {
204 return;
205 }
206
207 self.copilot
208 .update(cx, |copilot, cx| {
209 copilot.discard_completions(&self.completions, cx)
210 })
211 .detach_and_log_err(cx);
212 }
213
214 fn suggest(
215 &mut self,
216 buffer: &Entity<Buffer>,
217 cursor_position: language::Anchor,
218 cx: &mut Context<Self>,
219 ) -> Option<EditPrediction> {
220 let buffer_id = buffer.entity_id();
221 let buffer = buffer.read(cx);
222 let completion = self.active_completion()?;
223 if Some(buffer_id) != self.buffer_id
224 || !completion.range.start.is_valid(buffer)
225 || !completion.range.end.is_valid(buffer)
226 {
227 return None;
228 }
229
230 let mut completion_range = completion.range.to_offset(buffer);
231 let prefix_len = common_prefix(
232 buffer.chars_for_range(completion_range.clone()),
233 completion.text.chars(),
234 );
235 completion_range.start += prefix_len;
236 let suffix_len = common_prefix(
237 buffer.reversed_chars_for_range(completion_range.clone()),
238 completion.text[prefix_len..].chars().rev(),
239 );
240 completion_range.end = completion_range.end.saturating_sub(suffix_len);
241
242 if completion_range.is_empty()
243 && completion_range.start == cursor_position.to_offset(buffer)
244 {
245 let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
246 if completion_text.trim().is_empty() {
247 None
248 } else {
249 let position = cursor_position.bias_right(buffer);
250 Some(EditPrediction::Local {
251 id: None,
252 edits: vec![(position..position, completion_text.into())],
253 edit_preview: None,
254 })
255 }
256 } else {
257 None
258 }
259 }
260}
261
262fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
263 a.zip(b)
264 .take_while(|(a, b)| a == b)
265 .map(|(a, _)| a.len_utf8())
266 .sum()
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use edit_prediction_types::EditPredictionGranularity;
273 use editor::{
274 Editor, ExcerptRange, MultiBuffer, MultiBufferOffset, SelectionEffects,
275 test::editor_lsp_test_context::EditorLspTestContext,
276 };
277 use fs::FakeFs;
278 use futures::StreamExt;
279 use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
280 use indoc::indoc;
281 use language::{
282 Point,
283 language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
284 };
285 use project::Project;
286 use serde_json::json;
287 use settings::{AllLanguageSettingsContent, SettingsStore};
288 use std::future::Future;
289 use util::{
290 path,
291 test::{TextRangeMarker, marked_text_ranges_by},
292 };
293
294 #[gpui::test(iterations = 10)]
295 async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
296 // flaky
297 init_test(cx, |settings| {
298 settings.defaults.completions = Some(CompletionSettingsContent {
299 words: Some(WordsCompletionMode::Disabled),
300 words_min_length: Some(0),
301 lsp_insert_mode: Some(LspInsertMode::Insert),
302 ..Default::default()
303 });
304 });
305
306 let (copilot, copilot_lsp) = Copilot::fake(cx);
307 let mut cx = EditorLspTestContext::new_rust(
308 lsp::ServerCapabilities {
309 completion_provider: Some(lsp::CompletionOptions {
310 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
311 ..Default::default()
312 }),
313 ..Default::default()
314 },
315 cx,
316 )
317 .await;
318 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
319 cx.update_editor(|editor, window, cx| {
320 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
321 });
322
323 cx.set_state(indoc! {"
324 oneˇ
325 two
326 three
327 "});
328 cx.simulate_keystroke(".");
329 drop(handle_completion_request(
330 &mut cx,
331 indoc! {"
332 one.|<>
333 two
334 three
335 "},
336 vec!["completion_a", "completion_b"],
337 ));
338 handle_copilot_completion_request(
339 &copilot_lsp,
340 vec![crate::request::Completion {
341 text: "one.copilot1".into(),
342 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
343 ..Default::default()
344 }],
345 vec![],
346 );
347 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
348 cx.update_editor(|editor, window, cx| {
349 assert!(editor.context_menu_visible());
350 assert!(editor.has_active_edit_prediction());
351 // Since we have both, the copilot suggestion is existing but does not show up as ghost text
352 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
353 assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
354
355 // Confirming a non-copilot completion inserts it and hides the context menu, without showing
356 // the copilot suggestion afterwards.
357 editor
358 .confirm_completion(&Default::default(), window, cx)
359 .unwrap()
360 .detach();
361 assert!(!editor.context_menu_visible());
362 assert!(!editor.has_active_edit_prediction());
363 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
364 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
365 });
366
367 // Reset editor and only return copilot suggestions
368 cx.set_state(indoc! {"
369 oneˇ
370 two
371 three
372 "});
373 cx.simulate_keystroke(".");
374
375 drop(handle_completion_request(
376 &mut cx,
377 indoc! {"
378 one.|<>
379 two
380 three
381 "},
382 vec![],
383 ));
384 handle_copilot_completion_request(
385 &copilot_lsp,
386 vec![crate::request::Completion {
387 text: "one.copilot1".into(),
388 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
389 ..Default::default()
390 }],
391 vec![],
392 );
393 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
394 cx.update_editor(|editor, _, cx| {
395 assert!(!editor.context_menu_visible());
396 assert!(editor.has_active_edit_prediction());
397 // Since only the copilot is available, it's shown inline
398 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
399 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
400 });
401
402 // Ensure existing edit prediction is interpolated when inserting again.
403 cx.simulate_keystroke("c");
404 executor.run_until_parked();
405 cx.update_editor(|editor, _, cx| {
406 assert!(!editor.context_menu_visible());
407 assert!(editor.has_active_edit_prediction());
408 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
409 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
410 });
411
412 // After debouncing, new Copilot completions should be requested.
413 handle_copilot_completion_request(
414 &copilot_lsp,
415 vec![crate::request::Completion {
416 text: "one.copilot2".into(),
417 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
418 ..Default::default()
419 }],
420 vec![],
421 );
422 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
423 cx.update_editor(|editor, window, cx| {
424 assert!(!editor.context_menu_visible());
425 assert!(editor.has_active_edit_prediction());
426 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
427 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
428
429 // Canceling should remove the active Copilot suggestion.
430 editor.cancel(&Default::default(), window, cx);
431 assert!(!editor.has_active_edit_prediction());
432 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
433 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
434
435 // After canceling, tabbing shouldn't insert the previously shown suggestion.
436 editor.tab(&Default::default(), window, cx);
437 assert!(!editor.has_active_edit_prediction());
438 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
439 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
440
441 // When undoing the previously active suggestion is shown again.
442 editor.undo(&Default::default(), window, cx);
443 assert!(editor.has_active_edit_prediction());
444 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
445 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
446 });
447
448 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
449 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
450 cx.update_editor(|editor, window, cx| {
451 assert!(editor.has_active_edit_prediction());
452 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
453 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
454
455 // AcceptEditPrediction when there is an active suggestion inserts it.
456 editor.accept_edit_prediction(&Default::default(), window, cx);
457 assert!(!editor.has_active_edit_prediction());
458 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
459 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
460
461 // When undoing the previously active suggestion is shown again.
462 editor.undo(&Default::default(), window, cx);
463 assert!(editor.has_active_edit_prediction());
464 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
465 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
466
467 // Hide suggestion.
468 editor.cancel(&Default::default(), window, cx);
469 assert!(!editor.has_active_edit_prediction());
470 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
471 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
472 });
473
474 // If an edit occurs outside of this editor but no suggestion is being shown,
475 // we won't make it visible.
476 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
477 cx.update_editor(|editor, _, cx| {
478 assert!(!editor.has_active_edit_prediction());
479 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
480 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
481 });
482
483 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
484 cx.update_editor(|editor, window, cx| {
485 editor.set_text("fn foo() {\n \n}", window, cx);
486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
487 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
488 });
489 });
490 handle_copilot_completion_request(
491 &copilot_lsp,
492 vec![crate::request::Completion {
493 text: " let x = 4;".into(),
494 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
495 ..Default::default()
496 }],
497 vec![],
498 );
499
500 cx.update_editor(|editor, window, cx| {
501 editor.next_edit_prediction(&Default::default(), window, cx)
502 });
503 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
504 cx.update_editor(|editor, window, cx| {
505 assert!(editor.has_active_edit_prediction());
506 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
507 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
508
509 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
510 editor.tab(&Default::default(), window, cx);
511 assert!(editor.has_active_edit_prediction());
512 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
513 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
514
515 // Using AcceptEditPrediction again accepts the suggestion.
516 editor.accept_edit_prediction(&Default::default(), window, cx);
517 assert!(!editor.has_active_edit_prediction());
518 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
519 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
520 });
521 }
522
523 #[gpui::test(iterations = 10)]
524 async fn test_accept_partial_copilot_suggestion(
525 executor: BackgroundExecutor,
526 cx: &mut TestAppContext,
527 ) {
528 // flaky
529 init_test(cx, |settings| {
530 settings.defaults.completions = Some(CompletionSettingsContent {
531 words: Some(WordsCompletionMode::Disabled),
532 words_min_length: Some(0),
533 lsp_insert_mode: Some(LspInsertMode::Insert),
534 ..Default::default()
535 });
536 });
537
538 let (copilot, copilot_lsp) = Copilot::fake(cx);
539 let mut cx = EditorLspTestContext::new_rust(
540 lsp::ServerCapabilities {
541 completion_provider: Some(lsp::CompletionOptions {
542 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
543 ..Default::default()
544 }),
545 ..Default::default()
546 },
547 cx,
548 )
549 .await;
550 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
551 cx.update_editor(|editor, window, cx| {
552 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
553 });
554
555 // Setup the editor with a completion request.
556 cx.set_state(indoc! {"
557 oneˇ
558 two
559 three
560 "});
561 cx.simulate_keystroke(".");
562 drop(handle_completion_request(
563 &mut cx,
564 indoc! {"
565 one.|<>
566 two
567 three
568 "},
569 vec![],
570 ));
571 handle_copilot_completion_request(
572 &copilot_lsp,
573 vec![crate::request::Completion {
574 text: "one.copilot1".into(),
575 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
576 ..Default::default()
577 }],
578 vec![],
579 );
580 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
581 cx.update_editor(|editor, window, cx| {
582 assert!(editor.has_active_edit_prediction());
583
584 // Accepting the first word of the suggestion should only accept the first word and still show the rest.
585 editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
586
587 assert!(editor.has_active_edit_prediction());
588 assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
589 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
590
591 // Accepting next word should accept the non-word and copilot suggestion should be gone
592 editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
593
594 assert!(!editor.has_active_edit_prediction());
595 assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
596 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
597 });
598
599 // Reset the editor and check non-word and whitespace completion
600 cx.set_state(indoc! {"
601 oneˇ
602 two
603 three
604 "});
605 cx.simulate_keystroke(".");
606 drop(handle_completion_request(
607 &mut cx,
608 indoc! {"
609 one.|<>
610 two
611 three
612 "},
613 vec![],
614 ));
615 handle_copilot_completion_request(
616 &copilot_lsp,
617 vec![crate::request::Completion {
618 text: "one.123. copilot\n 456".into(),
619 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
620 ..Default::default()
621 }],
622 vec![],
623 );
624 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
625 cx.update_editor(|editor, window, cx| {
626 assert!(editor.has_active_edit_prediction());
627
628 // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
629 editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
630 assert!(editor.has_active_edit_prediction());
631 assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
632 assert_eq!(
633 editor.display_text(cx),
634 "one.123. copilot\n 456\ntwo\nthree\n"
635 );
636
637 // Accepting next word should accept the next word and copilot suggestion should still exist
638 editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
639 assert!(editor.has_active_edit_prediction());
640 assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
641 assert_eq!(
642 editor.display_text(cx),
643 "one.123. copilot\n 456\ntwo\nthree\n"
644 );
645
646 // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
647 editor.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
648 assert!(!editor.has_active_edit_prediction());
649 assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
650 assert_eq!(
651 editor.display_text(cx),
652 "one.123. copilot\n 456\ntwo\nthree\n"
653 );
654 });
655 }
656
657 #[gpui::test]
658 async fn test_copilot_completion_invalidation(
659 executor: BackgroundExecutor,
660 cx: &mut TestAppContext,
661 ) {
662 init_test(cx, |_| {});
663
664 let (copilot, copilot_lsp) = Copilot::fake(cx);
665 let mut cx = EditorLspTestContext::new_rust(
666 lsp::ServerCapabilities {
667 completion_provider: Some(lsp::CompletionOptions {
668 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
669 ..Default::default()
670 }),
671 ..Default::default()
672 },
673 cx,
674 )
675 .await;
676 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
677 cx.update_editor(|editor, window, cx| {
678 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
679 });
680
681 cx.set_state(indoc! {"
682 one
683 twˇ
684 three
685 "});
686
687 handle_copilot_completion_request(
688 &copilot_lsp,
689 vec![crate::request::Completion {
690 text: "two.foo()".into(),
691 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
692 ..Default::default()
693 }],
694 vec![],
695 );
696 cx.update_editor(|editor, window, cx| {
697 editor.next_edit_prediction(&Default::default(), window, cx)
698 });
699 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
700 cx.update_editor(|editor, window, cx| {
701 assert!(editor.has_active_edit_prediction());
702 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
703 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
704
705 editor.backspace(&Default::default(), window, cx);
706 assert!(editor.has_active_edit_prediction());
707 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
708 assert_eq!(editor.text(cx), "one\nt\nthree\n");
709
710 editor.backspace(&Default::default(), window, cx);
711 assert!(editor.has_active_edit_prediction());
712 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
713 assert_eq!(editor.text(cx), "one\n\nthree\n");
714
715 // Deleting across the original suggestion range invalidates it.
716 editor.backspace(&Default::default(), window, cx);
717 assert!(!editor.has_active_edit_prediction());
718 assert_eq!(editor.display_text(cx), "one\nthree\n");
719 assert_eq!(editor.text(cx), "one\nthree\n");
720
721 // Undoing the deletion restores the suggestion.
722 editor.undo(&Default::default(), window, cx);
723 assert!(editor.has_active_edit_prediction());
724 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
725 assert_eq!(editor.text(cx), "one\n\nthree\n");
726 });
727 }
728
729 #[gpui::test]
730 async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut TestAppContext) {
731 init_test(cx, |_| {});
732
733 let (copilot, copilot_lsp) = Copilot::fake(cx);
734
735 let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx));
736 let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx));
737 let multibuffer = cx.new(|cx| {
738 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
739 multibuffer.push_excerpts(
740 buffer_1.clone(),
741 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
742 cx,
743 );
744 multibuffer.push_excerpts(
745 buffer_2.clone(),
746 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
747 cx,
748 );
749 multibuffer
750 });
751 let editor =
752 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
753 editor
754 .update(cx, |editor, window, cx| {
755 use gpui::Focusable;
756 window.focus(&editor.focus_handle(cx), cx);
757 })
758 .unwrap();
759 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
760 editor
761 .update(cx, |editor, window, cx| {
762 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
763 })
764 .unwrap();
765
766 handle_copilot_completion_request(
767 &copilot_lsp,
768 vec![crate::request::Completion {
769 text: "b = 2 + a".into(),
770 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
771 ..Default::default()
772 }],
773 vec![],
774 );
775 _ = editor.update(cx, |editor, window, cx| {
776 // Ensure copilot suggestions are shown for the first excerpt.
777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
778 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
779 });
780 editor.next_edit_prediction(&Default::default(), window, cx);
781 });
782 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
783 _ = editor.update(cx, |editor, _, cx| {
784 assert!(editor.has_active_edit_prediction());
785 assert_eq!(
786 editor.display_text(cx),
787 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
788 );
789 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
790 });
791
792 handle_copilot_completion_request(
793 &copilot_lsp,
794 vec![crate::request::Completion {
795 text: "d = 4 + c".into(),
796 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
797 ..Default::default()
798 }],
799 vec![],
800 );
801 _ = editor.update(cx, |editor, window, cx| {
802 // Move to another excerpt, ensuring the suggestion gets cleared.
803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
804 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
805 });
806 assert!(!editor.has_active_edit_prediction());
807 assert_eq!(
808 editor.display_text(cx),
809 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
810 );
811 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
812
813 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
814 editor.handle_input(" ", window, cx);
815 assert!(!editor.has_active_edit_prediction());
816 assert_eq!(
817 editor.display_text(cx),
818 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
819 );
820 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
821 });
822
823 // Ensure the new suggestion is displayed when the debounce timeout expires.
824 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
825 _ = editor.update(cx, |editor, _, cx| {
826 assert!(editor.has_active_edit_prediction());
827 assert_eq!(
828 editor.display_text(cx),
829 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
830 );
831 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
832 });
833 }
834
835 #[gpui::test]
836 async fn test_copilot_does_not_prevent_completion_triggers(
837 executor: BackgroundExecutor,
838 cx: &mut TestAppContext,
839 ) {
840 init_test(cx, |_| {});
841
842 let (copilot, copilot_lsp) = Copilot::fake(cx);
843 let mut cx = EditorLspTestContext::new_rust(
844 lsp::ServerCapabilities {
845 completion_provider: Some(lsp::CompletionOptions {
846 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
847 ..lsp::CompletionOptions::default()
848 }),
849 ..lsp::ServerCapabilities::default()
850 },
851 cx,
852 )
853 .await;
854 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
855 cx.update_editor(|editor, window, cx| {
856 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
857 });
858
859 cx.set_state(indoc! {"
860 one
861 twˇ
862 three
863 "});
864
865 drop(handle_completion_request(
866 &mut cx,
867 indoc! {"
868 one
869 tw|<>
870 three
871 "},
872 vec!["completion_a", "completion_b"],
873 ));
874 handle_copilot_completion_request(
875 &copilot_lsp,
876 vec![crate::request::Completion {
877 text: "two.foo()".into(),
878 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
879 ..Default::default()
880 }],
881 vec![],
882 );
883 cx.update_editor(|editor, window, cx| {
884 editor.next_edit_prediction(&Default::default(), window, cx)
885 });
886 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
887 cx.update_editor(|editor, _, cx| {
888 assert!(!editor.context_menu_visible());
889 assert!(editor.has_active_edit_prediction());
890 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
891 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
892 });
893
894 cx.simulate_keystroke("o");
895 drop(handle_completion_request(
896 &mut cx,
897 indoc! {"
898 one
899 two|<>
900 three
901 "},
902 vec!["completion_a_2", "completion_b_2"],
903 ));
904 handle_copilot_completion_request(
905 &copilot_lsp,
906 vec![crate::request::Completion {
907 text: "two.foo()".into(),
908 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
909 ..Default::default()
910 }],
911 vec![],
912 );
913 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
914 cx.update_editor(|editor, _, cx| {
915 assert!(!editor.context_menu_visible());
916 assert!(editor.has_active_edit_prediction());
917 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
918 assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
919 });
920
921 cx.simulate_keystroke(".");
922 drop(handle_completion_request(
923 &mut cx,
924 indoc! {"
925 one
926 two.|<>
927 three
928 "},
929 vec!["something_else()"],
930 ));
931 handle_copilot_completion_request(
932 &copilot_lsp,
933 vec![crate::request::Completion {
934 text: "two.foo()".into(),
935 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
936 ..Default::default()
937 }],
938 vec![],
939 );
940 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
941 cx.update_editor(|editor, _, cx| {
942 assert!(editor.context_menu_visible());
943 assert!(editor.has_active_edit_prediction());
944 assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
945 assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
946 });
947 }
948
949 #[gpui::test]
950 async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
951 init_test(cx, |settings| {
952 settings
953 .edit_predictions
954 .get_or_insert(Default::default())
955 .disabled_globs = Some(vec![".env*".to_string()]);
956 });
957
958 let (copilot, copilot_lsp) = Copilot::fake(cx);
959
960 let fs = FakeFs::new(cx.executor());
961 fs.insert_tree(
962 path!("/test"),
963 json!({
964 ".env": "SECRET=something\n",
965 "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
966 }),
967 )
968 .await;
969 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
970
971 let private_buffer = project
972 .update(cx, |project, cx| {
973 project.open_local_buffer(path!("/test/.env"), cx)
974 })
975 .await
976 .unwrap();
977 let public_buffer = project
978 .update(cx, |project, cx| {
979 project.open_local_buffer(path!("/test/README.md"), cx)
980 })
981 .await
982 .unwrap();
983
984 let multibuffer = cx.new(|cx| {
985 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
986 multibuffer.push_excerpts(
987 private_buffer.clone(),
988 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
989 cx,
990 );
991 multibuffer.push_excerpts(
992 public_buffer.clone(),
993 [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
994 cx,
995 );
996 multibuffer
997 });
998 let editor =
999 cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
1000 editor
1001 .update(cx, |editor, window, cx| {
1002 use gpui::Focusable;
1003 window.focus(&editor.focus_handle(cx), cx)
1004 })
1005 .unwrap();
1006 let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
1007 editor
1008 .update(cx, |editor, window, cx| {
1009 editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1010 })
1011 .unwrap();
1012
1013 let mut copilot_requests = copilot_lsp
1014 .set_request_handler::<crate::request::GetCompletions, _, _>(
1015 move |_params, _cx| async move {
1016 Ok(crate::request::GetCompletionsResult {
1017 completions: vec![crate::request::Completion {
1018 text: "next line".into(),
1019 range: lsp::Range::new(
1020 lsp::Position::new(1, 0),
1021 lsp::Position::new(1, 0),
1022 ),
1023 ..Default::default()
1024 }],
1025 })
1026 },
1027 );
1028
1029 _ = editor.update(cx, |editor, window, cx| {
1030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1031 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1032 });
1033 editor.refresh_edit_prediction(true, false, window, cx);
1034 });
1035
1036 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1037 assert!(copilot_requests.try_next().is_err());
1038
1039 _ = editor.update(cx, |editor, window, cx| {
1040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1041 s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1042 });
1043 editor.refresh_edit_prediction(true, false, window, cx);
1044 });
1045
1046 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1047 assert!(copilot_requests.try_next().is_ok());
1048 }
1049
1050 fn handle_copilot_completion_request(
1051 lsp: &lsp::FakeLanguageServer,
1052 completions: Vec<crate::request::Completion>,
1053 completions_cycling: Vec<crate::request::Completion>,
1054 ) {
1055 lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1056 let completions = completions.clone();
1057 async move {
1058 Ok(crate::request::GetCompletionsResult {
1059 completions: completions.clone(),
1060 })
1061 }
1062 });
1063 lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1064 move |_params, _cx| {
1065 let completions_cycling = completions_cycling.clone();
1066 async move {
1067 Ok(crate::request::GetCompletionsResult {
1068 completions: completions_cycling.clone(),
1069 })
1070 }
1071 },
1072 );
1073 }
1074
1075 fn handle_completion_request(
1076 cx: &mut EditorLspTestContext,
1077 marked_string: &str,
1078 completions: Vec<&'static str>,
1079 ) -> impl Future<Output = ()> {
1080 let complete_from_marker: TextRangeMarker = '|'.into();
1081 let replace_range_marker: TextRangeMarker = ('<', '>').into();
1082 let (_, mut marked_ranges) = marked_text_ranges_by(
1083 marked_string,
1084 vec![complete_from_marker, replace_range_marker.clone()],
1085 );
1086
1087 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
1088 let replace_range =
1089 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
1090
1091 let mut request =
1092 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1093 let completions = completions.clone();
1094 async move {
1095 assert_eq!(params.text_document_position.text_document.uri, url.clone());
1096 Ok(Some(lsp::CompletionResponse::Array(
1097 completions
1098 .iter()
1099 .map(|completion_text| lsp::CompletionItem {
1100 label: completion_text.to_string(),
1101 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1102 range: replace_range,
1103 new_text: completion_text.to_string(),
1104 })),
1105 ..Default::default()
1106 })
1107 .collect(),
1108 )))
1109 }
1110 });
1111
1112 async move {
1113 request.next().await;
1114 }
1115 }
1116
1117 fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1118 cx.update(|cx| {
1119 let store = SettingsStore::test(cx);
1120 cx.set_global(store);
1121 theme::init(theme::LoadThemes::JustBase, cx);
1122 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1123 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
1124 });
1125 });
1126 }
1127}