copilot_edit_prediction_delegate.rs

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