edit_prediction_tests.rs

   1use super::*;
   2use crate::udiff::apply_diff_to_string;
   3use client::{UserStore, test::FakeServer};
   4use clock::FakeSystemClock;
   5use clock::ReplicaId;
   6use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
   7use cloud_llm_client::{
   8    EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody,
   9    predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response},
  10};
  11
  12use futures::{
  13    AsyncReadExt, FutureExt, StreamExt,
  14    channel::{mpsc, oneshot},
  15};
  16use gpui::App;
  17use gpui::{
  18    Entity, TestAppContext,
  19    http_client::{FakeHttpClient, Response},
  20};
  21use indoc::indoc;
  22use language::{
  23    Anchor, Buffer, Capability, CursorShape, Diagnostic, DiagnosticEntry, DiagnosticSet,
  24    DiagnosticSeverity, Operation, Point, Selection, SelectionGoal,
  25};
  26use language_model::RefreshLlmTokenListener;
  27use lsp::LanguageServerId;
  28use parking_lot::Mutex;
  29use pretty_assertions::{assert_eq, assert_matches};
  30use project::{FakeFs, Project};
  31use serde_json::json;
  32use settings::SettingsStore;
  33use std::{ops::Range, path::Path, sync::Arc, time::Duration};
  34use util::{
  35    path,
  36    test::{TextRangeMarker, marked_text_ranges_by},
  37};
  38use uuid::Uuid;
  39use workspace::{AppState, CollaboratorId, MultiWorkspace};
  40use zeta_prompt::ZetaPromptInput;
  41
  42use crate::{
  43    BufferEditPrediction, EDIT_PREDICTION_SETTLED_QUIESCENCE, EditPredictionId,
  44    EditPredictionJumpsFeatureFlag, EditPredictionStore, REJECT_REQUEST_DEBOUNCE,
  45};
  46
  47#[gpui::test]
  48async fn test_current_state(cx: &mut TestAppContext) {
  49    let (ep_store, mut requests) = init_test_with_fake_client(cx);
  50    let fs = FakeFs::new(cx.executor());
  51    fs.insert_tree(
  52        "/root",
  53        json!({
  54            "1.txt": "Hello!\nHow\nBye\n",
  55            "2.txt": "Hola!\nComo\nAdios\n"
  56        }),
  57    )
  58    .await;
  59    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
  60
  61    let buffer1 = project
  62        .update(cx, |project, cx| {
  63            let path = project.find_project_path(path!("/root/1.txt"), cx).unwrap();
  64            project.set_active_path(Some(path.clone()), cx);
  65            project.open_buffer(path, cx)
  66        })
  67        .await
  68        .unwrap();
  69    let snapshot1 = buffer1.read_with(cx, |buffer, _cx| buffer.snapshot());
  70    let position = snapshot1.anchor_before(language::Point::new(1, 3));
  71
  72    ep_store.update(cx, |ep_store, cx| {
  73        ep_store.register_project(&project, cx);
  74        ep_store.register_buffer(&buffer1, &project, cx);
  75    });
  76
  77    // Prediction for current file
  78
  79    ep_store.update(cx, |ep_store, cx| {
  80        ep_store.refresh_prediction_from_buffer(project.clone(), buffer1.clone(), position, cx)
  81    });
  82    let (request, respond_tx) = requests.predict.next().await.unwrap();
  83
  84    respond_tx
  85        .send(model_response(
  86            &request,
  87            indoc! {r"
  88                --- a/root/1.txt
  89                +++ b/root/1.txt
  90                @@ ... @@
  91                 Hello!
  92                -How
  93                +How are you?
  94                 Bye
  95            "},
  96        ))
  97        .unwrap();
  98
  99    cx.run_until_parked();
 100
 101    ep_store.update(cx, |ep_store, cx| {
 102        let prediction = ep_store
 103            .prediction_at(&buffer1, None, &project, cx)
 104            .unwrap();
 105        assert_matches!(prediction, BufferEditPrediction::Local { .. });
 106    });
 107
 108    ep_store.update(cx, |ep_store, cx| {
 109        ep_store.reject_current_prediction(EditPredictionRejectReason::Discarded, &project, cx);
 110    });
 111
 112    // Prediction for diagnostic in another file
 113
 114    let diagnostic = lsp::Diagnostic {
 115        range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
 116        severity: Some(lsp::DiagnosticSeverity::ERROR),
 117        message: "Sentence is incomplete".to_string(),
 118        ..Default::default()
 119    };
 120
 121    project.update(cx, |project, cx| {
 122        project.lsp_store().update(cx, |lsp_store, cx| {
 123            lsp_store
 124                .update_diagnostics(
 125                    LanguageServerId(0),
 126                    lsp::PublishDiagnosticsParams {
 127                        uri: lsp::Uri::from_file_path(path!("/root/2.txt")).unwrap(),
 128                        diagnostics: vec![diagnostic],
 129                        version: None,
 130                    },
 131                    None,
 132                    language::DiagnosticSourceKind::Pushed,
 133                    &[],
 134                    cx,
 135                )
 136                .unwrap();
 137        });
 138    });
 139
 140    let (request, respond_tx) = requests.predict.next().await.unwrap();
 141    respond_tx
 142        .send(model_response(
 143            &request,
 144            indoc! {r#"
 145                --- a/root/2.txt
 146                +++ b/root/2.txt
 147                @@ ... @@
 148                 Hola!
 149                -Como
 150                +Como estas?
 151                 Adios
 152            "#},
 153        ))
 154        .unwrap();
 155    cx.run_until_parked();
 156
 157    ep_store.update(cx, |ep_store, cx| {
 158        let prediction = ep_store
 159            .prediction_at(&buffer1, None, &project, cx)
 160            .unwrap();
 161        assert_matches!(
 162            prediction,
 163            BufferEditPrediction::Jump { prediction } if prediction.snapshot.file().unwrap().full_path(cx) == Path::new(path!("root/2.txt"))
 164        );
 165    });
 166
 167    let buffer2 = project
 168        .update(cx, |project, cx| {
 169            let path = project.find_project_path(path!("root/2.txt"), cx).unwrap();
 170            project.open_buffer(path, cx)
 171        })
 172        .await
 173        .unwrap();
 174
 175    ep_store.update(cx, |ep_store, cx| {
 176        let prediction = ep_store
 177            .prediction_at(&buffer2, None, &project, cx)
 178            .unwrap();
 179        assert_matches!(prediction, BufferEditPrediction::Local { .. });
 180    });
 181}
 182
 183#[gpui::test]
 184async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppContext) {
 185    let (ep_store, mut requests) = init_test_with_fake_client(cx);
 186
 187    cx.update(|cx| {
 188        cx.update_flags(
 189            false,
 190            vec![EditPredictionJumpsFeatureFlag::NAME.to_string()],
 191        );
 192    });
 193
 194    let fs = FakeFs::new(cx.executor());
 195    fs.insert_tree(
 196        "/root",
 197        json!({
 198            "1.txt": "Hello!\nHow\nBye\n",
 199            "2.txt": "Hola!\nComo\nAdios\n"
 200        }),
 201    )
 202    .await;
 203    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 204
 205    let app_state = cx.update(|cx| {
 206        let app_state = AppState::test(cx);
 207        AppState::set_global(Arc::downgrade(&app_state), cx);
 208        app_state
 209    });
 210
 211    let multi_workspace =
 212        cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 213    let workspace = multi_workspace
 214        .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
 215        .unwrap();
 216    cx.update(|cx| {
 217        AppState::set_global(Arc::downgrade(workspace.read(cx).app_state()), cx);
 218    });
 219    let _ = app_state;
 220
 221    let buffer1 = project
 222        .update(cx, |project, cx| {
 223            let path = project.find_project_path(path!("root/1.txt"), cx).unwrap();
 224            project.set_active_path(Some(path.clone()), cx);
 225            project.open_buffer(path, cx)
 226        })
 227        .await
 228        .unwrap();
 229    let snapshot1 = buffer1.read_with(cx, |buffer, _cx| buffer.snapshot());
 230    let position = snapshot1.anchor_before(language::Point::new(1, 3));
 231
 232    ep_store.update(cx, |ep_store, cx| {
 233        ep_store.register_project(&project, cx);
 234        ep_store.register_buffer(&buffer1, &project, cx);
 235        ep_store.refresh_prediction_from_buffer(project.clone(), buffer1.clone(), position, cx);
 236    });
 237
 238    let (request, respond_tx) = requests.predict.next().await.unwrap();
 239    respond_tx
 240        .send(model_response(
 241            &request,
 242            indoc! {r"
 243                --- a/root/1.txt
 244                +++ b/root/1.txt
 245                @@ ... @@
 246                 Hello!
 247                -How
 248                +How are you?
 249                 Bye
 250            "},
 251        ))
 252        .unwrap();
 253    cx.run_until_parked();
 254
 255    ep_store.update(cx, |ep_store, cx| {
 256        ep_store.reject_current_prediction(EditPredictionRejectReason::Discarded, &project, cx);
 257    });
 258
 259    let _ = multi_workspace.update(cx, |multi_workspace, window, cx| {
 260        multi_workspace.workspace().update(cx, |workspace, cx| {
 261            workspace.start_following(CollaboratorId::Agent, window, cx);
 262        });
 263    });
 264    cx.run_until_parked();
 265
 266    let diagnostic = lsp::Diagnostic {
 267        range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
 268        severity: Some(lsp::DiagnosticSeverity::ERROR),
 269        message: "Sentence is incomplete".to_string(),
 270        ..Default::default()
 271    };
 272
 273    project.update(cx, |project, cx| {
 274        project.lsp_store().update(cx, |lsp_store, cx| {
 275            lsp_store
 276                .update_diagnostics(
 277                    LanguageServerId(0),
 278                    lsp::PublishDiagnosticsParams {
 279                        uri: lsp::Uri::from_file_path(path!("/root/2.txt")).unwrap(),
 280                        diagnostics: vec![diagnostic.clone()],
 281                        version: None,
 282                    },
 283                    None,
 284                    language::DiagnosticSourceKind::Pushed,
 285                    &[],
 286                    cx,
 287                )
 288                .unwrap();
 289        });
 290    });
 291
 292    cx.run_until_parked();
 293    assert_no_predict_request_ready(&mut requests.predict);
 294
 295    let _ = multi_workspace.update(cx, |multi_workspace, window, cx| {
 296        multi_workspace.workspace().update(cx, |workspace, cx| {
 297            workspace.unfollow(CollaboratorId::Agent, window, cx);
 298        });
 299    });
 300    cx.run_until_parked();
 301
 302    project.update(cx, |project, cx| {
 303        project.lsp_store().update(cx, |lsp_store, cx| {
 304            lsp_store
 305                .update_diagnostics(
 306                    LanguageServerId(0),
 307                    lsp::PublishDiagnosticsParams {
 308                        uri: lsp::Uri::from_file_path(path!("/root/2.txt")).unwrap(),
 309                        diagnostics: vec![diagnostic],
 310                        version: None,
 311                    },
 312                    None,
 313                    language::DiagnosticSourceKind::Pushed,
 314                    &[],
 315                    cx,
 316                )
 317                .unwrap();
 318        });
 319    });
 320
 321    let (request, respond_tx) = requests.predict.next().await.unwrap();
 322    respond_tx
 323        .send(model_response(
 324            &request,
 325            indoc! {r#"
 326                --- a/root/2.txt
 327                +++ b/root/2.txt
 328                @@ ... @@
 329                 Hola!
 330                -Como
 331                +Como estas?
 332                 Adios
 333            "#},
 334        ))
 335        .unwrap();
 336    cx.run_until_parked();
 337
 338    ep_store.update(cx, |ep_store, cx| {
 339        let prediction = ep_store
 340            .prediction_at(&buffer1, None, &project, cx)
 341            .unwrap();
 342        assert_matches!(
 343            prediction,
 344            BufferEditPrediction::Jump { prediction } if prediction.snapshot.file().unwrap().full_path(cx) == Path::new(path!("root/2.txt"))
 345        );
 346    });
 347}
 348
 349#[gpui::test]
 350async fn test_simple_request(cx: &mut TestAppContext) {
 351    let (ep_store, mut requests) = init_test_with_fake_client(cx);
 352    let fs = FakeFs::new(cx.executor());
 353    fs.insert_tree(
 354        "/root",
 355        json!({
 356            "foo.md":  "Hello!\nHow\nBye\n"
 357        }),
 358    )
 359    .await;
 360    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 361
 362    let buffer = project
 363        .update(cx, |project, cx| {
 364            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
 365            project.open_buffer(path, cx)
 366        })
 367        .await
 368        .unwrap();
 369    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 370    let position = snapshot.anchor_before(language::Point::new(1, 3));
 371
 372    let prediction_task = ep_store.update(cx, |ep_store, cx| {
 373        ep_store.request_prediction(&project, &buffer, position, Default::default(), cx)
 374    });
 375
 376    let (request, respond_tx) = requests.predict.next().await.unwrap();
 377
 378    // TODO Put back when we have a structured request again
 379    // assert_eq!(
 380    //     request.excerpt_path.as_ref(),
 381    //     Path::new(path!("root/foo.md"))
 382    // );
 383    // assert_eq!(
 384    //     request.cursor_point,
 385    //     Point {
 386    //         line: Line(1),
 387    //         column: 3
 388    //     }
 389    // );
 390
 391    respond_tx
 392        .send(model_response(
 393            &request,
 394            indoc! { r"
 395                --- a/root/foo.md
 396                +++ b/root/foo.md
 397                @@ ... @@
 398                 Hello!
 399                -How
 400                +How are you?
 401                 Bye
 402            "},
 403        ))
 404        .unwrap();
 405
 406    let prediction = prediction_task.await.unwrap().unwrap().prediction.unwrap();
 407
 408    assert_eq!(prediction.edits.len(), 1);
 409    assert_eq!(
 410        prediction.edits[0].0.to_point(&snapshot).start,
 411        language::Point::new(1, 3)
 412    );
 413    assert_eq!(prediction.edits[0].1.as_ref(), " are you?");
 414}
 415
 416#[gpui::test]
 417async fn test_request_events(cx: &mut TestAppContext) {
 418    let (ep_store, mut requests) = init_test_with_fake_client(cx);
 419    let fs = FakeFs::new(cx.executor());
 420    fs.insert_tree(
 421        "/root",
 422        json!({
 423            "foo.md": "Hello!\n\nBye\n"
 424        }),
 425    )
 426    .await;
 427    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 428
 429    let buffer = project
 430        .update(cx, |project, cx| {
 431            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
 432            project.open_buffer(path, cx)
 433        })
 434        .await
 435        .unwrap();
 436
 437    ep_store.update(cx, |ep_store, cx| {
 438        ep_store.register_buffer(&buffer, &project, cx);
 439    });
 440
 441    buffer.update(cx, |buffer, cx| {
 442        buffer.edit(vec![(7..7, "How")], None, cx);
 443    });
 444
 445    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
 446    let position = snapshot.anchor_before(language::Point::new(1, 3));
 447
 448    let prediction_task = ep_store.update(cx, |ep_store, cx| {
 449        ep_store.request_prediction(&project, &buffer, position, Default::default(), cx)
 450    });
 451
 452    let (request, respond_tx) = requests.predict.next().await.unwrap();
 453
 454    let prompt = prompt_from_request(&request);
 455    assert!(
 456        prompt.contains(indoc! {"
 457        --- a/root/foo.md
 458        +++ b/root/foo.md
 459        @@ -1,3 +1,3 @@
 460         Hello!
 461        -
 462        +How
 463         Bye
 464    "}),
 465        "{prompt}"
 466    );
 467
 468    respond_tx
 469        .send(model_response(
 470            &request,
 471            indoc! {r#"
 472                --- a/root/foo.md
 473                +++ b/root/foo.md
 474                @@ ... @@
 475                 Hello!
 476                -How
 477                +How are you?
 478                 Bye
 479        "#},
 480        ))
 481        .unwrap();
 482
 483    let prediction = prediction_task.await.unwrap().unwrap().prediction.unwrap();
 484
 485    assert_eq!(prediction.edits.len(), 1);
 486    assert_eq!(prediction.edits[0].1.as_ref(), " are you?");
 487}
 488
 489#[gpui::test]
 490async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContext) {
 491    let (ep_store, _requests) = init_test_with_fake_client(cx);
 492    let fs = FakeFs::new(cx.executor());
 493    fs.insert_tree(
 494        "/root",
 495        json!({
 496            "foo.md": "Hello!\n\nBye\n"
 497        }),
 498    )
 499    .await;
 500    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 501
 502    let buffer = project
 503        .update(cx, |project, cx| {
 504            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
 505            project.open_buffer(path, cx)
 506        })
 507        .await
 508        .unwrap();
 509
 510    ep_store.update(cx, |ep_store, cx| {
 511        ep_store.register_buffer(&buffer, &project, cx);
 512    });
 513
 514    // First burst: insert "How"
 515    buffer.update(cx, |buffer, cx| {
 516        buffer.edit(vec![(7..7, "How")], None, cx);
 517    });
 518
 519    // Simulate a pause longer than the grouping threshold (e.g. 500ms).
 520    cx.executor().advance_clock(LAST_CHANGE_GROUPING_TIME * 2);
 521    cx.run_until_parked();
 522
 523    // Second burst: append " are you?" immediately after "How" on the same line.
 524    //
 525    // Keeping both bursts on the same line ensures the existing line-span coalescing logic
 526    // groups them into a single `LastEvent`, allowing the pause-split getter to return two diffs.
 527    buffer.update(cx, |buffer, cx| {
 528        buffer.edit(vec![(10..10, " are you?")], None, cx);
 529    });
 530
 531    // A second edit shortly after the first post-pause edit ensures the last edit timestamp is
 532    // advanced after the pause boundary is recorded, making pause-splitting deterministic.
 533    buffer.update(cx, |buffer, cx| {
 534        buffer.edit(vec![(19..19, "!")], None, cx);
 535    });
 536
 537    // With time-based splitting, there are two distinct events.
 538    let events = ep_store.update(cx, |ep_store, cx| {
 539        ep_store.edit_history_for_project(&project, cx)
 540    });
 541    assert_eq!(events.len(), 2);
 542
 543    let first_total_edit_range = buffer.read_with(cx, |buffer, _| {
 544        events[0].total_edit_range.to_point(&buffer.snapshot())
 545    });
 546    assert_eq!(first_total_edit_range, Point::new(1, 0)..Point::new(1, 3));
 547
 548    let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
 549    assert_eq!(
 550        diff.as_str(),
 551        indoc! {"
 552            @@ -1,3 +1,3 @@
 553             Hello!
 554            -
 555            +How
 556             Bye
 557        "}
 558    );
 559
 560    let second_total_edit_range = buffer.read_with(cx, |buffer, _| {
 561        events[1].total_edit_range.to_point(&buffer.snapshot())
 562    });
 563    assert_eq!(second_total_edit_range, Point::new(1, 3)..Point::new(1, 13));
 564
 565    let zeta_prompt::Event::BufferChange { diff, .. } = events[1].event.as_ref();
 566    assert_eq!(
 567        diff.as_str(),
 568        indoc! {"
 569            @@ -1,3 +1,3 @@
 570             Hello!
 571            -How
 572            +How are you?!
 573             Bye
 574        "}
 575    );
 576}
 577
 578#[gpui::test]
 579async fn test_predicted_edits_are_separated_in_edit_history(cx: &mut TestAppContext) {
 580    let (ep_store, _requests) = init_test_with_fake_client(cx);
 581    let fs = FakeFs::new(cx.executor());
 582
 583    // Create a file with 30 lines to test line-based coalescing
 584    let content = (1..=30)
 585        .map(|i| format!("Line {}\n", i))
 586        .collect::<String>();
 587    fs.insert_tree(
 588        "/root",
 589        json!({
 590            "foo.md": content
 591        }),
 592    )
 593    .await;
 594    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 595
 596    let buffer = project
 597        .update(cx, |project, cx| {
 598            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
 599            project.open_buffer(path, cx)
 600        })
 601        .await
 602        .unwrap();
 603
 604    ep_store.update(cx, |ep_store, cx| {
 605        ep_store.register_buffer(&buffer, &project, cx);
 606    });
 607
 608    // First edit: multi-line edit spanning rows 10-12 (replacing lines 11-13)
 609    buffer.update(cx, |buffer, cx| {
 610        let start = Point::new(10, 0).to_offset(buffer);
 611        let end = Point::new(13, 0).to_offset(buffer);
 612        buffer.edit(vec![(start..end, "Middle A\nMiddle B\n")], None, cx);
 613    });
 614
 615    let events = ep_store.update(cx, |ep_store, cx| {
 616        ep_store.edit_history_for_project(&project, cx)
 617    });
 618    assert_eq!(
 619        render_events(&events),
 620        indoc! {"
 621            @@ -8,9 +8,8 @@
 622             Line 8
 623             Line 9
 624             Line 10
 625            -Line 11
 626            -Line 12
 627            -Line 13
 628            +Middle A
 629            +Middle B
 630             Line 14
 631             Line 15
 632             Line 16
 633        "},
 634        "After first edit"
 635    );
 636
 637    // Second edit: insert ABOVE the first edit's range (row 5, within 8 lines of row 10)
 638    // This tests that coalescing considers the START of the existing range
 639    buffer.update(cx, |buffer, cx| {
 640        let offset = Point::new(5, 0).to_offset(buffer);
 641        buffer.edit(vec![(offset..offset, "Above\n")], None, cx);
 642    });
 643
 644    let events = ep_store.update(cx, |ep_store, cx| {
 645        ep_store.edit_history_for_project(&project, cx)
 646    });
 647    assert_eq!(
 648        render_events(&events),
 649        indoc! {"
 650            @@ -3,14 +3,14 @@
 651             Line 3
 652             Line 4
 653             Line 5
 654            +Above
 655             Line 6
 656             Line 7
 657             Line 8
 658             Line 9
 659             Line 10
 660            -Line 11
 661            -Line 12
 662            -Line 13
 663            +Middle A
 664            +Middle B
 665             Line 14
 666             Line 15
 667             Line 16
 668        "},
 669        "After inserting above (should coalesce)"
 670    );
 671
 672    // Third edit: insert BELOW the first edit's range (row 14 in current buffer, within 8 lines of row 12)
 673    // This tests that coalescing considers the END of the existing range
 674    buffer.update(cx, |buffer, cx| {
 675        let offset = Point::new(14, 0).to_offset(buffer);
 676        buffer.edit(vec![(offset..offset, "Below\n")], None, cx);
 677    });
 678
 679    let events = ep_store.update(cx, |ep_store, cx| {
 680        ep_store.edit_history_for_project(&project, cx)
 681    });
 682    assert_eq!(
 683        render_events(&events),
 684        indoc! {"
 685            @@ -3,15 +3,16 @@
 686             Line 3
 687             Line 4
 688             Line 5
 689            +Above
 690             Line 6
 691             Line 7
 692             Line 8
 693             Line 9
 694             Line 10
 695            -Line 11
 696            -Line 12
 697            -Line 13
 698            +Middle A
 699            +Middle B
 700             Line 14
 701            +Below
 702             Line 15
 703             Line 16
 704             Line 17
 705        "},
 706        "After inserting below (should coalesce)"
 707    );
 708
 709    // Fourth edit: insert FAR BELOW (row 25, beyond 8 lines from the current range end ~row 15)
 710    // This should NOT coalesce - creates a new event
 711    buffer.update(cx, |buffer, cx| {
 712        let offset = Point::new(25, 0).to_offset(buffer);
 713        buffer.edit(vec![(offset..offset, "Far below\n")], None, cx);
 714    });
 715
 716    let events = ep_store.update(cx, |ep_store, cx| {
 717        ep_store.edit_history_for_project(&project, cx)
 718    });
 719    assert_eq!(
 720        render_events(&events),
 721        indoc! {"
 722            @@ -3,15 +3,16 @@
 723             Line 3
 724             Line 4
 725             Line 5
 726            +Above
 727             Line 6
 728             Line 7
 729             Line 8
 730             Line 9
 731             Line 10
 732            -Line 11
 733            -Line 12
 734            -Line 13
 735            +Middle A
 736            +Middle B
 737             Line 14
 738            +Below
 739             Line 15
 740             Line 16
 741             Line 17
 742
 743            ---
 744            @@ -23,6 +23,7 @@
 745             Line 22
 746             Line 23
 747             Line 24
 748            +Far below
 749             Line 25
 750             Line 26
 751             Line 27
 752        "},
 753        "After inserting far below (should NOT coalesce)"
 754    );
 755}
 756
 757fn render_events(events: &[StoredEvent]) -> String {
 758    events
 759        .iter()
 760        .map(|e| {
 761            let zeta_prompt::Event::BufferChange { diff, .. } = e.event.as_ref();
 762            diff.as_str()
 763        })
 764        .collect::<Vec<_>>()
 765        .join("\n---\n")
 766}
 767
 768fn render_events_with_predicted(events: &[StoredEvent]) -> Vec<String> {
 769    events
 770        .iter()
 771        .map(|e| {
 772            let zeta_prompt::Event::BufferChange {
 773                diff, predicted, ..
 774            } = e.event.as_ref();
 775            let prefix = if *predicted { "predicted" } else { "manual" };
 776            format!("{}\n{}", prefix, diff)
 777        })
 778        .collect()
 779}
 780
 781fn make_collaborator_replica(
 782    buffer: &Entity<Buffer>,
 783    cx: &mut TestAppContext,
 784) -> (Entity<Buffer>, clock::Global) {
 785    let (state, version) =
 786        buffer.read_with(cx, |buffer, _cx| (buffer.to_proto(_cx), buffer.version()));
 787    let collaborator = cx.new(|_cx| {
 788        Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None).unwrap()
 789    });
 790    (collaborator, version)
 791}
 792
 793async fn apply_collaborator_edit(
 794    collaborator: &Entity<Buffer>,
 795    buffer: &Entity<Buffer>,
 796    since_version: &mut clock::Global,
 797    edit_range: Range<usize>,
 798    new_text: &str,
 799    cx: &mut TestAppContext,
 800) {
 801    collaborator.update(cx, |collaborator, cx| {
 802        collaborator.edit([(edit_range, new_text)], None, cx);
 803    });
 804
 805    let serialize_task = collaborator.read_with(cx, |collaborator, cx| {
 806        collaborator.serialize_ops(Some(since_version.clone()), cx)
 807    });
 808    let ops = serialize_task.await;
 809    *since_version = collaborator.read_with(cx, |collaborator, _cx| collaborator.version());
 810
 811    buffer.update(cx, |buffer, cx| {
 812        buffer.apply_ops(
 813            ops.into_iter()
 814                .map(|op| language::proto::deserialize_operation(op).unwrap()),
 815            cx,
 816        );
 817    });
 818}
 819
 820#[gpui::test]
 821async fn test_nearby_collaborator_edits_are_kept_in_history(cx: &mut TestAppContext) {
 822    let (ep_store, _requests) = init_test_with_fake_client(cx);
 823    let fs = FakeFs::new(cx.executor());
 824    fs.insert_tree(
 825        "/root",
 826        json!({
 827            "foo.rs": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\n"
 828        }),
 829    )
 830    .await;
 831    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 832
 833    let buffer = project
 834        .update(cx, |project, cx| {
 835            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
 836            project.set_active_path(Some(path.clone()), cx);
 837            project.open_buffer(path, cx)
 838        })
 839        .await
 840        .unwrap();
 841
 842    let cursor = buffer.read_with(cx, |buffer, _cx| buffer.anchor_before(Point::new(1, 0)));
 843
 844    ep_store.update(cx, |ep_store, cx| {
 845        ep_store.register_buffer(&buffer, &project, cx);
 846        let _ = ep_store.prediction_at(&buffer, Some(cursor), &project, cx);
 847    });
 848
 849    buffer.update(cx, |buffer, cx| {
 850        buffer.edit(vec![(0..6, "LOCAL ZERO")], None, cx);
 851    });
 852
 853    let (collaborator, mut collaborator_version) = make_collaborator_replica(&buffer, cx);
 854
 855    let (line_one_start, line_one_len) = collaborator.read_with(cx, |buffer, _cx| {
 856        (Point::new(1, 0).to_offset(buffer), buffer.line_len(1))
 857    });
 858
 859    apply_collaborator_edit(
 860        &collaborator,
 861        &buffer,
 862        &mut collaborator_version,
 863        line_one_start..line_one_start + line_one_len as usize,
 864        "REMOTE ONE",
 865        cx,
 866    )
 867    .await;
 868
 869    let events = ep_store.update(cx, |ep_store, cx| {
 870        ep_store.edit_history_for_project(&project, cx)
 871    });
 872
 873    assert_eq!(
 874        render_events_with_predicted(&events),
 875        vec![indoc! {"
 876            manual
 877            @@ -1,5 +1,5 @@
 878            -line 0
 879            -line 1
 880            +LOCAL ZERO
 881            +REMOTE ONE
 882             line 2
 883             line 3
 884             line 4
 885        "}]
 886    );
 887}
 888
 889#[gpui::test]
 890async fn test_distant_collaborator_edits_are_omitted_from_history(cx: &mut TestAppContext) {
 891    let (ep_store, _requests) = init_test_with_fake_client(cx);
 892    let fs = FakeFs::new(cx.executor());
 893    fs.insert_tree(
 894        "/root",
 895        json!({
 896            "foo.rs": (0..1000)
 897                .map(|i| format!("line {i}\n"))
 898                .collect::<String>()
 899        }),
 900    )
 901    .await;
 902    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 903
 904    let buffer = project
 905        .update(cx, |project, cx| {
 906            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
 907            project.set_active_path(Some(path.clone()), cx);
 908            project.open_buffer(path, cx)
 909        })
 910        .await
 911        .unwrap();
 912
 913    let cursor = buffer.read_with(cx, |buffer, _cx| buffer.anchor_before(Point::new(1, 0)));
 914
 915    ep_store.update(cx, |ep_store, cx| {
 916        ep_store.register_buffer(&buffer, &project, cx);
 917        let _ = ep_store.prediction_at(&buffer, Some(cursor), &project, cx);
 918    });
 919
 920    buffer.update(cx, |buffer, cx| {
 921        buffer.edit(vec![(0..6, "LOCAL ZERO")], None, cx);
 922    });
 923
 924    let (collaborator, mut collaborator_version) = make_collaborator_replica(&buffer, cx);
 925
 926    let far_line_start = buffer.read_with(cx, |buffer, _cx| Point::new(900, 0).to_offset(buffer));
 927
 928    apply_collaborator_edit(
 929        &collaborator,
 930        &buffer,
 931        &mut collaborator_version,
 932        far_line_start..far_line_start + 7,
 933        "REMOTE FAR",
 934        cx,
 935    )
 936    .await;
 937
 938    let events = ep_store.update(cx, |ep_store, cx| {
 939        ep_store.edit_history_for_project(&project, cx)
 940    });
 941
 942    assert_eq!(
 943        render_events_with_predicted(&events),
 944        vec![indoc! {"
 945            manual
 946            @@ -1,4 +1,4 @@
 947            -line 0
 948            +LOCAL ZERO
 949             line 1
 950             line 2
 951             line 3
 952        "}]
 953    );
 954}
 955
 956#[gpui::test]
 957async fn test_irrelevant_collaborator_edits_in_different_files_are_omitted_from_history(
 958    cx: &mut TestAppContext,
 959) {
 960    let (ep_store, _requests) = init_test_with_fake_client(cx);
 961    let fs = FakeFs::new(cx.executor());
 962    fs.insert_tree(
 963        "/root",
 964        json!({
 965            "foo.rs": "line 0\nline 1\nline 2\nline 3\n",
 966            "bar.rs": "line 0\nline 1\nline 2\nline 3\n"
 967        }),
 968    )
 969    .await;
 970    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
 971
 972    let foo_buffer = project
 973        .update(cx, |project, cx| {
 974            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
 975            project.set_active_path(Some(path.clone()), cx);
 976            project.open_buffer(path, cx)
 977        })
 978        .await
 979        .unwrap();
 980    let bar_buffer = project
 981        .update(cx, |project, cx| {
 982            let path = project.find_project_path(path!("root/bar.rs"), cx).unwrap();
 983            project.open_buffer(path, cx)
 984        })
 985        .await
 986        .unwrap();
 987
 988    let foo_cursor = foo_buffer.read_with(cx, |buffer, _cx| buffer.anchor_before(Point::new(1, 0)));
 989
 990    ep_store.update(cx, |ep_store, cx| {
 991        ep_store.register_buffer(&foo_buffer, &project, cx);
 992        ep_store.register_buffer(&bar_buffer, &project, cx);
 993        let _ = ep_store.prediction_at(&foo_buffer, Some(foo_cursor), &project, cx);
 994    });
 995
 996    let (bar_collaborator, mut bar_version) = make_collaborator_replica(&bar_buffer, cx);
 997
 998    apply_collaborator_edit(
 999        &bar_collaborator,
1000        &bar_buffer,
1001        &mut bar_version,
1002        0..6,
1003        "REMOTE BAR",
1004        cx,
1005    )
1006    .await;
1007
1008    let events = ep_store.update(cx, |ep_store, cx| {
1009        ep_store.edit_history_for_project(&project, cx)
1010    });
1011
1012    assert!(events.is_empty());
1013}
1014
1015#[gpui::test]
1016async fn test_predicted_flag_coalescing(cx: &mut TestAppContext) {
1017    let (ep_store, _requests) = init_test_with_fake_client(cx);
1018    let fs = FakeFs::new(cx.executor());
1019    fs.insert_tree(
1020        "/root",
1021        json!({
1022            "foo.rs": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\n"
1023        }),
1024    )
1025    .await;
1026    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1027
1028    let buffer = project
1029        .update(cx, |project, cx| {
1030            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
1031            project.open_buffer(path, cx)
1032        })
1033        .await
1034        .unwrap();
1035
1036    ep_store.update(cx, |ep_store, cx| {
1037        ep_store.register_buffer(&buffer, &project, cx);
1038    });
1039
1040    // Case 1: Manual edits have `predicted` set to false.
1041    buffer.update(cx, |buffer, cx| {
1042        buffer.edit(vec![(0..6, "LINE ZERO")], None, cx);
1043    });
1044
1045    let events = ep_store.update(cx, |ep_store, cx| {
1046        ep_store.edit_history_for_project(&project, cx)
1047    });
1048
1049    assert_eq!(
1050        render_events_with_predicted(&events),
1051        vec![indoc! {"
1052            manual
1053            @@ -1,4 +1,4 @@
1054            -line 0
1055            +LINE ZERO
1056             line 1
1057             line 2
1058             line 3
1059        "}]
1060    );
1061
1062    // Case 2: Multiple successive manual edits near each other are merged into one
1063    // event with `predicted` set to false.
1064    buffer.update(cx, |buffer, cx| {
1065        let offset = Point::new(1, 0).to_offset(buffer);
1066        let end = Point::new(1, 6).to_offset(buffer);
1067        buffer.edit(vec![(offset..end, "LINE ONE")], None, cx);
1068    });
1069
1070    let events = ep_store.update(cx, |ep_store, cx| {
1071        ep_store.edit_history_for_project(&project, cx)
1072    });
1073    assert_eq!(
1074        render_events_with_predicted(&events),
1075        vec![indoc! {"
1076            manual
1077            @@ -1,5 +1,5 @@
1078            -line 0
1079            -line 1
1080            +LINE ZERO
1081            +LINE ONE
1082             line 2
1083             line 3
1084             line 4
1085        "}]
1086    );
1087
1088    // Case 3: Accepted predictions have `predicted` set to true.
1089    // Case 5: A manual edit that follows a predicted edit is not merged with the
1090    // predicted edit, even if it is nearby.
1091    ep_store.update(cx, |ep_store, cx| {
1092        buffer.update(cx, |buffer, cx| {
1093            let offset = Point::new(2, 0).to_offset(buffer);
1094            let end = Point::new(2, 6).to_offset(buffer);
1095            buffer.edit(vec![(offset..end, "LINE TWO")], None, cx);
1096        });
1097        ep_store.report_changes_for_buffer(&buffer, &project, true, true, cx);
1098    });
1099
1100    let events = ep_store.update(cx, |ep_store, cx| {
1101        ep_store.edit_history_for_project(&project, cx)
1102    });
1103    assert_eq!(
1104        render_events_with_predicted(&events),
1105        vec![
1106            indoc! {"
1107                manual
1108                @@ -1,5 +1,5 @@
1109                -line 0
1110                -line 1
1111                +LINE ZERO
1112                +LINE ONE
1113                 line 2
1114                 line 3
1115                 line 4
1116            "},
1117            indoc! {"
1118                predicted
1119                @@ -1,6 +1,6 @@
1120                 LINE ZERO
1121                 LINE ONE
1122                -line 2
1123                +LINE TWO
1124                 line 3
1125                 line 4
1126                 line 5
1127            "}
1128        ]
1129    );
1130
1131    // Case 4: Multiple successive accepted predictions near each other are merged
1132    // into one event with `predicted` set to true.
1133    ep_store.update(cx, |ep_store, cx| {
1134        buffer.update(cx, |buffer, cx| {
1135            let offset = Point::new(3, 0).to_offset(buffer);
1136            let end = Point::new(3, 6).to_offset(buffer);
1137            buffer.edit(vec![(offset..end, "LINE THREE")], None, cx);
1138        });
1139        ep_store.report_changes_for_buffer(&buffer, &project, true, true, cx);
1140    });
1141
1142    let events = ep_store.update(cx, |ep_store, cx| {
1143        ep_store.edit_history_for_project(&project, cx)
1144    });
1145    assert_eq!(
1146        render_events_with_predicted(&events),
1147        vec![
1148            indoc! {"
1149                manual
1150                @@ -1,5 +1,5 @@
1151                -line 0
1152                -line 1
1153                +LINE ZERO
1154                +LINE ONE
1155                 line 2
1156                 line 3
1157                 line 4
1158            "},
1159            indoc! {"
1160                predicted
1161                @@ -1,7 +1,7 @@
1162                 LINE ZERO
1163                 LINE ONE
1164                -line 2
1165                -line 3
1166                +LINE TWO
1167                +LINE THREE
1168                 line 4
1169                 line 5
1170                 line 6
1171            "}
1172        ]
1173    );
1174
1175    // Case 5 (continued): A manual edit that follows a predicted edit is not merged
1176    // with the predicted edit, even if it is nearby.
1177    buffer.update(cx, |buffer, cx| {
1178        let offset = Point::new(4, 0).to_offset(buffer);
1179        let end = Point::new(4, 6).to_offset(buffer);
1180        buffer.edit(vec![(offset..end, "LINE FOUR")], None, cx);
1181    });
1182
1183    let events = ep_store.update(cx, |ep_store, cx| {
1184        ep_store.edit_history_for_project(&project, cx)
1185    });
1186    assert_eq!(
1187        render_events_with_predicted(&events),
1188        vec![
1189            indoc! {"
1190                manual
1191                @@ -1,5 +1,5 @@
1192                -line 0
1193                -line 1
1194                +LINE ZERO
1195                +LINE ONE
1196                 line 2
1197                 line 3
1198                 line 4
1199            "},
1200            indoc! {"
1201                predicted
1202                @@ -1,7 +1,7 @@
1203                 LINE ZERO
1204                 LINE ONE
1205                -line 2
1206                -line 3
1207                +LINE TWO
1208                +LINE THREE
1209                 line 4
1210                 line 5
1211                 line 6
1212            "},
1213            indoc! {"
1214                manual
1215                @@ -2,7 +2,7 @@
1216                 LINE ONE
1217                 LINE TWO
1218                 LINE THREE
1219                -line 4
1220                +LINE FOUR
1221                 line 5
1222                 line 6
1223                 line 7
1224            "}
1225        ]
1226    );
1227
1228    // Case 6: If we then perform a manual edit at a *different* location (more than
1229    // 8 lines away), then the edits at the prior location can be merged with each
1230    // other, even if some are predicted and some are not. `predicted` means all
1231    // constituent edits were predicted.
1232    buffer.update(cx, |buffer, cx| {
1233        let offset = Point::new(14, 0).to_offset(buffer);
1234        let end = Point::new(14, 7).to_offset(buffer);
1235        buffer.edit(vec![(offset..end, "LINE FOURTEEN")], None, cx);
1236    });
1237
1238    let events = ep_store.update(cx, |ep_store, cx| {
1239        ep_store.edit_history_for_project(&project, cx)
1240    });
1241    assert_eq!(
1242        render_events_with_predicted(&events),
1243        vec![
1244            indoc! {"
1245                manual
1246                @@ -1,8 +1,8 @@
1247                -line 0
1248                -line 1
1249                -line 2
1250                -line 3
1251                -line 4
1252                +LINE ZERO
1253                +LINE ONE
1254                +LINE TWO
1255                +LINE THREE
1256                +LINE FOUR
1257                 line 5
1258                 line 6
1259                 line 7
1260            "},
1261            indoc! {"
1262                manual
1263                @@ -12,4 +12,4 @@
1264                 line 11
1265                 line 12
1266                 line 13
1267                -line 14
1268                +LINE FOURTEEN
1269            "}
1270        ]
1271    );
1272}
1273
1274#[gpui::test]
1275async fn test_empty_prediction(cx: &mut TestAppContext) {
1276    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1277    let fs = FakeFs::new(cx.executor());
1278    fs.insert_tree(
1279        "/root",
1280        json!({
1281            "foo.md":  "Hello!\nHow\nBye\n"
1282        }),
1283    )
1284    .await;
1285    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1286
1287    let buffer = project
1288        .update(cx, |project, cx| {
1289            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1290            project.open_buffer(path, cx)
1291        })
1292        .await
1293        .unwrap();
1294    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1295    let position = snapshot.anchor_before(language::Point::new(1, 3));
1296
1297    ep_store.update(cx, |ep_store, cx| {
1298        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1299    });
1300
1301    let (request, respond_tx) = requests.predict.next().await.unwrap();
1302    let response = model_response(&request, "");
1303    let id = response.request_id.clone();
1304    respond_tx.send(response).unwrap();
1305
1306    cx.run_until_parked();
1307
1308    ep_store.update(cx, |ep_store, cx| {
1309        assert!(
1310            ep_store
1311                .prediction_at(&buffer, None, &project, cx)
1312                .is_none()
1313        );
1314    });
1315
1316    // prediction is reported as rejected
1317    let (reject_request, _) = requests.reject.next().await.unwrap();
1318
1319    assert_eq!(
1320        &reject_request.rejections,
1321        &[EditPredictionRejection {
1322            request_id: id,
1323            reason: EditPredictionRejectReason::Empty,
1324            was_shown: false,
1325            model_version: None,
1326            e2e_latency_ms: Some(0),
1327        }]
1328    );
1329}
1330
1331#[gpui::test]
1332async fn test_interpolated_empty(cx: &mut TestAppContext) {
1333    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1334    let fs = FakeFs::new(cx.executor());
1335    fs.insert_tree(
1336        "/root",
1337        json!({
1338            "foo.md":  "Hello!\nHow\nBye\n"
1339        }),
1340    )
1341    .await;
1342    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1343
1344    let buffer = project
1345        .update(cx, |project, cx| {
1346            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1347            project.open_buffer(path, cx)
1348        })
1349        .await
1350        .unwrap();
1351    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1352    let position = snapshot.anchor_before(language::Point::new(1, 3));
1353
1354    ep_store.update(cx, |ep_store, cx| {
1355        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1356    });
1357
1358    let (request, respond_tx) = requests.predict.next().await.unwrap();
1359
1360    buffer.update(cx, |buffer, cx| {
1361        buffer.set_text("Hello!\nHow are you?\nBye", cx);
1362    });
1363
1364    let response = model_response(&request, SIMPLE_DIFF);
1365    let id = response.request_id.clone();
1366    respond_tx.send(response).unwrap();
1367
1368    cx.run_until_parked();
1369
1370    ep_store.update(cx, |ep_store, cx| {
1371        assert!(
1372            ep_store
1373                .prediction_at(&buffer, None, &project, cx)
1374                .is_none()
1375        );
1376    });
1377
1378    // prediction is reported as rejected
1379    let (reject_request, _) = requests.reject.next().await.unwrap();
1380
1381    assert_eq!(
1382        &reject_request.rejections,
1383        &[EditPredictionRejection {
1384            request_id: id,
1385            reason: EditPredictionRejectReason::InterpolatedEmpty,
1386            was_shown: false,
1387            model_version: None,
1388            e2e_latency_ms: Some(0),
1389        }]
1390    );
1391}
1392
1393const SIMPLE_DIFF: &str = indoc! { r"
1394    --- a/root/foo.md
1395    +++ b/root/foo.md
1396    @@ ... @@
1397     Hello!
1398    -How
1399    +How are you?
1400     Bye
1401"};
1402
1403#[gpui::test]
1404async fn test_replace_current(cx: &mut TestAppContext) {
1405    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1406    let fs = FakeFs::new(cx.executor());
1407    fs.insert_tree(
1408        "/root",
1409        json!({
1410            "foo.md":  "Hello!\nHow\nBye\n"
1411        }),
1412    )
1413    .await;
1414    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1415
1416    let buffer = project
1417        .update(cx, |project, cx| {
1418            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1419            project.open_buffer(path, cx)
1420        })
1421        .await
1422        .unwrap();
1423    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1424    let position = snapshot.anchor_before(language::Point::new(1, 3));
1425
1426    ep_store.update(cx, |ep_store, cx| {
1427        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1428    });
1429
1430    let (request, respond_tx) = requests.predict.next().await.unwrap();
1431    let first_response = model_response(&request, SIMPLE_DIFF);
1432    let first_id = first_response.request_id.clone();
1433    respond_tx.send(first_response).unwrap();
1434
1435    cx.run_until_parked();
1436
1437    ep_store.update(cx, |ep_store, cx| {
1438        assert_eq!(
1439            ep_store
1440                .prediction_at(&buffer, None, &project, cx)
1441                .unwrap()
1442                .id
1443                .0,
1444            first_id
1445        );
1446    });
1447
1448    // a second request is triggered
1449    ep_store.update(cx, |ep_store, cx| {
1450        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1451    });
1452
1453    let (request, respond_tx) = requests.predict.next().await.unwrap();
1454    let second_response = model_response(&request, SIMPLE_DIFF);
1455    let second_id = second_response.request_id.clone();
1456    respond_tx.send(second_response).unwrap();
1457
1458    cx.run_until_parked();
1459
1460    ep_store.update(cx, |ep_store, cx| {
1461        // second replaces first
1462        assert_eq!(
1463            ep_store
1464                .prediction_at(&buffer, None, &project, cx)
1465                .unwrap()
1466                .id
1467                .0,
1468            second_id
1469        );
1470    });
1471
1472    // first is reported as replaced
1473    let (reject_request, _) = requests.reject.next().await.unwrap();
1474
1475    assert_eq!(
1476        &reject_request.rejections,
1477        &[EditPredictionRejection {
1478            request_id: first_id,
1479            reason: EditPredictionRejectReason::Replaced,
1480            was_shown: false,
1481            model_version: None,
1482            e2e_latency_ms: Some(0),
1483        }]
1484    );
1485}
1486
1487#[gpui::test]
1488async fn test_current_preferred(cx: &mut TestAppContext) {
1489    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1490    let fs = FakeFs::new(cx.executor());
1491    fs.insert_tree(
1492        "/root",
1493        json!({
1494            "foo.md":  "Hello!\nHow\nBye\n"
1495        }),
1496    )
1497    .await;
1498    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1499
1500    let buffer = project
1501        .update(cx, |project, cx| {
1502            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1503            project.open_buffer(path, cx)
1504        })
1505        .await
1506        .unwrap();
1507    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1508    let position = snapshot.anchor_before(language::Point::new(1, 3));
1509
1510    ep_store.update(cx, |ep_store, cx| {
1511        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1512    });
1513
1514    let (request, respond_tx) = requests.predict.next().await.unwrap();
1515    let first_response = model_response(&request, SIMPLE_DIFF);
1516    let first_id = first_response.request_id.clone();
1517    respond_tx.send(first_response).unwrap();
1518
1519    cx.run_until_parked();
1520
1521    ep_store.update(cx, |ep_store, cx| {
1522        assert_eq!(
1523            ep_store
1524                .prediction_at(&buffer, None, &project, cx)
1525                .unwrap()
1526                .id
1527                .0,
1528            first_id
1529        );
1530    });
1531
1532    // a second request is triggered
1533    ep_store.update(cx, |ep_store, cx| {
1534        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1535    });
1536
1537    let (request, respond_tx) = requests.predict.next().await.unwrap();
1538    // worse than current prediction
1539    let second_response = model_response(
1540        &request,
1541        indoc! { r"
1542            --- a/root/foo.md
1543            +++ b/root/foo.md
1544            @@ ... @@
1545             Hello!
1546            -How
1547            +How are
1548             Bye
1549        "},
1550    );
1551    let second_id = second_response.request_id.clone();
1552    respond_tx.send(second_response).unwrap();
1553
1554    cx.run_until_parked();
1555
1556    ep_store.update(cx, |ep_store, cx| {
1557        // first is preferred over second
1558        assert_eq!(
1559            ep_store
1560                .prediction_at(&buffer, None, &project, cx)
1561                .unwrap()
1562                .id
1563                .0,
1564            first_id
1565        );
1566    });
1567
1568    // second is reported as rejected
1569    let (reject_request, _) = requests.reject.next().await.unwrap();
1570
1571    assert_eq!(
1572        &reject_request.rejections,
1573        &[EditPredictionRejection {
1574            request_id: second_id,
1575            reason: EditPredictionRejectReason::CurrentPreferred,
1576            was_shown: false,
1577            model_version: None,
1578            e2e_latency_ms: Some(0),
1579        }]
1580    );
1581}
1582
1583#[gpui::test]
1584async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
1585    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1586    let fs = FakeFs::new(cx.executor());
1587    fs.insert_tree(
1588        "/root",
1589        json!({
1590            "foo.md":  "Hello!\nHow\nBye\n"
1591        }),
1592    )
1593    .await;
1594    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1595
1596    let buffer = project
1597        .update(cx, |project, cx| {
1598            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1599            project.open_buffer(path, cx)
1600        })
1601        .await
1602        .unwrap();
1603    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1604    let position = snapshot.anchor_before(language::Point::new(1, 3));
1605
1606    // start two refresh tasks
1607    ep_store.update(cx, |ep_store, cx| {
1608        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1609    });
1610
1611    let (request1, respond_first) = requests.predict.next().await.unwrap();
1612
1613    ep_store.update(cx, |ep_store, cx| {
1614        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1615    });
1616
1617    let (request, respond_second) = requests.predict.next().await.unwrap();
1618
1619    // wait for throttle
1620    cx.run_until_parked();
1621
1622    // second responds first
1623    let second_response = model_response(&request, SIMPLE_DIFF);
1624    let second_id = second_response.request_id.clone();
1625    respond_second.send(second_response).unwrap();
1626
1627    cx.run_until_parked();
1628
1629    ep_store.update(cx, |ep_store, cx| {
1630        // current prediction is second
1631        assert_eq!(
1632            ep_store
1633                .prediction_at(&buffer, None, &project, cx)
1634                .unwrap()
1635                .id
1636                .0,
1637            second_id
1638        );
1639    });
1640
1641    let first_response = model_response(&request1, SIMPLE_DIFF);
1642    let first_id = first_response.request_id.clone();
1643    respond_first.send(first_response).unwrap();
1644
1645    cx.run_until_parked();
1646
1647    ep_store.update(cx, |ep_store, cx| {
1648        // current prediction is still second, since first was cancelled
1649        assert_eq!(
1650            ep_store
1651                .prediction_at(&buffer, None, &project, cx)
1652                .unwrap()
1653                .id
1654                .0,
1655            second_id
1656        );
1657    });
1658
1659    // first is reported as rejected
1660    let (reject_request, _) = requests.reject.next().await.unwrap();
1661
1662    cx.run_until_parked();
1663
1664    assert_eq!(
1665        &reject_request.rejections,
1666        &[EditPredictionRejection {
1667            request_id: first_id,
1668            reason: EditPredictionRejectReason::Canceled,
1669            was_shown: false,
1670            model_version: None,
1671            e2e_latency_ms: None,
1672        }]
1673    );
1674}
1675
1676#[gpui::test]
1677async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
1678    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1679    let fs = FakeFs::new(cx.executor());
1680    fs.insert_tree(
1681        "/root",
1682        json!({
1683            "foo.md":  "Hello!\nHow\nBye\n"
1684        }),
1685    )
1686    .await;
1687    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1688
1689    let buffer = project
1690        .update(cx, |project, cx| {
1691            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1692            project.open_buffer(path, cx)
1693        })
1694        .await
1695        .unwrap();
1696    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1697    let position = snapshot.anchor_before(language::Point::new(1, 3));
1698
1699    // start two refresh tasks
1700    ep_store.update(cx, |ep_store, cx| {
1701        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1702    });
1703
1704    let (request1, respond_first) = requests.predict.next().await.unwrap();
1705
1706    ep_store.update(cx, |ep_store, cx| {
1707        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1708    });
1709
1710    let (request2, respond_second) = requests.predict.next().await.unwrap();
1711
1712    // wait for throttle, so requests are sent
1713    cx.run_until_parked();
1714
1715    ep_store.update(cx, |ep_store, cx| {
1716        // start a third request
1717        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1718
1719        // 2 are pending, so 2nd is cancelled
1720        assert_eq!(
1721            ep_store
1722                .get_or_init_project(&project, cx)
1723                .cancelled_predictions
1724                .iter()
1725                .copied()
1726                .collect::<Vec<_>>(),
1727            [1]
1728        );
1729    });
1730
1731    // wait for throttle
1732    cx.run_until_parked();
1733
1734    let (request3, respond_third) = requests.predict.next().await.unwrap();
1735
1736    let first_response = model_response(&request1, SIMPLE_DIFF);
1737    let first_id = first_response.request_id.clone();
1738    respond_first.send(first_response).unwrap();
1739
1740    cx.run_until_parked();
1741
1742    ep_store.update(cx, |ep_store, cx| {
1743        // current prediction is first
1744        assert_eq!(
1745            ep_store
1746                .prediction_at(&buffer, None, &project, cx)
1747                .unwrap()
1748                .id
1749                .0,
1750            first_id
1751        );
1752    });
1753
1754    let cancelled_response = model_response(&request2, SIMPLE_DIFF);
1755    let cancelled_id = cancelled_response.request_id.clone();
1756    respond_second.send(cancelled_response).unwrap();
1757
1758    cx.run_until_parked();
1759
1760    ep_store.update(cx, |ep_store, cx| {
1761        // current prediction is still first, since second was cancelled
1762        assert_eq!(
1763            ep_store
1764                .prediction_at(&buffer, None, &project, cx)
1765                .unwrap()
1766                .id
1767                .0,
1768            first_id
1769        );
1770    });
1771
1772    let third_response = model_response(&request3, SIMPLE_DIFF);
1773    let third_response_id = third_response.request_id.clone();
1774    respond_third.send(third_response).unwrap();
1775
1776    cx.run_until_parked();
1777
1778    ep_store.update(cx, |ep_store, cx| {
1779        // third completes and replaces first
1780        assert_eq!(
1781            ep_store
1782                .prediction_at(&buffer, None, &project, cx)
1783                .unwrap()
1784                .id
1785                .0,
1786            third_response_id
1787        );
1788    });
1789
1790    // second is reported as rejected
1791    let (reject_request, _) = requests.reject.next().await.unwrap();
1792
1793    cx.run_until_parked();
1794
1795    assert_eq!(
1796        &reject_request.rejections,
1797        &[
1798            EditPredictionRejection {
1799                request_id: cancelled_id,
1800                reason: EditPredictionRejectReason::Canceled,
1801                was_shown: false,
1802                model_version: None,
1803                e2e_latency_ms: None,
1804            },
1805            EditPredictionRejection {
1806                request_id: first_id,
1807                reason: EditPredictionRejectReason::Replaced,
1808                was_shown: false,
1809                model_version: None,
1810                e2e_latency_ms: Some(0),
1811            }
1812        ]
1813    );
1814}
1815
1816#[gpui::test]
1817async fn test_jump_and_edit_throttles_are_independent(cx: &mut TestAppContext) {
1818    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1819
1820    let fs = FakeFs::new(cx.executor());
1821    fs.insert_tree(
1822        "/root",
1823        json!({
1824            "foo.md":  "Hello!\nHow\nBye\n",
1825            "bar.md": "Hola!\nComo\nAdios\n"
1826        }),
1827    )
1828    .await;
1829    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1830
1831    let buffer = project
1832        .update(cx, |project, cx| {
1833            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1834            project.set_active_path(Some(path.clone()), cx);
1835            project.open_buffer(path, cx)
1836        })
1837        .await
1838        .unwrap();
1839    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1840    let position = snapshot.anchor_before(language::Point::new(1, 3));
1841
1842    ep_store.update(cx, |ep_store, cx| {
1843        ep_store.register_project(&project, cx);
1844        ep_store.register_buffer(&buffer, &project, cx);
1845    });
1846
1847    // First edit request - no prior edit, so not throttled.
1848    ep_store.update(cx, |ep_store, cx| {
1849        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1850    });
1851    let (_edit_request, edit_response_tx) = requests.predict.next().await.unwrap();
1852    edit_response_tx.send(empty_response()).unwrap();
1853    cx.run_until_parked();
1854
1855    let diagnostic = lsp::Diagnostic {
1856        range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
1857        severity: Some(lsp::DiagnosticSeverity::ERROR),
1858        message: "Sentence is incomplete".to_string(),
1859        ..Default::default()
1860    };
1861
1862    // First jump request triggered by diagnostic event on buffer - no prior jump, so not throttled (independent from edit).
1863    project.update(cx, |project, cx| {
1864        project.lsp_store().update(cx, |lsp_store, cx| {
1865            lsp_store
1866                .update_diagnostics(
1867                    LanguageServerId(0),
1868                    lsp::PublishDiagnosticsParams {
1869                        uri: lsp::Uri::from_file_path(path!("/root/bar.md")).unwrap(),
1870                        diagnostics: vec![diagnostic],
1871                        version: None,
1872                    },
1873                    None,
1874                    language::DiagnosticSourceKind::Pushed,
1875                    &[],
1876                    cx,
1877                )
1878                .unwrap();
1879        });
1880    });
1881    let (_jump_request, jump_response_tx) = requests.predict.next().await.unwrap();
1882    jump_response_tx.send(empty_response()).unwrap();
1883    cx.run_until_parked();
1884
1885    // Second edit request - should be throttled by the first edit.
1886    ep_store.update(cx, |ep_store, cx| {
1887        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1888    });
1889    assert_no_predict_request_ready(&mut requests.predict);
1890
1891    // Second jump request - should be throttled by the first jump.
1892    ep_store.update(cx, |ep_store, cx| {
1893        ep_store.refresh_prediction_from_diagnostics(
1894            project.clone(),
1895            DiagnosticSearchScope::Global,
1896            cx,
1897        );
1898    });
1899    assert_no_predict_request_ready(&mut requests.predict);
1900
1901    // Wait for both throttles to expire.
1902    cx.background_executor
1903        .advance_clock(EditPredictionStore::THROTTLE_TIMEOUT);
1904    cx.background_executor.run_until_parked();
1905    cx.run_until_parked();
1906
1907    // Both requests should now go through.
1908    let (_request_1, response_tx_1) = requests.predict.next().await.unwrap();
1909    response_tx_1.send(empty_response()).unwrap();
1910    cx.run_until_parked();
1911
1912    let (_request_2, response_tx_2) = requests.predict.next().await.unwrap();
1913    response_tx_2.send(empty_response()).unwrap();
1914    cx.run_until_parked();
1915}
1916
1917#[gpui::test]
1918async fn test_same_frame_duplicate_requests_deduplicated(cx: &mut TestAppContext) {
1919    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1920    let fs = FakeFs::new(cx.executor());
1921    fs.insert_tree(
1922        "/root",
1923        json!({
1924            "foo.md":  "Hello!\nHow\nBye\n"
1925        }),
1926    )
1927    .await;
1928    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1929
1930    let buffer = project
1931        .update(cx, |project, cx| {
1932            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1933            project.open_buffer(path, cx)
1934        })
1935        .await
1936        .unwrap();
1937    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1938    let position = snapshot.anchor_before(language::Point::new(1, 3));
1939
1940    // Enqueue two refresh calls in the same synchronous frame (no yielding).
1941    // Both `cx.spawn` tasks are created before either executes, so they both
1942    // capture the same `proceed_count_at_enqueue`. Only the first task should
1943    // pass the deduplication gate; the second should be skipped.
1944    ep_store.update(cx, |ep_store, cx| {
1945        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1946        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1947    });
1948
1949    // Let both spawned tasks run to completion (including any throttle waits).
1950    cx.run_until_parked();
1951
1952    // Exactly one prediction request should have been sent.
1953    let (request, respond_tx) = requests.predict.next().await.unwrap();
1954    respond_tx
1955        .send(model_response(&request, SIMPLE_DIFF))
1956        .unwrap();
1957    cx.run_until_parked();
1958
1959    // No second request should be pending.
1960    assert_no_predict_request_ready(&mut requests.predict);
1961}
1962
1963#[gpui::test]
1964async fn test_rejections_flushing(cx: &mut TestAppContext) {
1965    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1966
1967    ep_store.update(cx, |ep_store, cx| {
1968        ep_store.reject_prediction(
1969            EditPredictionId("test-1".into()),
1970            EditPredictionRejectReason::Discarded,
1971            false,
1972            None,
1973            None,
1974            cx,
1975        );
1976        ep_store.reject_prediction(
1977            EditPredictionId("test-2".into()),
1978            EditPredictionRejectReason::Canceled,
1979            true,
1980            None,
1981            None,
1982            cx,
1983        );
1984    });
1985
1986    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
1987    cx.run_until_parked();
1988
1989    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
1990    respond_tx.send(()).unwrap();
1991
1992    // batched
1993    assert_eq!(reject_request.rejections.len(), 2);
1994    assert_eq!(
1995        reject_request.rejections[0],
1996        EditPredictionRejection {
1997            request_id: "test-1".to_string(),
1998            reason: EditPredictionRejectReason::Discarded,
1999            was_shown: false,
2000            model_version: None,
2001            e2e_latency_ms: None
2002        }
2003    );
2004    assert_eq!(
2005        reject_request.rejections[1],
2006        EditPredictionRejection {
2007            request_id: "test-2".to_string(),
2008            reason: EditPredictionRejectReason::Canceled,
2009            was_shown: true,
2010            model_version: None,
2011            e2e_latency_ms: None
2012        }
2013    );
2014
2015    // Reaching batch size limit sends without debounce
2016    ep_store.update(cx, |ep_store, cx| {
2017        for i in 0..70 {
2018            ep_store.reject_prediction(
2019                EditPredictionId(format!("batch-{}", i).into()),
2020                EditPredictionRejectReason::Discarded,
2021                false,
2022                None,
2023                None,
2024                cx,
2025            );
2026        }
2027    });
2028
2029    // First MAX/2 items are sent immediately
2030    cx.run_until_parked();
2031    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2032    respond_tx.send(()).unwrap();
2033
2034    assert_eq!(reject_request.rejections.len(), 50);
2035    assert_eq!(reject_request.rejections[0].request_id, "batch-0");
2036    assert_eq!(reject_request.rejections[49].request_id, "batch-49");
2037
2038    // Remaining items are debounced with the next batch
2039    cx.executor().advance_clock(Duration::from_secs(15));
2040    cx.run_until_parked();
2041
2042    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2043    respond_tx.send(()).unwrap();
2044
2045    assert_eq!(reject_request.rejections.len(), 20);
2046    assert_eq!(reject_request.rejections[0].request_id, "batch-50");
2047    assert_eq!(reject_request.rejections[19].request_id, "batch-69");
2048
2049    // Request failure
2050    ep_store.update(cx, |ep_store, cx| {
2051        ep_store.reject_prediction(
2052            EditPredictionId("retry-1".into()),
2053            EditPredictionRejectReason::Discarded,
2054            false,
2055            None,
2056            None,
2057            cx,
2058        );
2059    });
2060
2061    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
2062    cx.run_until_parked();
2063
2064    let (reject_request, _respond_tx) = requests.reject.next().await.unwrap();
2065    assert_eq!(reject_request.rejections.len(), 1);
2066    assert_eq!(reject_request.rejections[0].request_id, "retry-1");
2067    // Simulate failure
2068    drop(_respond_tx);
2069
2070    // Add another rejection
2071    ep_store.update(cx, |ep_store, cx| {
2072        ep_store.reject_prediction(
2073            EditPredictionId("retry-2".into()),
2074            EditPredictionRejectReason::Discarded,
2075            false,
2076            None,
2077            None,
2078            cx,
2079        );
2080    });
2081
2082    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
2083    cx.run_until_parked();
2084
2085    // Retry should include both the failed item and the new one
2086    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2087    respond_tx.send(()).unwrap();
2088
2089    assert_eq!(reject_request.rejections.len(), 2);
2090    assert_eq!(reject_request.rejections[0].request_id, "retry-1");
2091    assert_eq!(reject_request.rejections[1].request_id, "retry-2");
2092}
2093
2094#[gpui::test]
2095fn test_active_buffer_diagnostics_fetching(cx: &mut TestAppContext) {
2096    let diagnostic_marker: TextRangeMarker = ('«', '»').into();
2097    let search_range_marker: TextRangeMarker = ('[', ']').into();
2098
2099    let (text, mut ranges) = marked_text_ranges_by(
2100        indoc! {r#"
2101            fn alpha() {
2102                let «first_value» = 1;
2103            }
2104
2105            [fn beta() {
2106                let «second_value» = 2;
2107                let third_value = second_value + missing_symbol;
2108            }ˇ]
2109
2110            fn gamma() {
2111                let «fourth_value» = missing_other_symbol;
2112            }
2113        "#},
2114        vec![diagnostic_marker.clone(), search_range_marker.clone()],
2115    );
2116
2117    let diagnostic_ranges = ranges.remove(&diagnostic_marker).unwrap_or_default();
2118    let search_ranges = ranges.remove(&search_range_marker).unwrap_or_default();
2119
2120    let buffer = cx.new(|cx| Buffer::local(&text, cx));
2121
2122    buffer.update(cx, |buffer, cx| {
2123        let snapshot = buffer.snapshot();
2124        let diagnostics = DiagnosticSet::new(
2125            diagnostic_ranges
2126                .iter()
2127                .enumerate()
2128                .map(|(index, range)| DiagnosticEntry {
2129                    range: snapshot.offset_to_point_utf16(range.start)
2130                        ..snapshot.offset_to_point_utf16(range.end),
2131                    diagnostic: Diagnostic {
2132                        severity: match index {
2133                            0 => DiagnosticSeverity::WARNING,
2134                            1 => DiagnosticSeverity::ERROR,
2135                            _ => DiagnosticSeverity::HINT,
2136                        },
2137                        message: match index {
2138                            0 => "first warning".to_string(),
2139                            1 => "second error".to_string(),
2140                            _ => "third hint".to_string(),
2141                        },
2142                        group_id: index + 1,
2143                        is_primary: true,
2144                        source_kind: language::DiagnosticSourceKind::Pushed,
2145                        ..Diagnostic::default()
2146                    },
2147                }),
2148            &snapshot,
2149        );
2150        buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
2151    });
2152
2153    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2154    let search_range = snapshot.offset_to_point(search_ranges[0].start)
2155        ..snapshot.offset_to_point(search_ranges[0].end);
2156
2157    let active_buffer_diagnostics = zeta::active_buffer_diagnostics(&snapshot, search_range, 100);
2158
2159    assert_eq!(
2160        active_buffer_diagnostics,
2161        vec![zeta_prompt::ActiveBufferDiagnostic {
2162            severity: Some(1),
2163            message: "second error".to_string(),
2164            snippet: text,
2165            snippet_buffer_row_range: 5..5,
2166            diagnostic_range_in_snippet: 61..73,
2167        }]
2168    );
2169
2170    let buffer = cx.new(|cx| {
2171        Buffer::local(
2172            indoc! {"
2173                one
2174                two
2175                three
2176                four
2177                five
2178            "},
2179            cx,
2180        )
2181    });
2182
2183    buffer.update(cx, |buffer, cx| {
2184        let snapshot = buffer.snapshot();
2185        let diagnostics = DiagnosticSet::new(
2186            vec![
2187                DiagnosticEntry {
2188                    range: text::PointUtf16::new(0, 0)..text::PointUtf16::new(0, 3),
2189                    diagnostic: Diagnostic {
2190                        severity: DiagnosticSeverity::ERROR,
2191                        message: "row zero".to_string(),
2192                        group_id: 1,
2193                        is_primary: true,
2194                        source_kind: language::DiagnosticSourceKind::Pushed,
2195                        ..Diagnostic::default()
2196                    },
2197                },
2198                DiagnosticEntry {
2199                    range: text::PointUtf16::new(2, 0)..text::PointUtf16::new(2, 5),
2200                    diagnostic: Diagnostic {
2201                        severity: DiagnosticSeverity::WARNING,
2202                        message: "row two".to_string(),
2203                        group_id: 2,
2204                        is_primary: true,
2205                        source_kind: language::DiagnosticSourceKind::Pushed,
2206                        ..Diagnostic::default()
2207                    },
2208                },
2209                DiagnosticEntry {
2210                    range: text::PointUtf16::new(4, 0)..text::PointUtf16::new(4, 4),
2211                    diagnostic: Diagnostic {
2212                        severity: DiagnosticSeverity::INFORMATION,
2213                        message: "row four".to_string(),
2214                        group_id: 3,
2215                        is_primary: true,
2216                        source_kind: language::DiagnosticSourceKind::Pushed,
2217                        ..Diagnostic::default()
2218                    },
2219                },
2220            ],
2221            &snapshot,
2222        );
2223        buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
2224    });
2225
2226    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2227
2228    let active_buffer_diagnostics =
2229        zeta::active_buffer_diagnostics(&snapshot, Point::new(2, 0)..Point::new(4, 0), 100);
2230
2231    assert_eq!(
2232        active_buffer_diagnostics
2233            .iter()
2234            .map(|diagnostic| (
2235                diagnostic.severity,
2236                diagnostic.message.clone(),
2237                diagnostic.snippet.clone(),
2238                diagnostic.snippet_buffer_row_range.clone(),
2239                diagnostic.diagnostic_range_in_snippet.clone(),
2240            ))
2241            .collect::<Vec<_>>(),
2242        vec![
2243            (
2244                Some(2),
2245                "row two".to_string(),
2246                "one\ntwo\nthree\nfour\nfive\n".to_string(),
2247                2..2,
2248                8..13,
2249            ),
2250            (
2251                Some(3),
2252                "row four".to_string(),
2253                "one\ntwo\nthree\nfour\nfive\n".to_string(),
2254                4..4,
2255                19..23,
2256            ),
2257        ]
2258    );
2259}
2260
2261// Generate a model response that would apply the given diff to the active file.
2262fn model_response(request: &PredictEditsV3Request, diff_to_apply: &str) -> PredictEditsV3Response {
2263    let editable_range =
2264        zeta_prompt::excerpt_range_for_format(Default::default(), &request.input.excerpt_ranges).1;
2265    let excerpt = request.input.cursor_excerpt[editable_range.clone()].to_string();
2266    let new_excerpt = apply_diff_to_string(diff_to_apply, &excerpt).unwrap();
2267
2268    PredictEditsV3Response {
2269        request_id: Uuid::new_v4().to_string(),
2270        editable_range,
2271        output: new_excerpt,
2272        model_version: None,
2273    }
2274}
2275
2276fn empty_response() -> PredictEditsV3Response {
2277    PredictEditsV3Response {
2278        request_id: Uuid::new_v4().to_string(),
2279        editable_range: 0..0,
2280        output: String::new(),
2281        model_version: None,
2282    }
2283}
2284
2285fn prompt_from_request(request: &PredictEditsV3Request) -> String {
2286    zeta_prompt::format_zeta_prompt(&request.input, zeta_prompt::ZetaFormat::default())
2287        .expect("default zeta prompt formatting should succeed in edit prediction tests")
2288}
2289
2290fn assert_no_predict_request_ready(
2291    requests: &mut mpsc::UnboundedReceiver<(
2292        PredictEditsV3Request,
2293        oneshot::Sender<PredictEditsV3Response>,
2294    )>,
2295) {
2296    if requests.next().now_or_never().flatten().is_some() {
2297        panic!("Unexpected prediction request while throttled.");
2298    }
2299}
2300
2301struct RequestChannels {
2302    predict: mpsc::UnboundedReceiver<(
2303        PredictEditsV3Request,
2304        oneshot::Sender<PredictEditsV3Response>,
2305    )>,
2306    reject: mpsc::UnboundedReceiver<(RejectEditPredictionsBody, oneshot::Sender<()>)>,
2307}
2308
2309fn init_test_with_fake_client(
2310    cx: &mut TestAppContext,
2311) -> (Entity<EditPredictionStore>, RequestChannels) {
2312    cx.update(move |cx| {
2313        let settings_store = SettingsStore::test(cx);
2314        cx.set_global(settings_store);
2315        zlog::init_test();
2316
2317        let (predict_req_tx, predict_req_rx) = mpsc::unbounded();
2318        let (reject_req_tx, reject_req_rx) = mpsc::unbounded();
2319
2320        let http_client = FakeHttpClient::create({
2321            move |req| {
2322                let uri = req.uri().path().to_string();
2323                let mut body = req.into_body();
2324                let predict_req_tx = predict_req_tx.clone();
2325                let reject_req_tx = reject_req_tx.clone();
2326                async move {
2327                    let resp = match uri.as_str() {
2328                        "/client/llm_tokens" => serde_json::to_string(&json!({
2329                            "token": "test"
2330                        }))
2331                        .unwrap(),
2332                        "/predict_edits/v3" => {
2333                            let mut buf = Vec::new();
2334                            body.read_to_end(&mut buf).await.ok();
2335                            let decompressed = zstd::decode_all(&buf[..]).unwrap();
2336                            let req = serde_json::from_slice(&decompressed).unwrap();
2337
2338                            let (res_tx, res_rx) = oneshot::channel();
2339                            predict_req_tx.unbounded_send((req, res_tx)).unwrap();
2340                            serde_json::to_string(&res_rx.await?).unwrap()
2341                        }
2342                        "/predict_edits/reject" => {
2343                            let mut buf = Vec::new();
2344                            body.read_to_end(&mut buf).await.ok();
2345                            let req = serde_json::from_slice(&buf).unwrap();
2346
2347                            let (res_tx, res_rx) = oneshot::channel();
2348                            reject_req_tx.unbounded_send((req, res_tx)).unwrap();
2349                            serde_json::to_string(&res_rx.await?).unwrap()
2350                        }
2351                        _ => {
2352                            panic!("Unexpected path: {}", uri)
2353                        }
2354                    };
2355
2356                    Ok(Response::builder().body(resp.into()).unwrap())
2357                }
2358            }
2359        });
2360
2361        let client = client::Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
2362        client.cloud_client().set_credentials(1, "test".into());
2363
2364        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2365        language_model::init(user_store.clone(), client.clone(), cx);
2366        let ep_store = EditPredictionStore::global(&client, &user_store, cx);
2367
2368        (
2369            ep_store,
2370            RequestChannels {
2371                predict: predict_req_rx,
2372                reject: reject_req_rx,
2373            },
2374        )
2375    })
2376}
2377
2378#[gpui::test]
2379async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
2380    let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx));
2381    let edits: Arc<[(Range<Anchor>, Arc<str>)]> = cx.update(|cx| {
2382        to_completion_edits([(2..5, "REM".into()), (9..11, "".into())], &buffer, cx).into()
2383    });
2384
2385    let edit_preview = cx
2386        .read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
2387        .await;
2388
2389    let prediction = EditPrediction {
2390        edits,
2391        cursor_position: None,
2392        edit_preview,
2393        buffer: buffer.clone(),
2394        snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
2395        id: EditPredictionId("the-id".into()),
2396        inputs: ZetaPromptInput {
2397            events: Default::default(),
2398            related_files: Default::default(),
2399            active_buffer_diagnostics: vec![],
2400            cursor_path: Path::new("").into(),
2401            cursor_excerpt: "".into(),
2402            cursor_offset_in_excerpt: 0,
2403            excerpt_start_row: None,
2404            excerpt_ranges: Default::default(),
2405            syntax_ranges: None,
2406            experiment: None,
2407            in_open_source_repo: false,
2408            can_collect_data: false,
2409            repo_url: None,
2410        },
2411        model_version: None,
2412    };
2413
2414    cx.update(|cx| {
2415        assert_eq!(
2416            from_completion_edits(
2417                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2418                &buffer,
2419                cx
2420            ),
2421            vec![(2..5, "REM".into()), (9..11, "".into())]
2422        );
2423
2424        buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
2425        assert_eq!(
2426            from_completion_edits(
2427                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2428                &buffer,
2429                cx
2430            ),
2431            vec![(2..2, "REM".into()), (6..8, "".into())]
2432        );
2433
2434        buffer.update(cx, |buffer, cx| buffer.undo(cx));
2435        assert_eq!(
2436            from_completion_edits(
2437                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2438                &buffer,
2439                cx
2440            ),
2441            vec![(2..5, "REM".into()), (9..11, "".into())]
2442        );
2443
2444        buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
2445        assert_eq!(
2446            from_completion_edits(
2447                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2448                &buffer,
2449                cx
2450            ),
2451            vec![(3..3, "EM".into()), (7..9, "".into())]
2452        );
2453
2454        buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
2455        assert_eq!(
2456            from_completion_edits(
2457                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2458                &buffer,
2459                cx
2460            ),
2461            vec![(4..4, "M".into()), (8..10, "".into())]
2462        );
2463
2464        buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
2465        assert_eq!(
2466            from_completion_edits(
2467                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2468                &buffer,
2469                cx
2470            ),
2471            vec![(9..11, "".into())]
2472        );
2473
2474        buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
2475        assert_eq!(
2476            from_completion_edits(
2477                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2478                &buffer,
2479                cx
2480            ),
2481            vec![(4..4, "M".into()), (8..10, "".into())]
2482        );
2483
2484        buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
2485        assert_eq!(
2486            from_completion_edits(
2487                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2488                &buffer,
2489                cx
2490            ),
2491            vec![(4..4, "M".into())]
2492        );
2493
2494        buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
2495        assert_eq!(prediction.interpolate(&buffer.read(cx).snapshot()), None);
2496    })
2497}
2498
2499#[gpui::test]
2500async fn test_clean_up_diff(cx: &mut TestAppContext) {
2501    init_test(cx);
2502
2503    assert_eq!(
2504        apply_edit_prediction(
2505            indoc! {"
2506                    fn main() {
2507                        let word_1 = \"lorem\";
2508                        let range = word.len()..word.len();
2509                    }
2510                "},
2511            indoc! {"
2512                    fn main() {
2513                        let word_1 = \"lorem\";
2514                        let range = word_1.len()..word_1.len();
2515                    }
2516                "},
2517            cx,
2518        )
2519        .await,
2520        indoc! {"
2521                fn main() {
2522                    let word_1 = \"lorem\";
2523                    let range = word_1.len()..word_1.len();
2524                }
2525            "},
2526    );
2527
2528    assert_eq!(
2529        apply_edit_prediction(
2530            indoc! {"
2531                    fn main() {
2532                        let story = \"the quick\"
2533                    }
2534                "},
2535            indoc! {"
2536                    fn main() {
2537                        let story = \"the quick brown fox jumps over the lazy dog\";
2538                    }
2539                "},
2540            cx,
2541        )
2542        .await,
2543        indoc! {"
2544                fn main() {
2545                    let story = \"the quick brown fox jumps over the lazy dog\";
2546                }
2547            "},
2548    );
2549}
2550
2551#[gpui::test]
2552async fn test_edit_prediction_end_of_buffer(cx: &mut TestAppContext) {
2553    init_test(cx);
2554
2555    let buffer_content = "lorem\n";
2556    let completion_response = "lorem\nipsum\n";
2557
2558    assert_eq!(
2559        apply_edit_prediction(buffer_content, completion_response, cx).await,
2560        "lorem\nipsum\n"
2561    );
2562}
2563
2564#[gpui::test]
2565async fn test_edit_prediction_no_spurious_trailing_newline(cx: &mut TestAppContext) {
2566    // Test that zeta2's newline normalization logic doesn't insert spurious newlines.
2567    // When the buffer ends without a trailing newline, but the model returns output
2568    // with a trailing newline, zeta2 should normalize both sides before diffing
2569    // so no spurious newline is inserted.
2570    let (ep_store, mut requests) = init_test_with_fake_client(cx);
2571    let fs = FakeFs::new(cx.executor());
2572
2573    // Single line buffer with no trailing newline
2574    fs.insert_tree(
2575        "/root",
2576        json!({
2577            "foo.txt": "hello"
2578        }),
2579    )
2580    .await;
2581    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
2582
2583    let buffer = project
2584        .update(cx, |project, cx| {
2585            let path = project
2586                .find_project_path(path!("root/foo.txt"), cx)
2587                .unwrap();
2588            project.open_buffer(path, cx)
2589        })
2590        .await
2591        .unwrap();
2592
2593    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2594    let position = snapshot.anchor_before(language::Point::new(0, 5));
2595
2596    ep_store.update(cx, |ep_store, cx| {
2597        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
2598    });
2599
2600    let (request, respond_tx) = requests.predict.next().await.unwrap();
2601
2602    // Model returns output WITH a trailing newline, even though the buffer doesn't have one.
2603    // Zeta2 should normalize both sides before diffing, so no spurious newline is inserted.
2604    let excerpt_length = request.input.cursor_excerpt.len();
2605    let response = PredictEditsV3Response {
2606        request_id: Uuid::new_v4().to_string(),
2607        output: "hello world\n".to_string(),
2608        editable_range: 0..excerpt_length,
2609        model_version: None,
2610    };
2611    respond_tx.send(response).unwrap();
2612
2613    cx.run_until_parked();
2614
2615    // The prediction should insert " world" without adding a newline
2616    ep_store.update(cx, |ep_store, cx| {
2617        let prediction = ep_store
2618            .prediction_at(&buffer, None, &project, cx)
2619            .expect("should have prediction");
2620        let edits: Vec<_> = prediction
2621            .edits
2622            .iter()
2623            .map(|(range, text)| {
2624                let snapshot = buffer.read(cx).snapshot();
2625                (range.to_offset(&snapshot), text.clone())
2626            })
2627            .collect();
2628        assert_eq!(edits, vec![(5..5, " world".into())]);
2629    });
2630}
2631
2632fn init_test(cx: &mut TestAppContext) {
2633    cx.update(|cx| {
2634        let settings_store = SettingsStore::test(cx);
2635        cx.set_global(settings_store);
2636    });
2637}
2638
2639async fn apply_edit_prediction(
2640    buffer_content: &str,
2641    completion_response: &str,
2642    cx: &mut TestAppContext,
2643) -> String {
2644    let fs = project::FakeFs::new(cx.executor());
2645    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2646    let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
2647    let (ep_store, response) = make_test_ep_store(&project, cx).await;
2648    *response.lock() = completion_response.to_string();
2649    let edit_prediction = run_edit_prediction(&buffer, &project, &ep_store, cx).await;
2650    buffer.update(cx, |buffer, cx| {
2651        buffer.edit(edit_prediction.edits.iter().cloned(), None, cx)
2652    });
2653    buffer.read_with(cx, |buffer, _| buffer.text())
2654}
2655
2656async fn run_edit_prediction(
2657    buffer: &Entity<Buffer>,
2658    project: &Entity<Project>,
2659    ep_store: &Entity<EditPredictionStore>,
2660    cx: &mut TestAppContext,
2661) -> EditPrediction {
2662    let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
2663    ep_store.update(cx, |ep_store, cx| {
2664        ep_store.register_buffer(buffer, &project, cx)
2665    });
2666    cx.background_executor.run_until_parked();
2667    let prediction_task = ep_store.update(cx, |ep_store, cx| {
2668        ep_store.request_prediction(&project, buffer, cursor, Default::default(), cx)
2669    });
2670    prediction_task.await.unwrap().unwrap().prediction.unwrap()
2671}
2672
2673async fn make_test_ep_store(
2674    project: &Entity<Project>,
2675    cx: &mut TestAppContext,
2676) -> (Entity<EditPredictionStore>, Arc<Mutex<String>>) {
2677    let default_response = "hello world\n".to_string();
2678    let completion_response: Arc<Mutex<String>> = Arc::new(Mutex::new(default_response));
2679    let http_client = FakeHttpClient::create({
2680        let completion_response = completion_response.clone();
2681        let mut next_request_id = 0;
2682        move |req| {
2683            let completion_response = completion_response.clone();
2684            let method = req.method().clone();
2685            let uri = req.uri().path().to_string();
2686            let mut body = req.into_body();
2687            async move {
2688                match (method, uri.as_str()) {
2689                    (Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder()
2690                        .status(200)
2691                        .body(
2692                            serde_json::to_string(&CreateLlmTokenResponse {
2693                                token: LlmToken("the-llm-token".to_string()),
2694                            })
2695                            .unwrap()
2696                            .into(),
2697                        )
2698                        .unwrap()),
2699                    (Method::POST, "/predict_edits/v3") => {
2700                        let mut buf = Vec::new();
2701                        body.read_to_end(&mut buf).await.ok();
2702                        let decompressed = zstd::decode_all(&buf[..]).unwrap();
2703                        let req: PredictEditsV3Request =
2704                            serde_json::from_slice(&decompressed).unwrap();
2705
2706                        next_request_id += 1;
2707                        Ok(http_client::Response::builder()
2708                            .status(200)
2709                            .body(
2710                                serde_json::to_string(&PredictEditsV3Response {
2711                                    request_id: format!("request-{next_request_id}"),
2712                                    editable_range: 0..req.input.cursor_excerpt.len(),
2713                                    output: completion_response.lock().clone(),
2714                                    model_version: None,
2715                                })
2716                                .unwrap()
2717                                .into(),
2718                            )
2719                            .unwrap())
2720                    }
2721                    _ => Ok(http_client::Response::builder()
2722                        .status(404)
2723                        .body("Not Found".to_string().into())
2724                        .unwrap()),
2725                }
2726            }
2727        }
2728    });
2729
2730    let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
2731    let user_store = cx.update(|cx| cx.new(|cx| client::UserStore::new(client.clone(), cx)));
2732    cx.update(|cx| {
2733        RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
2734    });
2735    let _server = FakeServer::for_client(42, &client, cx).await;
2736
2737    let ep_store = cx.new(|cx| {
2738        let mut ep_store = EditPredictionStore::new(client, project.read(cx).user_store(), cx);
2739        ep_store.set_edit_prediction_model(EditPredictionModel::Zeta);
2740
2741        let worktrees = project.read(cx).worktrees(cx).collect::<Vec<_>>();
2742        for worktree in worktrees {
2743            let worktree_id = worktree.read(cx).id();
2744            ep_store
2745                .get_or_init_project(project, cx)
2746                .license_detection_watchers
2747                .entry(worktree_id)
2748                .or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(&worktree, cx)));
2749        }
2750
2751        ep_store
2752    });
2753
2754    (ep_store, completion_response)
2755}
2756
2757fn to_completion_edits(
2758    iterator: impl IntoIterator<Item = (Range<usize>, Arc<str>)>,
2759    buffer: &Entity<Buffer>,
2760    cx: &App,
2761) -> Vec<(Range<Anchor>, Arc<str>)> {
2762    let buffer = buffer.read(cx);
2763    iterator
2764        .into_iter()
2765        .map(|(range, text)| {
2766            (
2767                buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
2768                text,
2769            )
2770        })
2771        .collect()
2772}
2773
2774fn from_completion_edits(
2775    editor_edits: &[(Range<Anchor>, Arc<str>)],
2776    buffer: &Entity<Buffer>,
2777    cx: &App,
2778) -> Vec<(Range<usize>, Arc<str>)> {
2779    let buffer = buffer.read(cx);
2780    editor_edits
2781        .iter()
2782        .map(|(range, text)| {
2783            (
2784                range.start.to_offset(buffer)..range.end.to_offset(buffer),
2785                text.clone(),
2786            )
2787        })
2788        .collect()
2789}
2790
2791#[gpui::test]
2792async fn test_unauthenticated_without_custom_url_blocks_prediction_impl(cx: &mut TestAppContext) {
2793    init_test(cx);
2794
2795    let fs = FakeFs::new(cx.executor());
2796    fs.insert_tree(
2797        "/project",
2798        serde_json::json!({
2799            "main.rs": "fn main() {\n    \n}\n"
2800        }),
2801    )
2802    .await;
2803
2804    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2805
2806    let http_client = FakeHttpClient::create(|_req| async move {
2807        Ok(gpui::http_client::Response::builder()
2808            .status(401)
2809            .body("Unauthorized".into())
2810            .unwrap())
2811    });
2812
2813    let client =
2814        cx.update(|cx| client::Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
2815    let user_store = cx.update(|cx| cx.new(|cx| client::UserStore::new(client.clone(), cx)));
2816    cx.update(|cx| {
2817        language_model::RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
2818    });
2819
2820    let ep_store = cx.new(|cx| EditPredictionStore::new(client, project.read(cx).user_store(), cx));
2821
2822    let buffer = project
2823        .update(cx, |project, cx| {
2824            let path = project
2825                .find_project_path(path!("/project/main.rs"), cx)
2826                .unwrap();
2827            project.open_buffer(path, cx)
2828        })
2829        .await
2830        .unwrap();
2831
2832    let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 4)));
2833    ep_store.update(cx, |ep_store, cx| {
2834        ep_store.register_buffer(&buffer, &project, cx)
2835    });
2836    cx.background_executor.run_until_parked();
2837
2838    let completion_task = ep_store.update(cx, |ep_store, cx| {
2839        ep_store.set_edit_prediction_model(EditPredictionModel::Zeta);
2840        ep_store.request_prediction(&project, &buffer, cursor, Default::default(), cx)
2841    });
2842
2843    let result = completion_task.await;
2844    assert!(
2845        result.is_err(),
2846        "Without authentication and without custom URL, prediction should fail"
2847    );
2848}
2849
2850#[gpui::test]
2851async fn test_diagnostic_jump_excludes_collaborator_regions(cx: &mut TestAppContext) {
2852    fn set_collaborator_cursor(buffer: &Entity<Buffer>, row: u32, cx: &mut TestAppContext) {
2853        let collab_replica = clock::ReplicaId::new(10);
2854        let anchor = buffer.read_with(cx, |buffer, _| {
2855            buffer.snapshot().anchor_before(Point::new(row, 0))
2856        });
2857        let selections: Arc<[Selection<Anchor>]> = Arc::new([Selection {
2858            id: 1,
2859            start: anchor,
2860            end: anchor,
2861            reversed: false,
2862            goal: SelectionGoal::None,
2863        }]);
2864        buffer.update(cx, |buffer, cx| {
2865            buffer.apply_ops(
2866                [Operation::UpdateSelections {
2867                    selections,
2868                    lamport_timestamp: clock::Lamport {
2869                        replica_id: collab_replica,
2870                        value: 1,
2871                    },
2872                    line_mode: false,
2873                    cursor_shape: CursorShape::Bar,
2874                }],
2875                cx,
2876            );
2877        });
2878    }
2879
2880    fn publish_diagnostics(
2881        uri_path: &'static str,
2882        rows: &[u32],
2883        project: &Entity<Project>,
2884        cx: &mut TestAppContext,
2885    ) {
2886        let diagnostics: Vec<_> = rows
2887            .iter()
2888            .map(|&row| lsp::Diagnostic {
2889                range: lsp::Range::new(lsp::Position::new(row, 0), lsp::Position::new(row, 5)),
2890                severity: Some(lsp::DiagnosticSeverity::ERROR),
2891                message: format!("error at row {row}"),
2892                ..Default::default()
2893            })
2894            .collect();
2895        project.update(cx, |project, cx| {
2896            project.lsp_store().update(cx, |lsp_store, cx| {
2897                lsp_store
2898                    .update_diagnostics(
2899                        LanguageServerId(0),
2900                        lsp::PublishDiagnosticsParams {
2901                            uri: lsp::Uri::from_file_path(uri_path).expect("invalid uri"),
2902                            diagnostics,
2903                            version: None,
2904                        },
2905                        None,
2906                        language::DiagnosticSourceKind::Pushed,
2907                        &[],
2908                        cx,
2909                    )
2910                    .expect("failed to update diagnostics");
2911            });
2912        });
2913    }
2914
2915    init_test(cx);
2916
2917    let mut lines = String::new();
2918    for i in 0..60 {
2919        lines.push_str(&format!("line {i}\n"));
2920    }
2921
2922    let fs = FakeFs::new(cx.executor());
2923    fs.insert_tree(
2924        "/root",
2925        json!({
2926            "active.txt": lines,
2927            "collab_file.txt": "error here\nsecond line\n",
2928            "free_file.txt": "another error\nsecond line\n",
2929        }),
2930    )
2931    .await;
2932    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
2933
2934    let active_buffer = project
2935        .update(cx, |project, cx| {
2936            let path = project
2937                .find_project_path(path!("/root/active.txt"), cx)
2938                .expect("active.txt not found");
2939            project.set_active_path(Some(path.clone()), cx);
2940            project.open_buffer(path, cx)
2941        })
2942        .await
2943        .expect("failed to open active buffer");
2944
2945    set_collaborator_cursor(&active_buffer, 5, cx);
2946
2947    publish_diagnostics(path!("/root/active.txt"), &[3, 25, 50], &project, cx);
2948
2949    cx.run_until_parked();
2950
2951    let cursor_point = Point::new(25, 0);
2952    let empty_search_range: Range<Point> = Default::default();
2953
2954    let snapshot = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
2955    let result = EditPredictionStore::next_diagnostic_location(
2956        active_buffer.clone(),
2957        &snapshot,
2958        empty_search_range.clone(),
2959        cursor_point,
2960        &project,
2961        &mut cx.to_async(),
2962    )
2963    .await
2964    .expect("next_diagnostic_location failed");
2965
2966    let (result_buffer, result_anchor) = result.expect("expected a diagnostic location");
2967    assert_eq!(result_buffer.entity_id(), active_buffer.entity_id());
2968    let result_row = result_buffer.read_with(cx, |buffer, _| {
2969        result_anchor.to_point(&buffer.snapshot()).row
2970    });
2971    assert_ne!(
2972        result_row, 3,
2973        "row 3 is near collaborator (row 5) but far from local cursor (row 25), should be excluded"
2974    );
2975    assert!(
2976        result_row == 25 || result_row == 50,
2977        "expected row 25 or 50, got {result_row}"
2978    );
2979
2980    let snapshot_near = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
2981    let near_cursor_point = Point::new(4, 0);
2982    let result_near = EditPredictionStore::next_diagnostic_location(
2983        active_buffer.clone(),
2984        &snapshot_near,
2985        empty_search_range.clone(),
2986        near_cursor_point,
2987        &project,
2988        &mut cx.to_async(),
2989    )
2990    .await
2991    .expect("next_diagnostic_location failed");
2992
2993    let (_, near_anchor) = result_near.expect("expected a diagnostic location when both are near");
2994    let near_row =
2995        active_buffer.read_with(cx, |buffer, _| near_anchor.to_point(&buffer.snapshot()).row);
2996    assert_eq!(
2997        near_row, 3,
2998        "row 3 should be included when local cursor (row 4) is also near the collaborator"
2999    );
3000
3001    let snapshot_far = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3002    let far_cursor_point = Point::new(50, 0);
3003    let result_far = EditPredictionStore::next_diagnostic_location(
3004        active_buffer.clone(),
3005        &snapshot_far,
3006        empty_search_range.clone(),
3007        far_cursor_point,
3008        &project,
3009        &mut cx.to_async(),
3010    )
3011    .await
3012    .expect("next_diagnostic_location failed");
3013
3014    let (_, far_anchor) = result_far.expect("expected a diagnostic location");
3015    let far_row =
3016        active_buffer.read_with(cx, |buffer, _| far_anchor.to_point(&buffer.snapshot()).row);
3017    assert_eq!(
3018        far_row, 50,
3019        "row 50 is near local cursor (row 50) and far from collaborator, should be picked"
3020    );
3021
3022    publish_diagnostics(path!("/root/collab_file.txt"), &[0], &project, cx);
3023    publish_diagnostics(path!("/root/free_file.txt"), &[0], &project, cx);
3024    cx.run_until_parked();
3025
3026    let collab_buffer = project
3027        .update(cx, |project, cx| {
3028            let path = project
3029                .find_project_path(path!("/root/collab_file.txt"), cx)
3030                .expect("collab_file.txt not found");
3031            project.open_buffer(path, cx)
3032        })
3033        .await
3034        .expect("failed to open collab buffer");
3035
3036    set_collaborator_cursor(&collab_buffer, 0, cx);
3037    cx.run_until_parked();
3038
3039    let no_same_file_search_range = Point::new(0, 0)..Point::new(59, 0);
3040    let snapshot_cross = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3041    let result_cross = EditPredictionStore::next_diagnostic_location(
3042        active_buffer.clone(),
3043        &snapshot_cross,
3044        no_same_file_search_range,
3045        Point::new(0, 0),
3046        &project,
3047        &mut cx.to_async(),
3048    )
3049    .await
3050    .expect("cross-file next_diagnostic_location failed");
3051
3052    let (cross_buffer, _) = result_cross.expect("expected a cross-file diagnostic location");
3053    let cross_path = cross_buffer.read_with(cx, |buffer, cx| {
3054        buffer
3055            .file()
3056            .expect("buffer should have a file")
3057            .full_path(cx)
3058    });
3059    assert_eq!(
3060        cross_path,
3061        Path::new(path!("root/free_file.txt")),
3062        "should skip collab_file.txt (has collaborator) and pick free_file.txt"
3063    );
3064}
3065
3066#[gpui::test]
3067async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
3068    let (ep_store, _requests) = init_test_with_fake_client(cx);
3069    let fs = FakeFs::new(cx.executor());
3070
3071    // Buffer with two clearly separated regions:
3072    //   Region A = lines 0-9   (offsets 0..50)
3073    //   Region B = lines 20-29 (offsets 105..155)
3074    // A big gap in between so edits in one region never overlap the other.
3075    let mut content = String::new();
3076    for i in 0..30 {
3077        content.push_str(&format!("line {i:02}\n"));
3078    }
3079
3080    fs.insert_tree(
3081        "/root",
3082        json!({
3083            "foo.md": content.clone()
3084        }),
3085    )
3086    .await;
3087    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
3088
3089    let buffer = project
3090        .update(cx, |project, cx| {
3091            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
3092            project.open_buffer(path, cx)
3093        })
3094        .await
3095        .unwrap();
3096
3097    type SettledEventRecord = (EditPredictionId, String);
3098    let settled_events: Arc<Mutex<Vec<SettledEventRecord>>> = Arc::new(Mutex::new(Vec::new()));
3099
3100    ep_store.update(cx, |ep_store, cx| {
3101        ep_store.register_buffer(&buffer, &project, cx);
3102
3103        let settled_events = settled_events.clone();
3104        ep_store.settled_event_callback = Some(Box::new(move |id, text| {
3105            settled_events.lock().push((id, text));
3106        }));
3107    });
3108
3109    // --- Phase 1: edit in region A and enqueue prediction A ---
3110
3111    buffer.update(cx, |buffer, cx| {
3112        // Edit at the start of line 0.
3113        buffer.edit(vec![(0..0, "ADDED ")], None, cx);
3114    });
3115    cx.run_until_parked();
3116
3117    let snapshot_a = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3118
3119    // Region A: first 10 lines of the buffer.
3120    let editable_region_a = 0..snapshot_a.point_to_offset(Point::new(10, 0));
3121
3122    ep_store.update(cx, |ep_store, cx| {
3123        ep_store.enqueue_settled_prediction(
3124            EditPredictionId("prediction-a".into()),
3125            &project,
3126            &buffer,
3127            &snapshot_a,
3128            editable_region_a.clone(),
3129            None,
3130            Duration::from_secs(0),
3131            cx,
3132        );
3133    });
3134
3135    // --- Phase 2: repeatedly edit in region A to keep it unsettled ---
3136
3137    // Let the worker process the channel message before we start advancing.
3138    cx.run_until_parked();
3139
3140    let mut region_a_edit_offset = 5;
3141    for _ in 0..3 {
3142        // Edit inside region A (not at the boundary) so `last_edit_at` is
3143        // updated before the worker's next wake.
3144        buffer.update(cx, |buffer, cx| {
3145            buffer.edit(
3146                vec![(region_a_edit_offset..region_a_edit_offset, "x")],
3147                None,
3148                cx,
3149            );
3150        });
3151        region_a_edit_offset += 1;
3152        cx.run_until_parked();
3153
3154        cx.executor()
3155            .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 2);
3156        cx.run_until_parked();
3157        assert!(
3158            settled_events.lock().is_empty(),
3159            "no settled events should fire while region A is still being edited"
3160        );
3161    }
3162
3163    // Still nothing settled.
3164    assert!(settled_events.lock().is_empty());
3165
3166    // --- Phase 3: edit in distinct region B, enqueue prediction B ---
3167    // Advance a small amount so B's quiescence window starts later than A's,
3168    // but not so much that A settles (A's last edit was at the start of
3169    // iteration 3, and it needs a full Q to settle).
3170    cx.executor()
3171        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 4);
3172    cx.run_until_parked();
3173    assert!(settled_events.lock().is_empty());
3174
3175    let snapshot_b = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3176    let line_20_offset = snapshot_b.point_to_offset(Point::new(20, 0));
3177
3178    buffer.update(cx, |buffer, cx| {
3179        buffer.edit(vec![(line_20_offset..line_20_offset, "NEW ")], None, cx);
3180    });
3181    cx.run_until_parked();
3182
3183    let snapshot_b2 = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3184    let editable_region_b = line_20_offset..snapshot_b2.point_to_offset(Point::new(25, 0));
3185
3186    ep_store.update(cx, |ep_store, cx| {
3187        ep_store.enqueue_settled_prediction(
3188            EditPredictionId("prediction-b".into()),
3189            &project,
3190            &buffer,
3191            &snapshot_b2,
3192            editable_region_b.clone(),
3193            None,
3194            Duration::from_secs(0),
3195            cx,
3196        );
3197    });
3198
3199    cx.run_until_parked();
3200    assert!(
3201        settled_events.lock().is_empty(),
3202        "neither prediction should have settled yet"
3203    );
3204
3205    // --- Phase 4: let enough time pass for region A to settle ---
3206    // A's last edit was at T_a (during the last loop iteration). The worker is
3207    // sleeping until T_a + Q. We advance just enough to reach that wake time
3208    // (Q/4 since we already advanced Q/4 in phase 3 on top of the loop's
3209    // 3*Q/2). At that point A has been quiet for Q and settles, but B was
3210    // enqueued only Q/4 ago and stays pending.
3211    cx.executor()
3212        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 4);
3213    cx.run_until_parked();
3214
3215    {
3216        let events = settled_events.lock().clone();
3217        assert_eq!(
3218            events.len(),
3219            1,
3220            "prediction and capture_sample for A should have settled, got: {events:?}"
3221        );
3222        assert_eq!(events[0].0, EditPredictionId("prediction-a".into()));
3223    }
3224
3225    // --- Phase 5: let more time pass for region B to settle ---
3226    // B's last edit was Q/4 before A settled. The worker rescheduled to
3227    // B's last_edit_at + Q, which is 3Q/4 from now.
3228    cx.executor()
3229        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE * 3 / 4);
3230    cx.run_until_parked();
3231
3232    {
3233        let events = settled_events.lock().clone();
3234        assert_eq!(
3235            events.len(),
3236            2,
3237            "both prediction and capture_sample settled events should be emitted for each request, got: {events:?}"
3238        );
3239        assert_eq!(events[1].0, EditPredictionId("prediction-b".into()));
3240    }
3241}
3242
3243#[ctor::ctor]
3244fn init_logger() {
3245    zlog::init_test();
3246}