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