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(app_state.clone(), 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(workspace.read(cx).app_state().clone(), 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_large_edits_are_omitted_from_history(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": (0..20)
1023                .map(|i| format!("line {i}\n"))
1024                .collect::<String>()
1025        }),
1026    )
1027    .await;
1028    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1029
1030    let buffer = project
1031        .update(cx, |project, cx| {
1032            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
1033            project.set_active_path(Some(path.clone()), cx);
1034            project.open_buffer(path, cx)
1035        })
1036        .await
1037        .unwrap();
1038
1039    let cursor = buffer.read_with(cx, |buffer, _cx| buffer.anchor_before(Point::new(1, 0)));
1040
1041    ep_store.update(cx, |ep_store, cx| {
1042        ep_store.register_buffer(&buffer, &project, cx);
1043        let _ = ep_store.prediction_at(&buffer, Some(cursor), &project, cx);
1044    });
1045
1046    buffer.update(cx, |buffer, cx| {
1047        buffer.edit(vec![(0..6, "LOCAL ZERO")], None, cx);
1048    });
1049
1050    let (collaborator, mut collaborator_version) = make_collaborator_replica(&buffer, cx);
1051
1052    let (line_three_start, line_three_len) = collaborator.read_with(cx, |buffer, _cx| {
1053        (Point::new(3, 0).to_offset(buffer), buffer.line_len(3))
1054    });
1055    let large_edit = "X".repeat(EDIT_HISTORY_DIFF_SIZE_LIMIT + 1);
1056
1057    apply_collaborator_edit(
1058        &collaborator,
1059        &buffer,
1060        &mut collaborator_version,
1061        line_three_start..line_three_start + line_three_len as usize,
1062        &large_edit,
1063        cx,
1064    )
1065    .await;
1066
1067    buffer.update(cx, |buffer, cx| {
1068        let line_seven_start = Point::new(7, 0).to_offset(buffer);
1069        let line_seven_end = Point::new(7, 6).to_offset(buffer);
1070        buffer.edit(
1071            vec![(line_seven_start..line_seven_end, "LOCAL SEVEN")],
1072            None,
1073            cx,
1074        );
1075    });
1076
1077    let events = ep_store.update(cx, |ep_store, cx| {
1078        ep_store.edit_history_for_project(&project, cx)
1079    });
1080
1081    let rendered_events = render_events_with_predicted(&events);
1082
1083    assert_eq!(rendered_events.len(), 2);
1084    assert!(rendered_events[0].contains("+LOCAL ZERO"));
1085    assert!(!rendered_events[0].contains(&large_edit));
1086    assert!(rendered_events[1].contains("+LOCAL SEVEN"));
1087    assert!(!rendered_events[1].contains(&large_edit));
1088}
1089
1090#[gpui::test]
1091async fn test_predicted_flag_coalescing(cx: &mut TestAppContext) {
1092    let (ep_store, _requests) = init_test_with_fake_client(cx);
1093    let fs = FakeFs::new(cx.executor());
1094    fs.insert_tree(
1095        "/root",
1096        json!({
1097            "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"
1098        }),
1099    )
1100    .await;
1101    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1102
1103    let buffer = project
1104        .update(cx, |project, cx| {
1105            let path = project.find_project_path(path!("root/foo.rs"), cx).unwrap();
1106            project.open_buffer(path, cx)
1107        })
1108        .await
1109        .unwrap();
1110
1111    ep_store.update(cx, |ep_store, cx| {
1112        ep_store.register_buffer(&buffer, &project, cx);
1113    });
1114
1115    // Case 1: Manual edits have `predicted` set to false.
1116    buffer.update(cx, |buffer, cx| {
1117        buffer.edit(vec![(0..6, "LINE ZERO")], None, cx);
1118    });
1119
1120    let events = ep_store.update(cx, |ep_store, cx| {
1121        ep_store.edit_history_for_project(&project, cx)
1122    });
1123
1124    assert_eq!(
1125        render_events_with_predicted(&events),
1126        vec![indoc! {"
1127            manual
1128            @@ -1,4 +1,4 @@
1129            -line 0
1130            +LINE ZERO
1131             line 1
1132             line 2
1133             line 3
1134        "}]
1135    );
1136
1137    // Case 2: Multiple successive manual edits near each other are merged into one
1138    // event with `predicted` set to false.
1139    buffer.update(cx, |buffer, cx| {
1140        let offset = Point::new(1, 0).to_offset(buffer);
1141        let end = Point::new(1, 6).to_offset(buffer);
1142        buffer.edit(vec![(offset..end, "LINE ONE")], None, cx);
1143    });
1144
1145    let events = ep_store.update(cx, |ep_store, cx| {
1146        ep_store.edit_history_for_project(&project, cx)
1147    });
1148    assert_eq!(
1149        render_events_with_predicted(&events),
1150        vec![indoc! {"
1151            manual
1152            @@ -1,5 +1,5 @@
1153            -line 0
1154            -line 1
1155            +LINE ZERO
1156            +LINE ONE
1157             line 2
1158             line 3
1159             line 4
1160        "}]
1161    );
1162
1163    // Case 3: Accepted predictions have `predicted` set to true.
1164    // Case 5: A manual edit that follows a predicted edit is not merged with the
1165    // predicted edit, even if it is nearby.
1166    ep_store.update(cx, |ep_store, cx| {
1167        buffer.update(cx, |buffer, cx| {
1168            let offset = Point::new(2, 0).to_offset(buffer);
1169            let end = Point::new(2, 6).to_offset(buffer);
1170            buffer.edit(vec![(offset..end, "LINE TWO")], None, cx);
1171        });
1172        ep_store.report_changes_for_buffer(&buffer, &project, true, true, cx);
1173    });
1174
1175    let events = ep_store.update(cx, |ep_store, cx| {
1176        ep_store.edit_history_for_project(&project, cx)
1177    });
1178    assert_eq!(
1179        render_events_with_predicted(&events),
1180        vec![
1181            indoc! {"
1182                manual
1183                @@ -1,5 +1,5 @@
1184                -line 0
1185                -line 1
1186                +LINE ZERO
1187                +LINE ONE
1188                 line 2
1189                 line 3
1190                 line 4
1191            "},
1192            indoc! {"
1193                predicted
1194                @@ -1,6 +1,6 @@
1195                 LINE ZERO
1196                 LINE ONE
1197                -line 2
1198                +LINE TWO
1199                 line 3
1200                 line 4
1201                 line 5
1202            "}
1203        ]
1204    );
1205
1206    // Case 4: Multiple successive accepted predictions near each other are merged
1207    // into one event with `predicted` set to true.
1208    ep_store.update(cx, |ep_store, cx| {
1209        buffer.update(cx, |buffer, cx| {
1210            let offset = Point::new(3, 0).to_offset(buffer);
1211            let end = Point::new(3, 6).to_offset(buffer);
1212            buffer.edit(vec![(offset..end, "LINE THREE")], None, cx);
1213        });
1214        ep_store.report_changes_for_buffer(&buffer, &project, true, true, cx);
1215    });
1216
1217    let events = ep_store.update(cx, |ep_store, cx| {
1218        ep_store.edit_history_for_project(&project, cx)
1219    });
1220    assert_eq!(
1221        render_events_with_predicted(&events),
1222        vec![
1223            indoc! {"
1224                manual
1225                @@ -1,5 +1,5 @@
1226                -line 0
1227                -line 1
1228                +LINE ZERO
1229                +LINE ONE
1230                 line 2
1231                 line 3
1232                 line 4
1233            "},
1234            indoc! {"
1235                predicted
1236                @@ -1,7 +1,7 @@
1237                 LINE ZERO
1238                 LINE ONE
1239                -line 2
1240                -line 3
1241                +LINE TWO
1242                +LINE THREE
1243                 line 4
1244                 line 5
1245                 line 6
1246            "}
1247        ]
1248    );
1249
1250    // Case 5 (continued): A manual edit that follows a predicted edit is not merged
1251    // with the predicted edit, even if it is nearby.
1252    buffer.update(cx, |buffer, cx| {
1253        let offset = Point::new(4, 0).to_offset(buffer);
1254        let end = Point::new(4, 6).to_offset(buffer);
1255        buffer.edit(vec![(offset..end, "LINE FOUR")], None, cx);
1256    });
1257
1258    let events = ep_store.update(cx, |ep_store, cx| {
1259        ep_store.edit_history_for_project(&project, cx)
1260    });
1261    assert_eq!(
1262        render_events_with_predicted(&events),
1263        vec![
1264            indoc! {"
1265                manual
1266                @@ -1,5 +1,5 @@
1267                -line 0
1268                -line 1
1269                +LINE ZERO
1270                +LINE ONE
1271                 line 2
1272                 line 3
1273                 line 4
1274            "},
1275            indoc! {"
1276                predicted
1277                @@ -1,7 +1,7 @@
1278                 LINE ZERO
1279                 LINE ONE
1280                -line 2
1281                -line 3
1282                +LINE TWO
1283                +LINE THREE
1284                 line 4
1285                 line 5
1286                 line 6
1287            "},
1288            indoc! {"
1289                manual
1290                @@ -2,7 +2,7 @@
1291                 LINE ONE
1292                 LINE TWO
1293                 LINE THREE
1294                -line 4
1295                +LINE FOUR
1296                 line 5
1297                 line 6
1298                 line 7
1299            "}
1300        ]
1301    );
1302
1303    // Case 6: If we then perform a manual edit at a *different* location (more than
1304    // 8 lines away), then the edits at the prior location can be merged with each
1305    // other, even if some are predicted and some are not. `predicted` means all
1306    // constituent edits were predicted.
1307    buffer.update(cx, |buffer, cx| {
1308        let offset = Point::new(14, 0).to_offset(buffer);
1309        let end = Point::new(14, 7).to_offset(buffer);
1310        buffer.edit(vec![(offset..end, "LINE FOURTEEN")], None, cx);
1311    });
1312
1313    let events = ep_store.update(cx, |ep_store, cx| {
1314        ep_store.edit_history_for_project(&project, cx)
1315    });
1316    assert_eq!(
1317        render_events_with_predicted(&events),
1318        vec![
1319            indoc! {"
1320                manual
1321                @@ -1,8 +1,8 @@
1322                -line 0
1323                -line 1
1324                -line 2
1325                -line 3
1326                -line 4
1327                +LINE ZERO
1328                +LINE ONE
1329                +LINE TWO
1330                +LINE THREE
1331                +LINE FOUR
1332                 line 5
1333                 line 6
1334                 line 7
1335            "},
1336            indoc! {"
1337                manual
1338                @@ -12,4 +12,4 @@
1339                 line 11
1340                 line 12
1341                 line 13
1342                -line 14
1343                +LINE FOURTEEN
1344            "}
1345        ]
1346    );
1347}
1348
1349#[gpui::test]
1350async fn test_empty_prediction(cx: &mut TestAppContext) {
1351    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1352    let fs = FakeFs::new(cx.executor());
1353    fs.insert_tree(
1354        "/root",
1355        json!({
1356            "foo.md":  "Hello!\nHow\nBye\n"
1357        }),
1358    )
1359    .await;
1360    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1361
1362    let buffer = project
1363        .update(cx, |project, cx| {
1364            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1365            project.open_buffer(path, cx)
1366        })
1367        .await
1368        .unwrap();
1369    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1370    let position = snapshot.anchor_before(language::Point::new(1, 3));
1371
1372    ep_store.update(cx, |ep_store, cx| {
1373        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1374    });
1375
1376    let (request, respond_tx) = requests.predict.next().await.unwrap();
1377    let response = model_response(&request, "");
1378    let id = response.request_id.clone();
1379    respond_tx.send(response).unwrap();
1380
1381    cx.run_until_parked();
1382
1383    ep_store.update(cx, |ep_store, cx| {
1384        assert!(
1385            ep_store
1386                .prediction_at(&buffer, None, &project, cx)
1387                .is_none()
1388        );
1389    });
1390
1391    // prediction is reported as rejected
1392    let (reject_request, _) = requests.reject.next().await.unwrap();
1393
1394    assert_eq!(
1395        &reject_request.rejections,
1396        &[EditPredictionRejection {
1397            request_id: id,
1398            reason: EditPredictionRejectReason::Empty,
1399            was_shown: false,
1400            model_version: None,
1401            e2e_latency_ms: Some(0),
1402        }]
1403    );
1404}
1405
1406#[gpui::test]
1407async fn test_interpolated_empty(cx: &mut TestAppContext) {
1408    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1409    let fs = FakeFs::new(cx.executor());
1410    fs.insert_tree(
1411        "/root",
1412        json!({
1413            "foo.md":  "Hello!\nHow\nBye\n"
1414        }),
1415    )
1416    .await;
1417    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1418
1419    let buffer = project
1420        .update(cx, |project, cx| {
1421            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1422            project.open_buffer(path, cx)
1423        })
1424        .await
1425        .unwrap();
1426    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1427    let position = snapshot.anchor_before(language::Point::new(1, 3));
1428
1429    ep_store.update(cx, |ep_store, cx| {
1430        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1431    });
1432
1433    let (request, respond_tx) = requests.predict.next().await.unwrap();
1434
1435    buffer.update(cx, |buffer, cx| {
1436        buffer.set_text("Hello!\nHow are you?\nBye", cx);
1437    });
1438
1439    let response = model_response(&request, SIMPLE_DIFF);
1440    let id = response.request_id.clone();
1441    respond_tx.send(response).unwrap();
1442
1443    cx.run_until_parked();
1444
1445    ep_store.update(cx, |ep_store, cx| {
1446        assert!(
1447            ep_store
1448                .prediction_at(&buffer, None, &project, cx)
1449                .is_none()
1450        );
1451    });
1452
1453    // prediction is reported as rejected
1454    let (reject_request, _) = requests.reject.next().await.unwrap();
1455
1456    assert_eq!(
1457        &reject_request.rejections,
1458        &[EditPredictionRejection {
1459            request_id: id,
1460            reason: EditPredictionRejectReason::InterpolatedEmpty,
1461            was_shown: false,
1462            model_version: None,
1463            e2e_latency_ms: Some(0),
1464        }]
1465    );
1466}
1467
1468const SIMPLE_DIFF: &str = indoc! { r"
1469    --- a/root/foo.md
1470    +++ b/root/foo.md
1471    @@ ... @@
1472     Hello!
1473    -How
1474    +How are you?
1475     Bye
1476"};
1477
1478#[gpui::test]
1479async fn test_replace_current(cx: &mut TestAppContext) {
1480    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1481    let fs = FakeFs::new(cx.executor());
1482    fs.insert_tree(
1483        "/root",
1484        json!({
1485            "foo.md":  "Hello!\nHow\nBye\n"
1486        }),
1487    )
1488    .await;
1489    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1490
1491    let buffer = project
1492        .update(cx, |project, cx| {
1493            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1494            project.open_buffer(path, cx)
1495        })
1496        .await
1497        .unwrap();
1498    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1499    let position = snapshot.anchor_before(language::Point::new(1, 3));
1500
1501    ep_store.update(cx, |ep_store, cx| {
1502        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1503    });
1504
1505    let (request, respond_tx) = requests.predict.next().await.unwrap();
1506    let first_response = model_response(&request, SIMPLE_DIFF);
1507    let first_id = first_response.request_id.clone();
1508    respond_tx.send(first_response).unwrap();
1509
1510    cx.run_until_parked();
1511
1512    ep_store.update(cx, |ep_store, cx| {
1513        assert_eq!(
1514            ep_store
1515                .prediction_at(&buffer, None, &project, cx)
1516                .unwrap()
1517                .id
1518                .0,
1519            first_id
1520        );
1521    });
1522
1523    // a second request is triggered
1524    ep_store.update(cx, |ep_store, cx| {
1525        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1526    });
1527
1528    let (request, respond_tx) = requests.predict.next().await.unwrap();
1529    let second_response = model_response(&request, SIMPLE_DIFF);
1530    let second_id = second_response.request_id.clone();
1531    respond_tx.send(second_response).unwrap();
1532
1533    cx.run_until_parked();
1534
1535    ep_store.update(cx, |ep_store, cx| {
1536        // second replaces first
1537        assert_eq!(
1538            ep_store
1539                .prediction_at(&buffer, None, &project, cx)
1540                .unwrap()
1541                .id
1542                .0,
1543            second_id
1544        );
1545    });
1546
1547    // first is reported as replaced
1548    let (reject_request, _) = requests.reject.next().await.unwrap();
1549
1550    assert_eq!(
1551        &reject_request.rejections,
1552        &[EditPredictionRejection {
1553            request_id: first_id,
1554            reason: EditPredictionRejectReason::Replaced,
1555            was_shown: false,
1556            model_version: None,
1557            e2e_latency_ms: Some(0),
1558        }]
1559    );
1560}
1561
1562#[gpui::test]
1563async fn test_current_preferred(cx: &mut TestAppContext) {
1564    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1565    let fs = FakeFs::new(cx.executor());
1566    fs.insert_tree(
1567        "/root",
1568        json!({
1569            "foo.md":  "Hello!\nHow\nBye\n"
1570        }),
1571    )
1572    .await;
1573    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1574
1575    let buffer = project
1576        .update(cx, |project, cx| {
1577            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1578            project.open_buffer(path, cx)
1579        })
1580        .await
1581        .unwrap();
1582    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1583    let position = snapshot.anchor_before(language::Point::new(1, 3));
1584
1585    ep_store.update(cx, |ep_store, cx| {
1586        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1587    });
1588
1589    let (request, respond_tx) = requests.predict.next().await.unwrap();
1590    let first_response = model_response(&request, SIMPLE_DIFF);
1591    let first_id = first_response.request_id.clone();
1592    respond_tx.send(first_response).unwrap();
1593
1594    cx.run_until_parked();
1595
1596    ep_store.update(cx, |ep_store, cx| {
1597        assert_eq!(
1598            ep_store
1599                .prediction_at(&buffer, None, &project, cx)
1600                .unwrap()
1601                .id
1602                .0,
1603            first_id
1604        );
1605    });
1606
1607    // a second request is triggered
1608    ep_store.update(cx, |ep_store, cx| {
1609        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1610    });
1611
1612    let (request, respond_tx) = requests.predict.next().await.unwrap();
1613    // worse than current prediction
1614    let second_response = model_response(
1615        &request,
1616        indoc! { r"
1617            --- a/root/foo.md
1618            +++ b/root/foo.md
1619            @@ ... @@
1620             Hello!
1621            -How
1622            +How are
1623             Bye
1624        "},
1625    );
1626    let second_id = second_response.request_id.clone();
1627    respond_tx.send(second_response).unwrap();
1628
1629    cx.run_until_parked();
1630
1631    ep_store.update(cx, |ep_store, cx| {
1632        // first is preferred over second
1633        assert_eq!(
1634            ep_store
1635                .prediction_at(&buffer, None, &project, cx)
1636                .unwrap()
1637                .id
1638                .0,
1639            first_id
1640        );
1641    });
1642
1643    // second is reported as rejected
1644    let (reject_request, _) = requests.reject.next().await.unwrap();
1645
1646    assert_eq!(
1647        &reject_request.rejections,
1648        &[EditPredictionRejection {
1649            request_id: second_id,
1650            reason: EditPredictionRejectReason::CurrentPreferred,
1651            was_shown: false,
1652            model_version: None,
1653            e2e_latency_ms: Some(0),
1654        }]
1655    );
1656}
1657
1658#[gpui::test]
1659async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
1660    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1661    let fs = FakeFs::new(cx.executor());
1662    fs.insert_tree(
1663        "/root",
1664        json!({
1665            "foo.md":  "Hello!\nHow\nBye\n"
1666        }),
1667    )
1668    .await;
1669    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1670
1671    let buffer = project
1672        .update(cx, |project, cx| {
1673            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1674            project.open_buffer(path, cx)
1675        })
1676        .await
1677        .unwrap();
1678    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1679    let position = snapshot.anchor_before(language::Point::new(1, 3));
1680
1681    // start two refresh tasks
1682    ep_store.update(cx, |ep_store, cx| {
1683        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1684    });
1685
1686    let (request1, respond_first) = requests.predict.next().await.unwrap();
1687
1688    ep_store.update(cx, |ep_store, cx| {
1689        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1690    });
1691
1692    let (request, respond_second) = requests.predict.next().await.unwrap();
1693
1694    // wait for throttle
1695    cx.run_until_parked();
1696
1697    // second responds first
1698    let second_response = model_response(&request, SIMPLE_DIFF);
1699    let second_id = second_response.request_id.clone();
1700    respond_second.send(second_response).unwrap();
1701
1702    cx.run_until_parked();
1703
1704    ep_store.update(cx, |ep_store, cx| {
1705        // current prediction is second
1706        assert_eq!(
1707            ep_store
1708                .prediction_at(&buffer, None, &project, cx)
1709                .unwrap()
1710                .id
1711                .0,
1712            second_id
1713        );
1714    });
1715
1716    let first_response = model_response(&request1, SIMPLE_DIFF);
1717    let first_id = first_response.request_id.clone();
1718    respond_first.send(first_response).unwrap();
1719
1720    cx.run_until_parked();
1721
1722    ep_store.update(cx, |ep_store, cx| {
1723        // current prediction is still second, since first was cancelled
1724        assert_eq!(
1725            ep_store
1726                .prediction_at(&buffer, None, &project, cx)
1727                .unwrap()
1728                .id
1729                .0,
1730            second_id
1731        );
1732    });
1733
1734    // first is reported as rejected
1735    let (reject_request, _) = requests.reject.next().await.unwrap();
1736
1737    cx.run_until_parked();
1738
1739    assert_eq!(
1740        &reject_request.rejections,
1741        &[EditPredictionRejection {
1742            request_id: first_id,
1743            reason: EditPredictionRejectReason::Canceled,
1744            was_shown: false,
1745            model_version: None,
1746            e2e_latency_ms: None,
1747        }]
1748    );
1749}
1750
1751#[gpui::test]
1752async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
1753    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1754    let fs = FakeFs::new(cx.executor());
1755    fs.insert_tree(
1756        "/root",
1757        json!({
1758            "foo.md":  "Hello!\nHow\nBye\n"
1759        }),
1760    )
1761    .await;
1762    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1763
1764    let buffer = project
1765        .update(cx, |project, cx| {
1766            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1767            project.open_buffer(path, cx)
1768        })
1769        .await
1770        .unwrap();
1771    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1772    let position = snapshot.anchor_before(language::Point::new(1, 3));
1773
1774    // start two refresh tasks
1775    ep_store.update(cx, |ep_store, cx| {
1776        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1777    });
1778
1779    let (request1, respond_first) = requests.predict.next().await.unwrap();
1780
1781    ep_store.update(cx, |ep_store, cx| {
1782        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1783    });
1784
1785    let (request2, respond_second) = requests.predict.next().await.unwrap();
1786
1787    // wait for throttle, so requests are sent
1788    cx.run_until_parked();
1789
1790    ep_store.update(cx, |ep_store, cx| {
1791        // start a third request
1792        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1793
1794        // 2 are pending, so 2nd is cancelled
1795        assert_eq!(
1796            ep_store
1797                .get_or_init_project(&project, cx)
1798                .cancelled_predictions
1799                .iter()
1800                .copied()
1801                .collect::<Vec<_>>(),
1802            [1]
1803        );
1804    });
1805
1806    // wait for throttle
1807    cx.run_until_parked();
1808
1809    let (request3, respond_third) = requests.predict.next().await.unwrap();
1810
1811    let first_response = model_response(&request1, SIMPLE_DIFF);
1812    let first_id = first_response.request_id.clone();
1813    respond_first.send(first_response).unwrap();
1814
1815    cx.run_until_parked();
1816
1817    ep_store.update(cx, |ep_store, cx| {
1818        // current prediction is first
1819        assert_eq!(
1820            ep_store
1821                .prediction_at(&buffer, None, &project, cx)
1822                .unwrap()
1823                .id
1824                .0,
1825            first_id
1826        );
1827    });
1828
1829    let cancelled_response = model_response(&request2, SIMPLE_DIFF);
1830    let cancelled_id = cancelled_response.request_id.clone();
1831    respond_second.send(cancelled_response).unwrap();
1832
1833    cx.run_until_parked();
1834
1835    ep_store.update(cx, |ep_store, cx| {
1836        // current prediction is still first, since second was cancelled
1837        assert_eq!(
1838            ep_store
1839                .prediction_at(&buffer, None, &project, cx)
1840                .unwrap()
1841                .id
1842                .0,
1843            first_id
1844        );
1845    });
1846
1847    let third_response = model_response(&request3, SIMPLE_DIFF);
1848    let third_response_id = third_response.request_id.clone();
1849    respond_third.send(third_response).unwrap();
1850
1851    cx.run_until_parked();
1852
1853    ep_store.update(cx, |ep_store, cx| {
1854        // third completes and replaces first
1855        assert_eq!(
1856            ep_store
1857                .prediction_at(&buffer, None, &project, cx)
1858                .unwrap()
1859                .id
1860                .0,
1861            third_response_id
1862        );
1863    });
1864
1865    // second is reported as rejected
1866    let (reject_request, _) = requests.reject.next().await.unwrap();
1867
1868    cx.run_until_parked();
1869
1870    assert_eq!(
1871        &reject_request.rejections,
1872        &[
1873            EditPredictionRejection {
1874                request_id: cancelled_id,
1875                reason: EditPredictionRejectReason::Canceled,
1876                was_shown: false,
1877                model_version: None,
1878                e2e_latency_ms: None,
1879            },
1880            EditPredictionRejection {
1881                request_id: first_id,
1882                reason: EditPredictionRejectReason::Replaced,
1883                was_shown: false,
1884                model_version: None,
1885                // 2 throttle waits (for 2nd and 3rd requests) elapsed
1886                // between this request's start and response.
1887                e2e_latency_ms: Some(2 * EditPredictionStore::THROTTLE_TIMEOUT.as_millis()),
1888            }
1889        ]
1890    );
1891}
1892
1893#[gpui::test]
1894async fn test_jump_and_edit_throttles_are_independent(cx: &mut TestAppContext) {
1895    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1896
1897    let fs = FakeFs::new(cx.executor());
1898    fs.insert_tree(
1899        "/root",
1900        json!({
1901            "foo.md":  "Hello!\nHow\nBye\n",
1902            "bar.md": "Hola!\nComo\nAdios\n"
1903        }),
1904    )
1905    .await;
1906    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
1907
1908    let buffer = project
1909        .update(cx, |project, cx| {
1910            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
1911            project.set_active_path(Some(path.clone()), cx);
1912            project.open_buffer(path, cx)
1913        })
1914        .await
1915        .unwrap();
1916    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
1917    let position = snapshot.anchor_before(language::Point::new(1, 3));
1918
1919    ep_store.update(cx, |ep_store, cx| {
1920        ep_store.register_project(&project, cx);
1921        ep_store.register_buffer(&buffer, &project, cx);
1922    });
1923
1924    // First edit request - no prior edit, so not throttled.
1925    ep_store.update(cx, |ep_store, cx| {
1926        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1927    });
1928    let (_edit_request, edit_response_tx) = requests.predict.next().await.unwrap();
1929    edit_response_tx.send(empty_response()).unwrap();
1930    cx.run_until_parked();
1931
1932    let diagnostic = lsp::Diagnostic {
1933        range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
1934        severity: Some(lsp::DiagnosticSeverity::ERROR),
1935        message: "Sentence is incomplete".to_string(),
1936        ..Default::default()
1937    };
1938
1939    // First jump request triggered by diagnostic event on buffer - no prior jump, so not throttled (independent from edit).
1940    project.update(cx, |project, cx| {
1941        project.lsp_store().update(cx, |lsp_store, cx| {
1942            lsp_store
1943                .update_diagnostics(
1944                    LanguageServerId(0),
1945                    lsp::PublishDiagnosticsParams {
1946                        uri: lsp::Uri::from_file_path(path!("/root/bar.md")).unwrap(),
1947                        diagnostics: vec![diagnostic],
1948                        version: None,
1949                    },
1950                    None,
1951                    language::DiagnosticSourceKind::Pushed,
1952                    &[],
1953                    cx,
1954                )
1955                .unwrap();
1956        });
1957    });
1958    let (_jump_request, jump_response_tx) = requests.predict.next().await.unwrap();
1959    jump_response_tx.send(empty_response()).unwrap();
1960    cx.run_until_parked();
1961
1962    // Second edit request - should be throttled by the first edit.
1963    ep_store.update(cx, |ep_store, cx| {
1964        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
1965    });
1966    assert_no_predict_request_ready(&mut requests.predict);
1967
1968    // Second jump request - should be throttled by the first jump.
1969    ep_store.update(cx, |ep_store, cx| {
1970        ep_store.refresh_prediction_from_diagnostics(
1971            project.clone(),
1972            DiagnosticSearchScope::Global,
1973            cx,
1974        );
1975    });
1976    assert_no_predict_request_ready(&mut requests.predict);
1977
1978    // Wait for both throttles to expire.
1979    cx.background_executor
1980        .advance_clock(EditPredictionStore::THROTTLE_TIMEOUT);
1981    cx.background_executor.run_until_parked();
1982    cx.run_until_parked();
1983
1984    // Both requests should now go through.
1985    let (_request_1, response_tx_1) = requests.predict.next().await.unwrap();
1986    response_tx_1.send(empty_response()).unwrap();
1987    cx.run_until_parked();
1988
1989    let (_request_2, response_tx_2) = requests.predict.next().await.unwrap();
1990    response_tx_2.send(empty_response()).unwrap();
1991    cx.run_until_parked();
1992}
1993
1994#[gpui::test]
1995async fn test_same_frame_duplicate_requests_deduplicated(cx: &mut TestAppContext) {
1996    let (ep_store, mut requests) = init_test_with_fake_client(cx);
1997    let fs = FakeFs::new(cx.executor());
1998    fs.insert_tree(
1999        "/root",
2000        json!({
2001            "foo.md":  "Hello!\nHow\nBye\n"
2002        }),
2003    )
2004    .await;
2005    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
2006
2007    let buffer = project
2008        .update(cx, |project, cx| {
2009            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
2010            project.open_buffer(path, cx)
2011        })
2012        .await
2013        .unwrap();
2014    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2015    let position = snapshot.anchor_before(language::Point::new(1, 3));
2016
2017    // Enqueue two refresh calls in the same synchronous frame (no yielding).
2018    // Both `cx.spawn` tasks are created before either executes, so they both
2019    // capture the same `proceed_count_at_enqueue`. Only the first task should
2020    // pass the deduplication gate; the second should be skipped.
2021    ep_store.update(cx, |ep_store, cx| {
2022        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
2023        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
2024    });
2025
2026    // Let both spawned tasks run to completion (including any throttle waits).
2027    cx.run_until_parked();
2028
2029    // Exactly one prediction request should have been sent.
2030    let (request, respond_tx) = requests.predict.next().await.unwrap();
2031    respond_tx
2032        .send(model_response(&request, SIMPLE_DIFF))
2033        .unwrap();
2034    cx.run_until_parked();
2035
2036    // No second request should be pending.
2037    assert_no_predict_request_ready(&mut requests.predict);
2038}
2039
2040#[gpui::test]
2041async fn test_rejections_flushing(cx: &mut TestAppContext) {
2042    let (ep_store, mut requests) = init_test_with_fake_client(cx);
2043
2044    ep_store.update(cx, |ep_store, cx| {
2045        ep_store.reject_prediction(
2046            EditPredictionId("test-1".into()),
2047            EditPredictionRejectReason::Discarded,
2048            false,
2049            None,
2050            None,
2051            cx,
2052        );
2053        ep_store.reject_prediction(
2054            EditPredictionId("test-2".into()),
2055            EditPredictionRejectReason::Canceled,
2056            true,
2057            None,
2058            None,
2059            cx,
2060        );
2061    });
2062
2063    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
2064    cx.run_until_parked();
2065
2066    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2067    respond_tx.send(()).unwrap();
2068
2069    // batched
2070    assert_eq!(reject_request.rejections.len(), 2);
2071    assert_eq!(
2072        reject_request.rejections[0],
2073        EditPredictionRejection {
2074            request_id: "test-1".to_string(),
2075            reason: EditPredictionRejectReason::Discarded,
2076            was_shown: false,
2077            model_version: None,
2078            e2e_latency_ms: None
2079        }
2080    );
2081    assert_eq!(
2082        reject_request.rejections[1],
2083        EditPredictionRejection {
2084            request_id: "test-2".to_string(),
2085            reason: EditPredictionRejectReason::Canceled,
2086            was_shown: true,
2087            model_version: None,
2088            e2e_latency_ms: None
2089        }
2090    );
2091
2092    // Reaching batch size limit sends without debounce
2093    ep_store.update(cx, |ep_store, cx| {
2094        for i in 0..70 {
2095            ep_store.reject_prediction(
2096                EditPredictionId(format!("batch-{}", i).into()),
2097                EditPredictionRejectReason::Discarded,
2098                false,
2099                None,
2100                None,
2101                cx,
2102            );
2103        }
2104    });
2105
2106    // First MAX/2 items are sent immediately
2107    cx.run_until_parked();
2108    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2109    respond_tx.send(()).unwrap();
2110
2111    assert_eq!(reject_request.rejections.len(), 50);
2112    assert_eq!(reject_request.rejections[0].request_id, "batch-0");
2113    assert_eq!(reject_request.rejections[49].request_id, "batch-49");
2114
2115    // Remaining items are debounced with the next batch
2116    cx.executor().advance_clock(Duration::from_secs(15));
2117    cx.run_until_parked();
2118
2119    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2120    respond_tx.send(()).unwrap();
2121
2122    assert_eq!(reject_request.rejections.len(), 20);
2123    assert_eq!(reject_request.rejections[0].request_id, "batch-50");
2124    assert_eq!(reject_request.rejections[19].request_id, "batch-69");
2125
2126    // Request failure
2127    ep_store.update(cx, |ep_store, cx| {
2128        ep_store.reject_prediction(
2129            EditPredictionId("retry-1".into()),
2130            EditPredictionRejectReason::Discarded,
2131            false,
2132            None,
2133            None,
2134            cx,
2135        );
2136    });
2137
2138    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
2139    cx.run_until_parked();
2140
2141    let (reject_request, _respond_tx) = requests.reject.next().await.unwrap();
2142    assert_eq!(reject_request.rejections.len(), 1);
2143    assert_eq!(reject_request.rejections[0].request_id, "retry-1");
2144    // Simulate failure
2145    drop(_respond_tx);
2146
2147    // Add another rejection
2148    ep_store.update(cx, |ep_store, cx| {
2149        ep_store.reject_prediction(
2150            EditPredictionId("retry-2".into()),
2151            EditPredictionRejectReason::Discarded,
2152            false,
2153            None,
2154            None,
2155            cx,
2156        );
2157    });
2158
2159    cx.executor().advance_clock(REJECT_REQUEST_DEBOUNCE);
2160    cx.run_until_parked();
2161
2162    // Retry should include both the failed item and the new one
2163    let (reject_request, respond_tx) = requests.reject.next().await.unwrap();
2164    respond_tx.send(()).unwrap();
2165
2166    assert_eq!(reject_request.rejections.len(), 2);
2167    assert_eq!(reject_request.rejections[0].request_id, "retry-1");
2168    assert_eq!(reject_request.rejections[1].request_id, "retry-2");
2169}
2170
2171#[gpui::test]
2172fn test_active_buffer_diagnostics_fetching(cx: &mut TestAppContext) {
2173    let diagnostic_marker: TextRangeMarker = ('«', '»').into();
2174    let search_range_marker: TextRangeMarker = ('[', ']').into();
2175
2176    let (text, mut ranges) = marked_text_ranges_by(
2177        indoc! {r#"
2178            fn alpha() {
2179                let «first_value» = 1;
2180            }
2181
2182            [fn beta() {
2183                let «second_value» = 2;
2184                let third_value = second_value + missing_symbol;
2185            }ˇ]
2186
2187            fn gamma() {
2188                let «fourth_value» = missing_other_symbol;
2189            }
2190        "#},
2191        vec![diagnostic_marker.clone(), search_range_marker.clone()],
2192    );
2193
2194    let diagnostic_ranges = ranges.remove(&diagnostic_marker).unwrap_or_default();
2195    let search_ranges = ranges.remove(&search_range_marker).unwrap_or_default();
2196
2197    let buffer = cx.new(|cx| Buffer::local(&text, cx));
2198
2199    buffer.update(cx, |buffer, cx| {
2200        let snapshot = buffer.snapshot();
2201        let diagnostics = DiagnosticSet::new(
2202            diagnostic_ranges
2203                .iter()
2204                .enumerate()
2205                .map(|(index, range)| DiagnosticEntry {
2206                    range: snapshot.offset_to_point_utf16(range.start)
2207                        ..snapshot.offset_to_point_utf16(range.end),
2208                    diagnostic: Diagnostic {
2209                        severity: match index {
2210                            0 => DiagnosticSeverity::WARNING,
2211                            1 => DiagnosticSeverity::ERROR,
2212                            _ => DiagnosticSeverity::HINT,
2213                        },
2214                        message: match index {
2215                            0 => "first warning".to_string(),
2216                            1 => "second error".to_string(),
2217                            _ => "third hint".to_string(),
2218                        },
2219                        group_id: index + 1,
2220                        is_primary: true,
2221                        source_kind: language::DiagnosticSourceKind::Pushed,
2222                        ..Diagnostic::default()
2223                    },
2224                }),
2225            &snapshot,
2226        );
2227        buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
2228    });
2229
2230    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2231    let search_range = snapshot.offset_to_point(search_ranges[0].start)
2232        ..snapshot.offset_to_point(search_ranges[0].end);
2233
2234    let active_buffer_diagnostics = zeta::active_buffer_diagnostics(&snapshot, search_range, 100);
2235
2236    assert_eq!(
2237        active_buffer_diagnostics,
2238        vec![zeta_prompt::ActiveBufferDiagnostic {
2239            severity: Some(1),
2240            message: "second error".to_string(),
2241            snippet: text,
2242            snippet_buffer_row_range: 5..5,
2243            diagnostic_range_in_snippet: 61..73,
2244        }]
2245    );
2246
2247    let buffer = cx.new(|cx| {
2248        Buffer::local(
2249            indoc! {"
2250                one
2251                two
2252                three
2253                four
2254                five
2255            "},
2256            cx,
2257        )
2258    });
2259
2260    buffer.update(cx, |buffer, cx| {
2261        let snapshot = buffer.snapshot();
2262        let diagnostics = DiagnosticSet::new(
2263            vec![
2264                DiagnosticEntry {
2265                    range: text::PointUtf16::new(0, 0)..text::PointUtf16::new(0, 3),
2266                    diagnostic: Diagnostic {
2267                        severity: DiagnosticSeverity::ERROR,
2268                        message: "row zero".to_string(),
2269                        group_id: 1,
2270                        is_primary: true,
2271                        source_kind: language::DiagnosticSourceKind::Pushed,
2272                        ..Diagnostic::default()
2273                    },
2274                },
2275                DiagnosticEntry {
2276                    range: text::PointUtf16::new(2, 0)..text::PointUtf16::new(2, 5),
2277                    diagnostic: Diagnostic {
2278                        severity: DiagnosticSeverity::WARNING,
2279                        message: "row two".to_string(),
2280                        group_id: 2,
2281                        is_primary: true,
2282                        source_kind: language::DiagnosticSourceKind::Pushed,
2283                        ..Diagnostic::default()
2284                    },
2285                },
2286                DiagnosticEntry {
2287                    range: text::PointUtf16::new(4, 0)..text::PointUtf16::new(4, 4),
2288                    diagnostic: Diagnostic {
2289                        severity: DiagnosticSeverity::INFORMATION,
2290                        message: "row four".to_string(),
2291                        group_id: 3,
2292                        is_primary: true,
2293                        source_kind: language::DiagnosticSourceKind::Pushed,
2294                        ..Diagnostic::default()
2295                    },
2296                },
2297            ],
2298            &snapshot,
2299        );
2300        buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
2301    });
2302
2303    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2304
2305    let active_buffer_diagnostics =
2306        zeta::active_buffer_diagnostics(&snapshot, Point::new(2, 0)..Point::new(4, 0), 100);
2307
2308    assert_eq!(
2309        active_buffer_diagnostics
2310            .iter()
2311            .map(|diagnostic| (
2312                diagnostic.severity,
2313                diagnostic.message.clone(),
2314                diagnostic.snippet.clone(),
2315                diagnostic.snippet_buffer_row_range.clone(),
2316                diagnostic.diagnostic_range_in_snippet.clone(),
2317            ))
2318            .collect::<Vec<_>>(),
2319        vec![
2320            (
2321                Some(2),
2322                "row two".to_string(),
2323                "one\ntwo\nthree\nfour\nfive\n".to_string(),
2324                2..2,
2325                8..13,
2326            ),
2327            (
2328                Some(3),
2329                "row four".to_string(),
2330                "one\ntwo\nthree\nfour\nfive\n".to_string(),
2331                4..4,
2332                19..23,
2333            ),
2334        ]
2335    );
2336}
2337
2338// Generate a model response that would apply the given diff to the active file.
2339fn model_response(request: &PredictEditsV3Request, diff_to_apply: &str) -> PredictEditsV3Response {
2340    let editable_range =
2341        zeta_prompt::excerpt_range_for_format(Default::default(), &request.input.excerpt_ranges).1;
2342    let excerpt = request.input.cursor_excerpt[editable_range.clone()].to_string();
2343    let new_excerpt = apply_diff_to_string(diff_to_apply, &excerpt).unwrap();
2344
2345    PredictEditsV3Response {
2346        request_id: Uuid::new_v4().to_string(),
2347        editable_range,
2348        output: new_excerpt,
2349        model_version: None,
2350    }
2351}
2352
2353fn empty_response() -> PredictEditsV3Response {
2354    PredictEditsV3Response {
2355        request_id: Uuid::new_v4().to_string(),
2356        editable_range: 0..0,
2357        output: String::new(),
2358        model_version: None,
2359    }
2360}
2361
2362fn prompt_from_request(request: &PredictEditsV3Request) -> String {
2363    zeta_prompt::format_zeta_prompt(&request.input, zeta_prompt::ZetaFormat::default())
2364        .expect("default zeta prompt formatting should succeed in edit prediction tests")
2365}
2366
2367fn assert_no_predict_request_ready(
2368    requests: &mut mpsc::UnboundedReceiver<(
2369        PredictEditsV3Request,
2370        oneshot::Sender<PredictEditsV3Response>,
2371    )>,
2372) {
2373    if requests.next().now_or_never().flatten().is_some() {
2374        panic!("Unexpected prediction request while throttled.");
2375    }
2376}
2377
2378struct RequestChannels {
2379    predict: mpsc::UnboundedReceiver<(
2380        PredictEditsV3Request,
2381        oneshot::Sender<PredictEditsV3Response>,
2382    )>,
2383    reject: mpsc::UnboundedReceiver<(RejectEditPredictionsBody, oneshot::Sender<()>)>,
2384}
2385
2386fn init_test_with_fake_client(
2387    cx: &mut TestAppContext,
2388) -> (Entity<EditPredictionStore>, RequestChannels) {
2389    cx.update(move |cx| {
2390        let settings_store = SettingsStore::test(cx);
2391        cx.set_global(settings_store);
2392        zlog::init_test();
2393
2394        let (predict_req_tx, predict_req_rx) = mpsc::unbounded();
2395        let (reject_req_tx, reject_req_rx) = mpsc::unbounded();
2396
2397        let http_client = FakeHttpClient::create({
2398            move |req| {
2399                let uri = req.uri().path().to_string();
2400                let mut body = req.into_body();
2401                let predict_req_tx = predict_req_tx.clone();
2402                let reject_req_tx = reject_req_tx.clone();
2403                async move {
2404                    let resp = match uri.as_str() {
2405                        "/client/llm_tokens" => serde_json::to_string(&json!({
2406                            "token": "test"
2407                        }))
2408                        .unwrap(),
2409                        "/predict_edits/v3" => {
2410                            let mut buf = Vec::new();
2411                            body.read_to_end(&mut buf).await.ok();
2412                            let decompressed = zstd::decode_all(&buf[..]).unwrap();
2413                            let req = serde_json::from_slice(&decompressed).unwrap();
2414
2415                            let (res_tx, res_rx) = oneshot::channel();
2416                            predict_req_tx.unbounded_send((req, res_tx)).unwrap();
2417                            serde_json::to_string(&res_rx.await?).unwrap()
2418                        }
2419                        "/predict_edits/reject" => {
2420                            let mut buf = Vec::new();
2421                            body.read_to_end(&mut buf).await.ok();
2422                            let req = serde_json::from_slice(&buf).unwrap();
2423
2424                            let (res_tx, res_rx) = oneshot::channel();
2425                            reject_req_tx.unbounded_send((req, res_tx)).unwrap();
2426                            serde_json::to_string(&res_rx.await?).unwrap()
2427                        }
2428                        _ => {
2429                            panic!("Unexpected path: {}", uri)
2430                        }
2431                    };
2432
2433                    Ok(Response::builder().body(resp.into()).unwrap())
2434                }
2435            }
2436        });
2437
2438        let client = client::Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
2439        client.cloud_client().set_credentials(1, "test".into());
2440
2441        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2442        language_model::init(user_store.clone(), client.clone(), cx);
2443        let ep_store = EditPredictionStore::global(&client, &user_store, cx);
2444
2445        (
2446            ep_store,
2447            RequestChannels {
2448                predict: predict_req_rx,
2449                reject: reject_req_rx,
2450            },
2451        )
2452    })
2453}
2454
2455#[gpui::test]
2456async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
2457    let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx));
2458    let edits: Arc<[(Range<Anchor>, Arc<str>)]> = cx.update(|cx| {
2459        to_completion_edits([(2..5, "REM".into()), (9..11, "".into())], &buffer, cx).into()
2460    });
2461
2462    let edit_preview = cx
2463        .read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
2464        .await;
2465
2466    let prediction = EditPrediction {
2467        edits,
2468        cursor_position: None,
2469        edit_preview,
2470        buffer: buffer.clone(),
2471        snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
2472        id: EditPredictionId("the-id".into()),
2473        inputs: ZetaPromptInput {
2474            events: Default::default(),
2475            related_files: Default::default(),
2476            active_buffer_diagnostics: vec![],
2477            cursor_path: Path::new("").into(),
2478            cursor_excerpt: "".into(),
2479            cursor_offset_in_excerpt: 0,
2480            excerpt_start_row: None,
2481            excerpt_ranges: Default::default(),
2482            syntax_ranges: None,
2483            experiment: None,
2484            in_open_source_repo: false,
2485            can_collect_data: false,
2486            repo_url: None,
2487        },
2488        model_version: None,
2489    };
2490
2491    cx.update(|cx| {
2492        assert_eq!(
2493            from_completion_edits(
2494                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2495                &buffer,
2496                cx
2497            ),
2498            vec![(2..5, "REM".into()), (9..11, "".into())]
2499        );
2500
2501        buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
2502        assert_eq!(
2503            from_completion_edits(
2504                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2505                &buffer,
2506                cx
2507            ),
2508            vec![(2..2, "REM".into()), (6..8, "".into())]
2509        );
2510
2511        buffer.update(cx, |buffer, cx| buffer.undo(cx));
2512        assert_eq!(
2513            from_completion_edits(
2514                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2515                &buffer,
2516                cx
2517            ),
2518            vec![(2..5, "REM".into()), (9..11, "".into())]
2519        );
2520
2521        buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
2522        assert_eq!(
2523            from_completion_edits(
2524                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2525                &buffer,
2526                cx
2527            ),
2528            vec![(3..3, "EM".into()), (7..9, "".into())]
2529        );
2530
2531        buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
2532        assert_eq!(
2533            from_completion_edits(
2534                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2535                &buffer,
2536                cx
2537            ),
2538            vec![(4..4, "M".into()), (8..10, "".into())]
2539        );
2540
2541        buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
2542        assert_eq!(
2543            from_completion_edits(
2544                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2545                &buffer,
2546                cx
2547            ),
2548            vec![(9..11, "".into())]
2549        );
2550
2551        buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
2552        assert_eq!(
2553            from_completion_edits(
2554                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2555                &buffer,
2556                cx
2557            ),
2558            vec![(4..4, "M".into()), (8..10, "".into())]
2559        );
2560
2561        buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
2562        assert_eq!(
2563            from_completion_edits(
2564                &prediction.interpolate(&buffer.read(cx).snapshot()).unwrap(),
2565                &buffer,
2566                cx
2567            ),
2568            vec![(4..4, "M".into())]
2569        );
2570
2571        buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
2572        assert_eq!(prediction.interpolate(&buffer.read(cx).snapshot()), None);
2573    })
2574}
2575
2576#[gpui::test]
2577async fn test_clean_up_diff(cx: &mut TestAppContext) {
2578    init_test(cx);
2579
2580    assert_eq!(
2581        apply_edit_prediction(
2582            indoc! {"
2583                    fn main() {
2584                        let word_1 = \"lorem\";
2585                        let range = word.len()..word.len();
2586                    }
2587                "},
2588            indoc! {"
2589                    fn main() {
2590                        let word_1 = \"lorem\";
2591                        let range = word_1.len()..word_1.len();
2592                    }
2593                "},
2594            cx,
2595        )
2596        .await,
2597        indoc! {"
2598                fn main() {
2599                    let word_1 = \"lorem\";
2600                    let range = word_1.len()..word_1.len();
2601                }
2602            "},
2603    );
2604
2605    assert_eq!(
2606        apply_edit_prediction(
2607            indoc! {"
2608                    fn main() {
2609                        let story = \"the quick\"
2610                    }
2611                "},
2612            indoc! {"
2613                    fn main() {
2614                        let story = \"the quick brown fox jumps over the lazy dog\";
2615                    }
2616                "},
2617            cx,
2618        )
2619        .await,
2620        indoc! {"
2621                fn main() {
2622                    let story = \"the quick brown fox jumps over the lazy dog\";
2623                }
2624            "},
2625    );
2626}
2627
2628#[gpui::test]
2629async fn test_edit_prediction_end_of_buffer(cx: &mut TestAppContext) {
2630    init_test(cx);
2631
2632    let buffer_content = "lorem\n";
2633    let completion_response = "lorem\nipsum\n";
2634
2635    assert_eq!(
2636        apply_edit_prediction(buffer_content, completion_response, cx).await,
2637        "lorem\nipsum\n"
2638    );
2639}
2640
2641#[gpui::test]
2642async fn test_edit_prediction_no_spurious_trailing_newline(cx: &mut TestAppContext) {
2643    // Test that zeta2's newline normalization logic doesn't insert spurious newlines.
2644    // When the buffer ends without a trailing newline, but the model returns output
2645    // with a trailing newline, zeta2 should normalize both sides before diffing
2646    // so no spurious newline is inserted.
2647    let (ep_store, mut requests) = init_test_with_fake_client(cx);
2648    let fs = FakeFs::new(cx.executor());
2649
2650    // Single line buffer with no trailing newline
2651    fs.insert_tree(
2652        "/root",
2653        json!({
2654            "foo.txt": "hello"
2655        }),
2656    )
2657    .await;
2658    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
2659
2660    let buffer = project
2661        .update(cx, |project, cx| {
2662            let path = project
2663                .find_project_path(path!("root/foo.txt"), cx)
2664                .unwrap();
2665            project.open_buffer(path, cx)
2666        })
2667        .await
2668        .unwrap();
2669
2670    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
2671    let position = snapshot.anchor_before(language::Point::new(0, 5));
2672
2673    ep_store.update(cx, |ep_store, cx| {
2674        ep_store.refresh_prediction_from_buffer(project.clone(), buffer.clone(), position, cx);
2675    });
2676
2677    let (request, respond_tx) = requests.predict.next().await.unwrap();
2678
2679    // Model returns output WITH a trailing newline, even though the buffer doesn't have one.
2680    // Zeta2 should normalize both sides before diffing, so no spurious newline is inserted.
2681    let excerpt_length = request.input.cursor_excerpt.len();
2682    let response = PredictEditsV3Response {
2683        request_id: Uuid::new_v4().to_string(),
2684        output: "hello world\n".to_string(),
2685        editable_range: 0..excerpt_length,
2686        model_version: None,
2687    };
2688    respond_tx.send(response).unwrap();
2689
2690    cx.run_until_parked();
2691
2692    // The prediction should insert " world" without adding a newline
2693    ep_store.update(cx, |ep_store, cx| {
2694        let prediction = ep_store
2695            .prediction_at(&buffer, None, &project, cx)
2696            .expect("should have prediction");
2697        let edits: Vec<_> = prediction
2698            .edits
2699            .iter()
2700            .map(|(range, text)| {
2701                let snapshot = buffer.read(cx).snapshot();
2702                (range.to_offset(&snapshot), text.clone())
2703            })
2704            .collect();
2705        assert_eq!(edits, vec![(5..5, " world".into())]);
2706    });
2707}
2708
2709fn init_test(cx: &mut TestAppContext) {
2710    cx.update(|cx| {
2711        let settings_store = SettingsStore::test(cx);
2712        cx.set_global(settings_store);
2713    });
2714}
2715
2716async fn apply_edit_prediction(
2717    buffer_content: &str,
2718    completion_response: &str,
2719    cx: &mut TestAppContext,
2720) -> String {
2721    let fs = project::FakeFs::new(cx.executor());
2722    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2723    let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
2724    let (ep_store, response) = make_test_ep_store(&project, cx).await;
2725    *response.lock() = completion_response.to_string();
2726    let edit_prediction = run_edit_prediction(&buffer, &project, &ep_store, cx).await;
2727    buffer.update(cx, |buffer, cx| {
2728        buffer.edit(edit_prediction.edits.iter().cloned(), None, cx)
2729    });
2730    buffer.read_with(cx, |buffer, _| buffer.text())
2731}
2732
2733async fn run_edit_prediction(
2734    buffer: &Entity<Buffer>,
2735    project: &Entity<Project>,
2736    ep_store: &Entity<EditPredictionStore>,
2737    cx: &mut TestAppContext,
2738) -> EditPrediction {
2739    let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
2740    ep_store.update(cx, |ep_store, cx| {
2741        ep_store.register_buffer(buffer, &project, cx)
2742    });
2743    cx.background_executor.run_until_parked();
2744    let prediction_task = ep_store.update(cx, |ep_store, cx| {
2745        ep_store.request_prediction(&project, buffer, cursor, Default::default(), cx)
2746    });
2747    prediction_task.await.unwrap().unwrap().prediction.unwrap()
2748}
2749
2750async fn make_test_ep_store(
2751    project: &Entity<Project>,
2752    cx: &mut TestAppContext,
2753) -> (Entity<EditPredictionStore>, Arc<Mutex<String>>) {
2754    let default_response = "hello world\n".to_string();
2755    let completion_response: Arc<Mutex<String>> = Arc::new(Mutex::new(default_response));
2756    let http_client = FakeHttpClient::create({
2757        let completion_response = completion_response.clone();
2758        let mut next_request_id = 0;
2759        move |req| {
2760            let completion_response = completion_response.clone();
2761            let method = req.method().clone();
2762            let uri = req.uri().path().to_string();
2763            let mut body = req.into_body();
2764            async move {
2765                match (method, uri.as_str()) {
2766                    (Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder()
2767                        .status(200)
2768                        .body(
2769                            serde_json::to_string(&CreateLlmTokenResponse {
2770                                token: LlmToken("the-llm-token".to_string()),
2771                            })
2772                            .unwrap()
2773                            .into(),
2774                        )
2775                        .unwrap()),
2776                    (Method::POST, "/predict_edits/v3") => {
2777                        let mut buf = Vec::new();
2778                        body.read_to_end(&mut buf).await.ok();
2779                        let decompressed = zstd::decode_all(&buf[..]).unwrap();
2780                        let req: PredictEditsV3Request =
2781                            serde_json::from_slice(&decompressed).unwrap();
2782
2783                        next_request_id += 1;
2784                        Ok(http_client::Response::builder()
2785                            .status(200)
2786                            .body(
2787                                serde_json::to_string(&PredictEditsV3Response {
2788                                    request_id: format!("request-{next_request_id}"),
2789                                    editable_range: 0..req.input.cursor_excerpt.len(),
2790                                    output: completion_response.lock().clone(),
2791                                    model_version: None,
2792                                })
2793                                .unwrap()
2794                                .into(),
2795                            )
2796                            .unwrap())
2797                    }
2798                    _ => Ok(http_client::Response::builder()
2799                        .status(404)
2800                        .body("Not Found".to_string().into())
2801                        .unwrap()),
2802                }
2803            }
2804        }
2805    });
2806
2807    let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
2808    let user_store = cx.update(|cx| cx.new(|cx| client::UserStore::new(client.clone(), cx)));
2809    cx.update(|cx| {
2810        RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
2811    });
2812    let _server = FakeServer::for_client(42, &client, cx).await;
2813
2814    let ep_store = cx.new(|cx| {
2815        let mut ep_store = EditPredictionStore::new(client, project.read(cx).user_store(), cx);
2816        ep_store.set_edit_prediction_model(EditPredictionModel::Zeta);
2817
2818        let worktrees = project.read(cx).worktrees(cx).collect::<Vec<_>>();
2819        for worktree in worktrees {
2820            let worktree_id = worktree.read(cx).id();
2821            ep_store
2822                .get_or_init_project(project, cx)
2823                .license_detection_watchers
2824                .entry(worktree_id)
2825                .or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(&worktree, cx)));
2826        }
2827
2828        ep_store
2829    });
2830
2831    (ep_store, completion_response)
2832}
2833
2834fn to_completion_edits(
2835    iterator: impl IntoIterator<Item = (Range<usize>, Arc<str>)>,
2836    buffer: &Entity<Buffer>,
2837    cx: &App,
2838) -> Vec<(Range<Anchor>, Arc<str>)> {
2839    let buffer = buffer.read(cx);
2840    iterator
2841        .into_iter()
2842        .map(|(range, text)| {
2843            (
2844                buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
2845                text,
2846            )
2847        })
2848        .collect()
2849}
2850
2851fn from_completion_edits(
2852    editor_edits: &[(Range<Anchor>, Arc<str>)],
2853    buffer: &Entity<Buffer>,
2854    cx: &App,
2855) -> Vec<(Range<usize>, Arc<str>)> {
2856    let buffer = buffer.read(cx);
2857    editor_edits
2858        .iter()
2859        .map(|(range, text)| {
2860            (
2861                range.start.to_offset(buffer)..range.end.to_offset(buffer),
2862                text.clone(),
2863            )
2864        })
2865        .collect()
2866}
2867
2868#[gpui::test]
2869async fn test_unauthenticated_without_custom_url_blocks_prediction_impl(cx: &mut TestAppContext) {
2870    init_test(cx);
2871
2872    let fs = FakeFs::new(cx.executor());
2873    fs.insert_tree(
2874        "/project",
2875        serde_json::json!({
2876            "main.rs": "fn main() {\n    \n}\n"
2877        }),
2878    )
2879    .await;
2880
2881    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
2882
2883    let http_client = FakeHttpClient::create(|_req| async move {
2884        Ok(gpui::http_client::Response::builder()
2885            .status(401)
2886            .body("Unauthorized".into())
2887            .unwrap())
2888    });
2889
2890    let client =
2891        cx.update(|cx| client::Client::new(Arc::new(FakeSystemClock::new()), http_client, cx));
2892    let user_store = cx.update(|cx| cx.new(|cx| client::UserStore::new(client.clone(), cx)));
2893    cx.update(|cx| {
2894        language_model::RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
2895    });
2896
2897    let ep_store = cx.new(|cx| EditPredictionStore::new(client, project.read(cx).user_store(), cx));
2898
2899    let buffer = project
2900        .update(cx, |project, cx| {
2901            let path = project
2902                .find_project_path(path!("/project/main.rs"), cx)
2903                .unwrap();
2904            project.open_buffer(path, cx)
2905        })
2906        .await
2907        .unwrap();
2908
2909    let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 4)));
2910    ep_store.update(cx, |ep_store, cx| {
2911        ep_store.register_buffer(&buffer, &project, cx)
2912    });
2913    cx.background_executor.run_until_parked();
2914
2915    let completion_task = ep_store.update(cx, |ep_store, cx| {
2916        ep_store.set_edit_prediction_model(EditPredictionModel::Zeta);
2917        ep_store.request_prediction(&project, &buffer, cursor, Default::default(), cx)
2918    });
2919
2920    let result = completion_task.await;
2921    assert!(
2922        result.is_err(),
2923        "Without authentication and without custom URL, prediction should fail"
2924    );
2925}
2926
2927#[gpui::test]
2928async fn test_diagnostic_jump_excludes_collaborator_regions(cx: &mut TestAppContext) {
2929    fn set_collaborator_cursor(buffer: &Entity<Buffer>, row: u32, cx: &mut TestAppContext) {
2930        let collab_replica = clock::ReplicaId::new(10);
2931        let anchor = buffer.read_with(cx, |buffer, _| {
2932            buffer.snapshot().anchor_before(Point::new(row, 0))
2933        });
2934        let selections: Arc<[Selection<Anchor>]> = Arc::new([Selection {
2935            id: 1,
2936            start: anchor,
2937            end: anchor,
2938            reversed: false,
2939            goal: SelectionGoal::None,
2940        }]);
2941        buffer.update(cx, |buffer, cx| {
2942            buffer.apply_ops(
2943                [Operation::UpdateSelections {
2944                    selections,
2945                    lamport_timestamp: clock::Lamport {
2946                        replica_id: collab_replica,
2947                        value: 1,
2948                    },
2949                    line_mode: false,
2950                    cursor_shape: CursorShape::Bar,
2951                }],
2952                cx,
2953            );
2954        });
2955    }
2956
2957    fn publish_diagnostics(
2958        uri_path: &'static str,
2959        rows: &[u32],
2960        project: &Entity<Project>,
2961        cx: &mut TestAppContext,
2962    ) {
2963        let diagnostics: Vec<_> = rows
2964            .iter()
2965            .map(|&row| lsp::Diagnostic {
2966                range: lsp::Range::new(lsp::Position::new(row, 0), lsp::Position::new(row, 5)),
2967                severity: Some(lsp::DiagnosticSeverity::ERROR),
2968                message: format!("error at row {row}"),
2969                ..Default::default()
2970            })
2971            .collect();
2972        project.update(cx, |project, cx| {
2973            project.lsp_store().update(cx, |lsp_store, cx| {
2974                lsp_store
2975                    .update_diagnostics(
2976                        LanguageServerId(0),
2977                        lsp::PublishDiagnosticsParams {
2978                            uri: lsp::Uri::from_file_path(uri_path).expect("invalid uri"),
2979                            diagnostics,
2980                            version: None,
2981                        },
2982                        None,
2983                        language::DiagnosticSourceKind::Pushed,
2984                        &[],
2985                        cx,
2986                    )
2987                    .expect("failed to update diagnostics");
2988            });
2989        });
2990    }
2991
2992    init_test(cx);
2993
2994    let mut lines = String::new();
2995    for i in 0..60 {
2996        lines.push_str(&format!("line {i}\n"));
2997    }
2998
2999    let fs = FakeFs::new(cx.executor());
3000    fs.insert_tree(
3001        "/root",
3002        json!({
3003            "active.txt": lines,
3004            "collab_file.txt": "error here\nsecond line\n",
3005            "free_file.txt": "another error\nsecond line\n",
3006        }),
3007    )
3008    .await;
3009    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
3010
3011    let active_buffer = project
3012        .update(cx, |project, cx| {
3013            let path = project
3014                .find_project_path(path!("/root/active.txt"), cx)
3015                .expect("active.txt not found");
3016            project.set_active_path(Some(path.clone()), cx);
3017            project.open_buffer(path, cx)
3018        })
3019        .await
3020        .expect("failed to open active buffer");
3021
3022    set_collaborator_cursor(&active_buffer, 5, cx);
3023
3024    publish_diagnostics(path!("/root/active.txt"), &[3, 25, 50], &project, cx);
3025
3026    cx.run_until_parked();
3027
3028    let cursor_point = Point::new(25, 0);
3029    let empty_search_range: Range<Point> = Default::default();
3030
3031    let snapshot = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3032    let result = EditPredictionStore::next_diagnostic_location(
3033        active_buffer.clone(),
3034        &snapshot,
3035        empty_search_range.clone(),
3036        cursor_point,
3037        &project,
3038        &mut cx.to_async(),
3039    )
3040    .await
3041    .expect("next_diagnostic_location failed");
3042
3043    let (result_buffer, result_anchor) = result.expect("expected a diagnostic location");
3044    assert_eq!(result_buffer.entity_id(), active_buffer.entity_id());
3045    let result_row = result_buffer.read_with(cx, |buffer, _| {
3046        result_anchor.to_point(&buffer.snapshot()).row
3047    });
3048    assert_ne!(
3049        result_row, 3,
3050        "row 3 is near collaborator (row 5) but far from local cursor (row 25), should be excluded"
3051    );
3052    assert!(
3053        result_row == 25 || result_row == 50,
3054        "expected row 25 or 50, got {result_row}"
3055    );
3056
3057    let snapshot_near = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3058    let near_cursor_point = Point::new(4, 0);
3059    let result_near = EditPredictionStore::next_diagnostic_location(
3060        active_buffer.clone(),
3061        &snapshot_near,
3062        empty_search_range.clone(),
3063        near_cursor_point,
3064        &project,
3065        &mut cx.to_async(),
3066    )
3067    .await
3068    .expect("next_diagnostic_location failed");
3069
3070    let (_, near_anchor) = result_near.expect("expected a diagnostic location when both are near");
3071    let near_row =
3072        active_buffer.read_with(cx, |buffer, _| near_anchor.to_point(&buffer.snapshot()).row);
3073    assert_eq!(
3074        near_row, 3,
3075        "row 3 should be included when local cursor (row 4) is also near the collaborator"
3076    );
3077
3078    let snapshot_far = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3079    let far_cursor_point = Point::new(50, 0);
3080    let result_far = EditPredictionStore::next_diagnostic_location(
3081        active_buffer.clone(),
3082        &snapshot_far,
3083        empty_search_range.clone(),
3084        far_cursor_point,
3085        &project,
3086        &mut cx.to_async(),
3087    )
3088    .await
3089    .expect("next_diagnostic_location failed");
3090
3091    let (_, far_anchor) = result_far.expect("expected a diagnostic location");
3092    let far_row =
3093        active_buffer.read_with(cx, |buffer, _| far_anchor.to_point(&buffer.snapshot()).row);
3094    assert_eq!(
3095        far_row, 50,
3096        "row 50 is near local cursor (row 50) and far from collaborator, should be picked"
3097    );
3098
3099    publish_diagnostics(path!("/root/collab_file.txt"), &[0], &project, cx);
3100    publish_diagnostics(path!("/root/free_file.txt"), &[0], &project, cx);
3101    cx.run_until_parked();
3102
3103    let collab_buffer = project
3104        .update(cx, |project, cx| {
3105            let path = project
3106                .find_project_path(path!("/root/collab_file.txt"), cx)
3107                .expect("collab_file.txt not found");
3108            project.open_buffer(path, cx)
3109        })
3110        .await
3111        .expect("failed to open collab buffer");
3112
3113    set_collaborator_cursor(&collab_buffer, 0, cx);
3114    cx.run_until_parked();
3115
3116    let no_same_file_search_range = Point::new(0, 0)..Point::new(59, 0);
3117    let snapshot_cross = active_buffer.read_with(cx, |buffer, _| buffer.snapshot());
3118    let result_cross = EditPredictionStore::next_diagnostic_location(
3119        active_buffer.clone(),
3120        &snapshot_cross,
3121        no_same_file_search_range,
3122        Point::new(0, 0),
3123        &project,
3124        &mut cx.to_async(),
3125    )
3126    .await
3127    .expect("cross-file next_diagnostic_location failed");
3128
3129    let (cross_buffer, _) = result_cross.expect("expected a cross-file diagnostic location");
3130    let cross_path = cross_buffer.read_with(cx, |buffer, cx| {
3131        buffer
3132            .file()
3133            .expect("buffer should have a file")
3134            .full_path(cx)
3135    });
3136    assert_eq!(
3137        cross_path,
3138        Path::new(path!("root/free_file.txt")),
3139        "should skip collab_file.txt (has collaborator) and pick free_file.txt"
3140    );
3141}
3142
3143#[gpui::test]
3144async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
3145    let (ep_store, _requests) = init_test_with_fake_client(cx);
3146    let fs = FakeFs::new(cx.executor());
3147
3148    // Buffer with two clearly separated regions:
3149    //   Region A = lines 0-9   (offsets 0..50)
3150    //   Region B = lines 20-29 (offsets 105..155)
3151    // A big gap in between so edits in one region never overlap the other.
3152    let mut content = String::new();
3153    for i in 0..30 {
3154        content.push_str(&format!("line {i:02}\n"));
3155    }
3156
3157    fs.insert_tree(
3158        "/root",
3159        json!({
3160            "foo.md": content.clone()
3161        }),
3162    )
3163    .await;
3164    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
3165
3166    let buffer = project
3167        .update(cx, |project, cx| {
3168            let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
3169            project.open_buffer(path, cx)
3170        })
3171        .await
3172        .unwrap();
3173
3174    type SettledEventRecord = (EditPredictionId, String);
3175    let settled_events: Arc<Mutex<Vec<SettledEventRecord>>> = Arc::new(Mutex::new(Vec::new()));
3176
3177    ep_store.update(cx, |ep_store, cx| {
3178        ep_store.register_buffer(&buffer, &project, cx);
3179
3180        let settled_events = settled_events.clone();
3181        ep_store.settled_event_callback = Some(Box::new(move |id, text| {
3182            settled_events.lock().push((id, text));
3183        }));
3184    });
3185
3186    // --- Phase 1: edit in region A and enqueue prediction A ---
3187
3188    buffer.update(cx, |buffer, cx| {
3189        // Edit at the start of line 0.
3190        buffer.edit(vec![(0..0, "ADDED ")], None, cx);
3191    });
3192    cx.run_until_parked();
3193
3194    let snapshot_a = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3195
3196    // Region A: first 10 lines of the buffer.
3197    let editable_region_a = 0..snapshot_a.point_to_offset(Point::new(10, 0));
3198
3199    ep_store.update(cx, |ep_store, cx| {
3200        ep_store.enqueue_settled_prediction(
3201            EditPredictionId("prediction-a".into()),
3202            &project,
3203            &buffer,
3204            &snapshot_a,
3205            editable_region_a.clone(),
3206            None,
3207            Duration::from_secs(0),
3208            cx,
3209        );
3210    });
3211
3212    // --- Phase 2: repeatedly edit in region A to keep it unsettled ---
3213
3214    // Let the worker process the channel message before we start advancing.
3215    cx.run_until_parked();
3216
3217    let mut region_a_edit_offset = 5;
3218    for _ in 0..3 {
3219        // Edit inside region A (not at the boundary) so `last_edit_at` is
3220        // updated before the worker's next wake.
3221        buffer.update(cx, |buffer, cx| {
3222            buffer.edit(
3223                vec![(region_a_edit_offset..region_a_edit_offset, "x")],
3224                None,
3225                cx,
3226            );
3227        });
3228        region_a_edit_offset += 1;
3229        cx.run_until_parked();
3230
3231        cx.executor()
3232            .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 2);
3233        cx.run_until_parked();
3234        assert!(
3235            settled_events.lock().is_empty(),
3236            "no settled events should fire while region A is still being edited"
3237        );
3238    }
3239
3240    // Still nothing settled.
3241    assert!(settled_events.lock().is_empty());
3242
3243    // --- Phase 3: edit in distinct region B, enqueue prediction B ---
3244    // Advance a small amount so B's quiescence window starts later than A's,
3245    // but not so much that A settles (A's last edit was at the start of
3246    // iteration 3, and it needs a full Q to settle).
3247    cx.executor()
3248        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 4);
3249    cx.run_until_parked();
3250    assert!(settled_events.lock().is_empty());
3251
3252    let snapshot_b = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3253    let line_20_offset = snapshot_b.point_to_offset(Point::new(20, 0));
3254
3255    buffer.update(cx, |buffer, cx| {
3256        buffer.edit(vec![(line_20_offset..line_20_offset, "NEW ")], None, cx);
3257    });
3258    cx.run_until_parked();
3259
3260    let snapshot_b2 = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
3261    let editable_region_b = line_20_offset..snapshot_b2.point_to_offset(Point::new(25, 0));
3262
3263    ep_store.update(cx, |ep_store, cx| {
3264        ep_store.enqueue_settled_prediction(
3265            EditPredictionId("prediction-b".into()),
3266            &project,
3267            &buffer,
3268            &snapshot_b2,
3269            editable_region_b.clone(),
3270            None,
3271            Duration::from_secs(0),
3272            cx,
3273        );
3274    });
3275
3276    cx.run_until_parked();
3277    assert!(
3278        settled_events.lock().is_empty(),
3279        "neither prediction should have settled yet"
3280    );
3281
3282    // --- Phase 4: let enough time pass for region A to settle ---
3283    // A's last edit was at T_a (during the last loop iteration). The worker is
3284    // sleeping until T_a + Q. We advance just enough to reach that wake time
3285    // (Q/4 since we already advanced Q/4 in phase 3 on top of the loop's
3286    // 3*Q/2). At that point A has been quiet for Q and settles, but B was
3287    // enqueued only Q/4 ago and stays pending.
3288    cx.executor()
3289        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE / 4);
3290    cx.run_until_parked();
3291
3292    {
3293        let events = settled_events.lock().clone();
3294        assert_eq!(
3295            events.len(),
3296            1,
3297            "prediction and capture_sample for A should have settled, got: {events:?}"
3298        );
3299        assert_eq!(events[0].0, EditPredictionId("prediction-a".into()));
3300    }
3301
3302    // --- Phase 5: let more time pass for region B to settle ---
3303    // B's last edit was Q/4 before A settled. The worker rescheduled to
3304    // B's last_edit_at + Q, which is 3Q/4 from now.
3305    cx.executor()
3306        .advance_clock(EDIT_PREDICTION_SETTLED_QUIESCENCE * 3 / 4);
3307    cx.run_until_parked();
3308
3309    {
3310        let events = settled_events.lock().clone();
3311        assert_eq!(
3312            events.len(),
3313            2,
3314            "both prediction and capture_sample settled events should be emitted for each request, got: {events:?}"
3315        );
3316        assert_eq!(events[1].0, EditPredictionId("prediction-b".into()));
3317    }
3318}
3319
3320#[ctor::ctor]
3321fn init_logger() {
3322    zlog::init_test();
3323}