copilot_completion_provider.rs

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