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, MultiBuffer, MultiBufferOffset, PathKey, SelectionEffects,
 237        test::{editor_content_with_blocks, 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.set_excerpts_for_path(
 689                PathKey::sorted(0),
 690                buffer_1.clone(),
 691                [Point::new(0, 0)..Point::new(1, 0)],
 692                0,
 693                cx,
 694            );
 695            multibuffer.set_excerpts_for_path(
 696                PathKey::sorted(1),
 697                buffer_2.clone(),
 698                [Point::new(0, 0)..Point::new(1, 0)],
 699                0,
 700                cx,
 701            );
 702            multibuffer
 703        });
 704        let (editor, cx) =
 705            cx.add_window_view(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
 706        editor.update_in(cx, |editor, window, cx| {
 707            use gpui::Focusable;
 708            window.focus(&editor.focus_handle(cx), cx);
 709        });
 710        let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
 711        editor.update_in(cx, |editor, window, cx| {
 712            editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
 713        });
 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_in(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_in(cx, |editor, _, _| {
 736            assert!(editor.has_active_edit_prediction());
 737        });
 738        pretty_assertions::assert_eq!(
 739            editor_content_with_blocks(&editor, cx),
 740            indoc! { "
 741                § <no file>
 742                § -----
 743                a = 1
 744                b = 2 + a
 745                § <no file>
 746                § -----
 747                c = 3
 748                d = 4"
 749            }
 750        );
 751
 752        handle_copilot_completion_request(
 753            &copilot_lsp,
 754            vec![crate::request::NextEditSuggestion {
 755                text: "d = 4 + c".into(),
 756                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
 757                command: None,
 758                text_document: lsp::VersionedTextDocumentIdentifier {
 759                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
 760                    version: 0,
 761                },
 762            }],
 763        );
 764        _ = editor.update_in(cx, |editor, window, cx| {
 765            // Move to another excerpt, ensuring the suggestion gets cleared.
 766            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 767                s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
 768            });
 769            assert!(!editor.has_active_edit_prediction());
 770        });
 771        pretty_assertions::assert_eq!(
 772            editor_content_with_blocks(&editor, cx),
 773            indoc! { "
 774                § <no file>
 775                § -----
 776                a = 1
 777                b = 2
 778                § <no file>
 779                § -----
 780                c = 3
 781                d = 4"}
 782        );
 783        editor.update_in(cx, |editor, window, cx| {
 784            // Type a character, ensuring we don't even try to interpolate the previous suggestion.
 785            editor.handle_input(" ", window, cx);
 786            assert!(!editor.has_active_edit_prediction());
 787        });
 788        pretty_assertions::assert_eq!(
 789            editor_content_with_blocks(&editor, cx),
 790            indoc! {"
 791                § <no file>
 792                § -----
 793                a = 1
 794                b = 2
 795                § <no file>
 796                § -----
 797                c = 3
 798                d = 4\x20"
 799            },
 800        );
 801
 802        // Ensure the new suggestion is displayed when the debounce timeout expires.
 803        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 804        _ = editor.update(cx, |editor, _| {
 805            assert!(editor.has_active_edit_prediction());
 806        });
 807        assert_eq!(
 808            editor_content_with_blocks(&editor, cx),
 809            indoc! {"
 810               § <no file>
 811               § -----
 812               a = 1
 813               b = 2
 814               § <no file>
 815               § -----
 816               c = 3
 817               d = 4 + c"}
 818        );
 819    }
 820
 821    #[gpui::test]
 822    async fn test_copilot_does_not_prevent_completion_triggers(
 823        executor: BackgroundExecutor,
 824        cx: &mut TestAppContext,
 825    ) {
 826        init_test(cx, |_| {});
 827
 828        let (copilot, copilot_lsp) = Copilot::fake(cx);
 829        let mut cx = EditorLspTestContext::new_rust(
 830            lsp::ServerCapabilities {
 831                completion_provider: Some(lsp::CompletionOptions {
 832                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
 833                    ..lsp::CompletionOptions::default()
 834                }),
 835                ..lsp::ServerCapabilities::default()
 836            },
 837            cx,
 838        )
 839        .await;
 840        let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
 841        cx.update_editor(|editor, window, cx| {
 842            editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
 843        });
 844
 845        cx.set_state(indoc! {"
 846                one
 847                twˇ
 848                three
 849            "});
 850
 851        drop(handle_completion_request(
 852            &mut cx,
 853            indoc! {"
 854                one
 855                tw|<>
 856                three
 857            "},
 858            vec!["completion_a", "completion_b"],
 859        ));
 860        handle_copilot_completion_request(
 861            &copilot_lsp,
 862            vec![crate::request::NextEditSuggestion {
 863                text: "two.foo()".into(),
 864                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
 865                command: None,
 866                text_document: lsp::VersionedTextDocumentIdentifier {
 867                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
 868                    version: 0,
 869                },
 870            }],
 871        );
 872        cx.update_editor(|editor, window, cx| {
 873            editor.show_edit_prediction(&Default::default(), window, cx)
 874        });
 875        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 876        cx.update_editor(|editor, _, cx| {
 877            assert!(!editor.context_menu_visible());
 878            assert!(editor.has_active_edit_prediction());
 879            assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
 880            assert_eq!(editor.text(cx), "one\ntw\nthree\n");
 881        });
 882
 883        cx.simulate_keystroke("o");
 884        drop(handle_completion_request(
 885            &mut cx,
 886            indoc! {"
 887                one
 888                two|<>
 889                three
 890            "},
 891            vec!["completion_a_2", "completion_b_2"],
 892        ));
 893        handle_copilot_completion_request(
 894            &copilot_lsp,
 895            vec![crate::request::NextEditSuggestion {
 896                text: "two.foo()".into(),
 897                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
 898                command: None,
 899                text_document: lsp::VersionedTextDocumentIdentifier {
 900                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
 901                    version: 0,
 902                },
 903            }],
 904        );
 905        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 906        cx.update_editor(|editor, _, cx| {
 907            assert!(!editor.context_menu_visible());
 908            assert!(editor.has_active_edit_prediction());
 909            assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
 910            assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
 911        });
 912
 913        cx.simulate_keystroke(".");
 914        drop(handle_completion_request(
 915            &mut cx,
 916            indoc! {"
 917                one
 918                two.|<>
 919                three
 920            "},
 921            vec!["something_else()"],
 922        ));
 923        handle_copilot_completion_request(
 924            &copilot_lsp,
 925            vec![crate::request::NextEditSuggestion {
 926                text: "two.foo()".into(),
 927                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
 928                command: None,
 929                text_document: lsp::VersionedTextDocumentIdentifier {
 930                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
 931                    version: 0,
 932                },
 933            }],
 934        );
 935        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
 936        cx.update_editor(|editor, _, cx| {
 937            assert!(editor.context_menu_visible());
 938            assert!(editor.has_active_edit_prediction());
 939            assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
 940            assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
 941        });
 942    }
 943
 944    #[gpui::test]
 945    async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
 946        init_test(cx, |settings| {
 947            settings
 948                .edit_predictions
 949                .get_or_insert(Default::default())
 950                .disabled_globs = Some(vec![".env*".to_string()]);
 951        });
 952
 953        let (copilot, copilot_lsp) = Copilot::fake(cx);
 954
 955        let fs = FakeFs::new(cx.executor());
 956        fs.insert_tree(
 957            path!("/test"),
 958            json!({
 959                ".env": "SECRET=something\n",
 960                "README.md": "hello\nworld\nhow\nare\nyou\ntoday"
 961            }),
 962        )
 963        .await;
 964        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
 965
 966        let private_buffer = project
 967            .update(cx, |project, cx| {
 968                project.open_local_buffer(path!("/test/.env"), cx)
 969            })
 970            .await
 971            .unwrap();
 972        let public_buffer = project
 973            .update(cx, |project, cx| {
 974                project.open_local_buffer(path!("/test/README.md"), cx)
 975            })
 976            .await
 977            .unwrap();
 978
 979        let multibuffer = cx.new(|cx| {
 980            let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
 981            multibuffer.set_excerpts_for_path(
 982                PathKey::sorted(0),
 983                private_buffer.clone(),
 984                [Point::new(0, 0)..Point::new(1, 0)],
 985                0,
 986                cx,
 987            );
 988            multibuffer.set_excerpts_for_path(
 989                PathKey::sorted(1),
 990                public_buffer.clone(),
 991                [Point::new(0, 0)..Point::new(6, 0)],
 992                0,
 993                cx,
 994            );
 995            multibuffer
 996        });
 997        let editor =
 998            cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
 999        editor
1000            .update(cx, |editor, window, cx| {
1001                use gpui::Focusable;
1002                window.focus(&editor.focus_handle(cx), cx)
1003            })
1004            .unwrap();
1005        let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
1006        editor
1007            .update(cx, |editor, window, cx| {
1008                editor.set_edit_prediction_provider(Some(copilot_provider), window, cx)
1009            })
1010            .unwrap();
1011
1012        let mut copilot_requests = copilot_lsp
1013            .set_request_handler::<crate::request::NextEditSuggestions, _, _>(
1014                move |_params, _cx| async move {
1015                    Ok(crate::request::NextEditSuggestionsResult {
1016                        edits: vec![crate::request::NextEditSuggestion {
1017                            text: "next line".into(),
1018                            range: lsp::Range::new(
1019                                lsp::Position::new(1, 0),
1020                                lsp::Position::new(1, 0),
1021                            ),
1022                            command: None,
1023                            text_document: lsp::VersionedTextDocumentIdentifier {
1024                                uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1025                                version: 0,
1026                            },
1027                        }],
1028                    })
1029                },
1030            );
1031
1032        _ = editor.update(cx, |editor, window, cx| {
1033            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1034                selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
1035            });
1036            editor.refresh_edit_prediction(true, false, window, cx);
1037        });
1038
1039        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1040        assert!(copilot_requests.try_next().is_err());
1041
1042        _ = editor.update(cx, |editor, window, cx| {
1043            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1044                s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
1045            });
1046            editor.refresh_edit_prediction(true, false, window, cx);
1047        });
1048
1049        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
1050        assert!(copilot_requests.try_next().is_ok());
1051    }
1052
1053    fn handle_copilot_completion_request(
1054        lsp: &lsp::FakeLanguageServer,
1055        completions: Vec<crate::request::NextEditSuggestion>,
1056    ) {
1057        lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
1058            move |_params, _cx| {
1059                let completions = completions.clone();
1060                async move {
1061                    Ok(crate::request::NextEditSuggestionsResult {
1062                        edits: completions.clone(),
1063                    })
1064                }
1065            },
1066        );
1067    }
1068
1069    fn handle_completion_request(
1070        cx: &mut EditorLspTestContext,
1071        marked_string: &str,
1072        completions: Vec<&'static str>,
1073    ) -> impl Future<Output = ()> {
1074        let complete_from_marker: TextRangeMarker = '|'.into();
1075        let replace_range_marker: TextRangeMarker = ('<', '>').into();
1076        let (_, mut marked_ranges) = marked_text_ranges_by(
1077            marked_string,
1078            vec![complete_from_marker, replace_range_marker.clone()],
1079        );
1080
1081        let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
1082        let replace_range =
1083            cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
1084
1085        let mut request =
1086            cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
1087                let completions = completions.clone();
1088                async move {
1089                    assert_eq!(params.text_document_position.text_document.uri, url.clone());
1090                    Ok(Some(lsp::CompletionResponse::Array(
1091                        completions
1092                            .iter()
1093                            .map(|completion_text| lsp::CompletionItem {
1094                                label: completion_text.to_string(),
1095                                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1096                                    range: replace_range,
1097                                    new_text: completion_text.to_string(),
1098                                })),
1099                                ..Default::default()
1100                            })
1101                            .collect(),
1102                    )))
1103                }
1104            });
1105
1106        async move {
1107            request.next().await;
1108        }
1109    }
1110
1111    fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
1112        cx.update(|cx| {
1113            let store = SettingsStore::test(cx);
1114            cx.set_global(store);
1115            theme::init(theme::LoadThemes::JustBase, cx);
1116            SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
1117                store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
1118            });
1119        });
1120    }
1121}