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