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