copilot_completion_provider.rs

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