copilot_completion_provider.rs

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