copilot_completion_provider.rs

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