copilot_completion_provider.rs

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