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