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