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