copilot_completion_provider.rs

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