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