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