copilot_completion_provider.rs

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