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