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