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