copilot_completion_provider.rs

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