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