copilot_edit_prediction_delegate.rs

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