copilot_completion_provider.rs

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