copilot_edit_prediction_delegate.rs

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