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