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