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