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