edit_prediction_tests.rs

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