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