copilot_completion_provider.rs

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