copilot_completion_provider.rs

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