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::new(Point::new(0, 0)..Point::new(2, 0))],
 733                cx,
 734            );
 735            multibuffer.push_excerpts(
 736                buffer_2.clone(),
 737                [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
 738                cx,
 739            );
 740            multibuffer
 741        });
 742        let editor =
 743            cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
 744        editor
 745            .update(cx, |editor, window, cx| {
 746                use gpui::Focusable;
 747                window.focus(&editor.focus_handle(cx));
 748            })
 749            .unwrap();
 750        let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
 751        editor
 752            .update(cx, |editor, window, cx| {
 753                editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
 754            })
 755            .unwrap();
 756
 757        handle_copilot_completion_request(
 758            &copilot_lsp,
 759            vec![crate::request::Completion {
 760                text: "b = 2 + a".into(),
 761                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
 762                ..Default::default()
 763            }],
 764            vec![],
 765        );
 766        _ = editor.update(cx, |editor, window, cx| {
 767            // Ensure copilot suggestions are shown for the first excerpt.
 768            editor.change_selections(None, window, cx, |s| {
 769                s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
 770            });
 771            editor.next_edit_prediction(&Default::default(), window, cx);
 772        });
 773        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 774        _ = editor.update(cx, |editor, _, cx| {
 775            assert!(editor.has_active_inline_completion());
 776            assert_eq!(
 777                editor.display_text(cx),
 778                "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
 779            );
 780            assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
 781        });
 782
 783        handle_copilot_completion_request(
 784            &copilot_lsp,
 785            vec![crate::request::Completion {
 786                text: "d = 4 + c".into(),
 787                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
 788                ..Default::default()
 789            }],
 790            vec![],
 791        );
 792        _ = editor.update(cx, |editor, window, cx| {
 793            // Move to another excerpt, ensuring the suggestion gets cleared.
 794            editor.change_selections(None, window, cx, |s| {
 795                s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
 796            });
 797            assert!(!editor.has_active_inline_completion());
 798            assert_eq!(
 799                editor.display_text(cx),
 800                "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
 801            );
 802            assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
 803
 804            // Type a character, ensuring we don't even try to interpolate the previous suggestion.
 805            editor.handle_input(" ", window, cx);
 806            assert!(!editor.has_active_inline_completion());
 807            assert_eq!(
 808                editor.display_text(cx),
 809                "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
 810            );
 811            assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
 812        });
 813
 814        // Ensure the new suggestion is displayed when the debounce timeout expires.
 815        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 816        _ = editor.update(cx, |editor, _, cx| {
 817            assert!(editor.has_active_inline_completion());
 818            assert_eq!(
 819                editor.display_text(cx),
 820                "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
 821            );
 822            assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
 823        });
 824    }
 825
 826    #[gpui::test]
 827    async fn test_copilot_does_not_prevent_completion_triggers(
 828        executor: BackgroundExecutor,
 829        cx: &mut TestAppContext,
 830    ) {
 831        init_test(cx, |_| {});
 832
 833        let (copilot, copilot_lsp) = Copilot::fake(cx);
 834        let mut cx = EditorLspTestContext::new_rust(
 835            lsp::ServerCapabilities {
 836                completion_provider: Some(lsp::CompletionOptions {
 837                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 838                    ..lsp::CompletionOptions::default()
 839                }),
 840                ..lsp::ServerCapabilities::default()
 841            },
 842            cx,
 843        )
 844        .await;
 845        let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
 846        cx.update_editor(|editor, window, cx| {
 847            editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
 848        });
 849
 850        cx.set_state(indoc! {"
 851                one
 852                twˇ
 853                three
 854            "});
 855
 856        drop(handle_completion_request(
 857            &mut cx,
 858            indoc! {"
 859                one
 860                tw|<>
 861                three
 862            "},
 863            vec!["completion_a", "completion_b"],
 864        ));
 865        handle_copilot_completion_request(
 866            &copilot_lsp,
 867            vec![crate::request::Completion {
 868                text: "two.foo()".into(),
 869                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
 870                ..Default::default()
 871            }],
 872            vec![],
 873        );
 874        cx.update_editor(|editor, window, cx| {
 875            editor.next_edit_prediction(&Default::default(), window, cx)
 876        });
 877        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 878        cx.update_editor(|editor, _, cx| {
 879            assert!(!editor.context_menu_visible());
 880            assert!(editor.has_active_inline_completion());
 881            assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
 882            assert_eq!(editor.text(cx), "one\ntw\nthree\n");
 883        });
 884
 885        cx.simulate_keystroke("o");
 886        drop(handle_completion_request(
 887            &mut cx,
 888            indoc! {"
 889                one
 890                two|<>
 891                three
 892            "},
 893            vec!["completion_a_2", "completion_b_2"],
 894        ));
 895        handle_copilot_completion_request(
 896            &copilot_lsp,
 897            vec![crate::request::Completion {
 898                text: "two.foo()".into(),
 899                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
 900                ..Default::default()
 901            }],
 902            vec![],
 903        );
 904        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 905        cx.update_editor(|editor, _, cx| {
 906            assert!(!editor.context_menu_visible());
 907            assert!(editor.has_active_inline_completion());
 908            assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
 909            assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
 910        });
 911
 912        cx.simulate_keystroke(".");
 913        drop(handle_completion_request(
 914            &mut cx,
 915            indoc! {"
 916                one
 917                two.|<>
 918                three
 919            "},
 920            vec!["something_else()"],
 921        ));
 922        handle_copilot_completion_request(
 923            &copilot_lsp,
 924            vec![crate::request::Completion {
 925                text: "two.foo()".into(),
 926                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
 927                ..Default::default()
 928            }],
 929            vec![],
 930        );
 931        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 932        cx.update_editor(|editor, _, cx| {
 933            assert!(editor.context_menu_visible());
 934            assert!(!editor.has_active_inline_completion(),);
 935            assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
 936        });
 937    }
 938
 939    #[gpui::test]
 940    async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
 941        init_test(cx, |settings| {
 942            settings
 943                .edit_predictions
 944                .get_or_insert(Default::default())
 945                .disabled_globs = Some(vec![".env*".to_string()]);
 946        });
 947
 948        let (copilot, copilot_lsp) = Copilot::fake(cx);
 949
 950        let fs = FakeFs::new(cx.executor());
 951        fs.insert_tree(
 952            path!("/test"),
 953            json!({
 954                ".env": "SECRET=something\n",
 955                "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
 956            }),
 957        )
 958        .await;
 959        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
 960
 961        let private_buffer = project
 962            .update(cx, |project, cx| {
 963                project.open_local_buffer(path!("/test/.env"), cx)
 964            })
 965            .await
 966            .unwrap();
 967        let public_buffer = project
 968            .update(cx, |project, cx| {
 969                project.open_local_buffer(path!("/test/README.md"), cx)
 970            })
 971            .await
 972            .unwrap();
 973
 974        let multibuffer = cx.new(|cx| {
 975            let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
 976            multibuffer.push_excerpts(
 977                private_buffer.clone(),
 978                [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
 979                cx,
 980            );
 981            multibuffer.push_excerpts(
 982                public_buffer.clone(),
 983                [ExcerptRange::new(Point::new(0, 0)..Point::new(6, 0))],
 984                cx,
 985            );
 986            multibuffer
 987        });
 988        let editor =
 989            cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
 990        editor
 991            .update(cx, |editor, window, cx| {
 992                use gpui::Focusable;
 993                window.focus(&editor.focus_handle(cx))
 994            })
 995            .unwrap();
 996        let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot));
 997        editor
 998            .update(cx, |editor, window, cx| {
 999                editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1000            })
1001            .unwrap();
1002
1003        let mut copilot_requests = copilot_lsp
1004            .set_request_handler::<crate::request::GetCompletions, _, _>(
1005                move |_params, _cx| async move {
1006                    Ok(crate::request::GetCompletionsResult {
1007                        completions: vec![crate::request::Completion {
1008                            text: "next line".into(),
1009                            range: lsp::Range::new(
1010                                lsp::Position::new(1, 0),
1011                                lsp::Position::new(1, 0),
1012                            ),
1013                            ..Default::default()
1014                        }],
1015                    })
1016                },
1017            );
1018
1019        _ = editor.update(cx, |editor, window, cx| {
1020            editor.change_selections(None, window, cx, |selections| {
1021                selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1022            });
1023            editor.refresh_inline_completion(true, false, window, cx);
1024        });
1025
1026        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1027        assert!(copilot_requests.try_next().is_err());
1028
1029        _ = editor.update(cx, |editor, window, cx| {
1030            editor.change_selections(None, window, cx, |s| {
1031                s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1032            });
1033            editor.refresh_inline_completion(true, false, window, cx);
1034        });
1035
1036        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1037        assert!(copilot_requests.try_next().is_ok());
1038    }
1039
1040    fn handle_copilot_completion_request(
1041        lsp: &lsp::FakeLanguageServer,
1042        completions: Vec<crate::request::Completion>,
1043        completions_cycling: Vec<crate::request::Completion>,
1044    ) {
1045        lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
1046            let completions = completions.clone();
1047            async move {
1048                Ok(crate::request::GetCompletionsResult {
1049                    completions: completions.clone(),
1050                })
1051            }
1052        });
1053        lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
1054            move |_params, _cx| {
1055                let completions_cycling = completions_cycling.clone();
1056                async move {
1057                    Ok(crate::request::GetCompletionsResult {
1058                        completions: completions_cycling.clone(),
1059                    })
1060                }
1061            },
1062        );
1063    }
1064
1065    fn handle_completion_request(
1066        cx: &mut EditorLspTestContext,
1067        marked_string: &str,
1068        completions: Vec<&'static str>,
1069    ) -> impl Future<Output = ()> {
1070        let complete_from_marker: TextRangeMarker = '|'.into();
1071        let replace_range_marker: TextRangeMarker = ('<', '>').into();
1072        let (_, mut marked_ranges) = marked_text_ranges_by(
1073            marked_string,
1074            vec![complete_from_marker.clone(), replace_range_marker.clone()],
1075        );
1076
1077        let complete_from_position =
1078            cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
1079        let replace_range =
1080            cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
1081
1082        let mut request =
1083            cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1084                let completions = completions.clone();
1085                async move {
1086                    assert_eq!(params.text_document_position.text_document.uri, url.clone());
1087                    assert_eq!(
1088                        params.text_document_position.position,
1089                        complete_from_position
1090                    );
1091                    Ok(Some(lsp::CompletionResponse::Array(
1092                        completions
1093                            .iter()
1094                            .map(|completion_text| lsp::CompletionItem {
1095                                label: completion_text.to_string(),
1096                                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1097                                    range: replace_range,
1098                                    new_text: completion_text.to_string(),
1099                                })),
1100                                ..Default::default()
1101                            })
1102                            .collect(),
1103                    )))
1104                }
1105            });
1106
1107        async move {
1108            request.next().await;
1109        }
1110    }
1111
1112    fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1113        cx.update(|cx| {
1114            let store = SettingsStore::test(cx);
1115            cx.set_global(store);
1116            theme::init(theme::LoadThemes::JustBase, cx);
1117            client::init_settings(cx);
1118            language::init(cx);
1119            editor::init_settings(cx);
1120            Project::init_settings(cx);
1121            workspace::init_settings(cx);
1122            SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1123                store.update_user_settings::<AllLanguageSettings>(cx, f);
1124            });
1125        });
1126    }
1127}