copilot_completion_provider.rs

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