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