copilot_edit_prediction_delegate.rs

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