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