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