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