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