copilot_completion_provider.rs

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