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