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