copilot_edit_prediction_delegate.rs

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