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