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