copilot_completion_provider.rs

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