copilot_completion_provider.rs

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