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