copilot_completion_provider.rs

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