diagnostics_tests.rs

   1use super::*;
   2use collections::HashMap;
   3use editor::{
   4    display_map::{BlockContext, TransformBlock},
   5    DisplayPoint, GutterDimensions,
   6};
   7use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext};
   8use language::{
   9    Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, PointUtf16, Rope, Unclipped,
  10};
  11use pretty_assertions::assert_eq;
  12use project::FakeFs;
  13use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng};
  14use serde_json::json;
  15use settings::SettingsStore;
  16use std::{env, path::Path};
  17use unindent::Unindent as _;
  18use util::{post_inc, RandomCharIter};
  19
  20#[ctor::ctor]
  21fn init_logger() {
  22    if env::var("RUST_LOG").is_ok() {
  23        env_logger::init();
  24    }
  25}
  26
  27#[gpui::test]
  28async fn test_diagnostics(cx: &mut TestAppContext) {
  29    init_test(cx);
  30
  31    let fs = FakeFs::new(cx.executor());
  32    fs.insert_tree(
  33        "/test",
  34        json!({
  35            "consts.rs": "
  36                const a: i32 = 'a';
  37                const b: i32 = c;
  38            "
  39            .unindent(),
  40
  41            "main.rs": "
  42                fn main() {
  43                    let x = vec![];
  44                    let y = vec![];
  45                    a(x);
  46                    b(y);
  47                    // comment 1
  48                    // comment 2
  49                    c(y);
  50                    d(x);
  51                }
  52            "
  53            .unindent(),
  54        }),
  55    )
  56    .await;
  57
  58    let language_server_id = LanguageServerId(0);
  59    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
  60    let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
  61    let cx = &mut VisualTestContext::from_window(*window, cx);
  62    let workspace = window.root(cx).unwrap();
  63
  64    // Create some diagnostics
  65    project.update(cx, |project, cx| {
  66        project
  67            .update_diagnostic_entries(
  68                language_server_id,
  69                PathBuf::from("/test/main.rs"),
  70                None,
  71                vec![
  72                    DiagnosticEntry {
  73                        range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
  74                        diagnostic: Diagnostic {
  75                            message:
  76                                "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
  77                                    .to_string(),
  78                            severity: DiagnosticSeverity::INFORMATION,
  79                            is_primary: false,
  80                            is_disk_based: true,
  81                            group_id: 1,
  82                            ..Default::default()
  83                        },
  84                    },
  85                    DiagnosticEntry {
  86                        range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
  87                        diagnostic: Diagnostic {
  88                            message:
  89                                "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
  90                                    .to_string(),
  91                            severity: DiagnosticSeverity::INFORMATION,
  92                            is_primary: false,
  93                            is_disk_based: true,
  94                            group_id: 0,
  95                            ..Default::default()
  96                        },
  97                    },
  98                    DiagnosticEntry {
  99                        range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 100                        diagnostic: Diagnostic {
 101                            message: "value moved here".to_string(),
 102                            severity: DiagnosticSeverity::INFORMATION,
 103                            is_primary: false,
 104                            is_disk_based: true,
 105                            group_id: 1,
 106                            ..Default::default()
 107                        },
 108                    },
 109                    DiagnosticEntry {
 110                        range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 111                        diagnostic: Diagnostic {
 112                            message: "value moved here".to_string(),
 113                            severity: DiagnosticSeverity::INFORMATION,
 114                            is_primary: false,
 115                            is_disk_based: true,
 116                            group_id: 0,
 117                            ..Default::default()
 118                        },
 119                    },
 120                    DiagnosticEntry {
 121                        range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 122                        diagnostic: Diagnostic {
 123                            message: "use of moved value\nvalue used here after move".to_string(),
 124                            severity: DiagnosticSeverity::ERROR,
 125                            is_primary: true,
 126                            is_disk_based: true,
 127                            group_id: 0,
 128                            ..Default::default()
 129                        },
 130                    },
 131                    DiagnosticEntry {
 132                        range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 133                        diagnostic: Diagnostic {
 134                            message: "use of moved value\nvalue used here after move".to_string(),
 135                            severity: DiagnosticSeverity::ERROR,
 136                            is_primary: true,
 137                            is_disk_based: true,
 138                            group_id: 1,
 139                            ..Default::default()
 140                        },
 141                    },
 142                ],
 143                cx,
 144            )
 145            .unwrap();
 146    });
 147
 148    // Open the project diagnostics view while there are already diagnostics.
 149    let view = window.build_view(cx, |cx| {
 150        ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
 151    });
 152    let editor = view.update(cx, |view, _| view.editor.clone());
 153
 154    view.next_notification(cx).await;
 155    assert_eq!(
 156        editor_blocks(&editor, cx),
 157        [
 158            (0, "path header block".into()),
 159            (2, "diagnostic header".into()),
 160            (15, "collapsed context".into()),
 161            (16, "diagnostic header".into()),
 162            (25, "collapsed context".into()),
 163        ]
 164    );
 165    assert_eq!(
 166        editor.update(cx, |editor, cx| editor.display_text(cx)),
 167        concat!(
 168            //
 169            // main.rs
 170            //
 171            "\n", // filename
 172            "\n", // padding
 173            // diagnostic group 1
 174            "\n", // primary message
 175            "\n", // padding
 176            "    let x = vec![];\n",
 177            "    let y = vec![];\n",
 178            "\n", // supporting diagnostic
 179            "    a(x);\n",
 180            "    b(y);\n",
 181            "\n", // supporting diagnostic
 182            "    // comment 1\n",
 183            "    // comment 2\n",
 184            "    c(y);\n",
 185            "\n", // supporting diagnostic
 186            "    d(x);\n",
 187            "\n", // context ellipsis
 188            // diagnostic group 2
 189            "\n", // primary message
 190            "\n", // padding
 191            "fn main() {\n",
 192            "    let x = vec![];\n",
 193            "\n", // supporting diagnostic
 194            "    let y = vec![];\n",
 195            "    a(x);\n",
 196            "\n", // supporting diagnostic
 197            "    b(y);\n",
 198            "\n", // context ellipsis
 199            "    c(y);\n",
 200            "    d(x);\n",
 201            "\n", // supporting diagnostic
 202            "}"
 203        )
 204    );
 205
 206    // Cursor is at the first diagnostic
 207    editor.update(cx, |editor, cx| {
 208        assert_eq!(
 209            editor.selections.display_ranges(cx),
 210            [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 211        );
 212    });
 213
 214    // Diagnostics are added for another earlier path.
 215    project.update(cx, |project, cx| {
 216        project.disk_based_diagnostics_started(language_server_id, cx);
 217        project
 218            .update_diagnostic_entries(
 219                language_server_id,
 220                PathBuf::from("/test/consts.rs"),
 221                None,
 222                vec![DiagnosticEntry {
 223                    range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 224                    diagnostic: Diagnostic {
 225                        message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 226                        severity: DiagnosticSeverity::ERROR,
 227                        is_primary: true,
 228                        is_disk_based: true,
 229                        group_id: 0,
 230                        ..Default::default()
 231                    },
 232                }],
 233                cx,
 234            )
 235            .unwrap();
 236        project.disk_based_diagnostics_finished(language_server_id, cx);
 237    });
 238
 239    view.next_notification(cx).await;
 240    assert_eq!(
 241        editor_blocks(&editor, cx),
 242        [
 243            (0, "path header block".into()),
 244            (2, "diagnostic header".into()),
 245            (7, "path header block".into()),
 246            (9, "diagnostic header".into()),
 247            (22, "collapsed context".into()),
 248            (23, "diagnostic header".into()),
 249            (32, "collapsed context".into()),
 250        ]
 251    );
 252
 253    assert_eq!(
 254        editor.update(cx, |editor, cx| editor.display_text(cx)),
 255        concat!(
 256            //
 257            // consts.rs
 258            //
 259            "\n", // filename
 260            "\n", // padding
 261            // diagnostic group 1
 262            "\n", // primary message
 263            "\n", // padding
 264            "const a: i32 = 'a';\n",
 265            "\n", // supporting diagnostic
 266            "const b: i32 = c;\n",
 267            //
 268            // main.rs
 269            //
 270            "\n", // filename
 271            "\n", // padding
 272            // diagnostic group 1
 273            "\n", // primary message
 274            "\n", // padding
 275            "    let x = vec![];\n",
 276            "    let y = vec![];\n",
 277            "\n", // supporting diagnostic
 278            "    a(x);\n",
 279            "    b(y);\n",
 280            "\n", // supporting diagnostic
 281            "    // comment 1\n",
 282            "    // comment 2\n",
 283            "    c(y);\n",
 284            "\n", // supporting diagnostic
 285            "    d(x);\n",
 286            "\n", // collapsed context
 287            // diagnostic group 2
 288            "\n", // primary message
 289            "\n", // filename
 290            "fn main() {\n",
 291            "    let x = vec![];\n",
 292            "\n", // supporting diagnostic
 293            "    let y = vec![];\n",
 294            "    a(x);\n",
 295            "\n", // supporting diagnostic
 296            "    b(y);\n",
 297            "\n", // context ellipsis
 298            "    c(y);\n",
 299            "    d(x);\n",
 300            "\n", // supporting diagnostic
 301            "}"
 302        )
 303    );
 304
 305    // Cursor keeps its position.
 306    editor.update(cx, |editor, cx| {
 307        assert_eq!(
 308            editor.selections.display_ranges(cx),
 309            [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
 310        );
 311    });
 312
 313    // Diagnostics are added to the first path
 314    project.update(cx, |project, cx| {
 315        project.disk_based_diagnostics_started(language_server_id, cx);
 316        project
 317            .update_diagnostic_entries(
 318                language_server_id,
 319                PathBuf::from("/test/consts.rs"),
 320                None,
 321                vec![
 322                    DiagnosticEntry {
 323                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 324                        diagnostic: Diagnostic {
 325                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 326                            severity: DiagnosticSeverity::ERROR,
 327                            is_primary: true,
 328                            is_disk_based: true,
 329                            group_id: 0,
 330                            ..Default::default()
 331                        },
 332                    },
 333                    DiagnosticEntry {
 334                        range: Unclipped(PointUtf16::new(1, 15))..Unclipped(PointUtf16::new(1, 15)),
 335                        diagnostic: Diagnostic {
 336                            message: "unresolved name `c`".to_string(),
 337                            severity: DiagnosticSeverity::ERROR,
 338                            is_primary: true,
 339                            is_disk_based: true,
 340                            group_id: 1,
 341                            ..Default::default()
 342                        },
 343                    },
 344                ],
 345                cx,
 346            )
 347            .unwrap();
 348        project.disk_based_diagnostics_finished(language_server_id, cx);
 349    });
 350
 351    view.next_notification(cx).await;
 352    assert_eq!(
 353        editor_blocks(&editor, cx),
 354        [
 355            (0, "path header block".into()),
 356            (2, "diagnostic header".into()),
 357            (7, "collapsed context".into()),
 358            (8, "diagnostic header".into()),
 359            (13, "path header block".into()),
 360            (15, "diagnostic header".into()),
 361            (28, "collapsed context".into()),
 362            (29, "diagnostic header".into()),
 363            (38, "collapsed context".into()),
 364        ]
 365    );
 366
 367    assert_eq!(
 368        editor.update(cx, |editor, cx| editor.display_text(cx)),
 369        concat!(
 370            //
 371            // consts.rs
 372            //
 373            "\n", // filename
 374            "\n", // padding
 375            // diagnostic group 1
 376            "\n", // primary message
 377            "\n", // padding
 378            "const a: i32 = 'a';\n",
 379            "\n", // supporting diagnostic
 380            "const b: i32 = c;\n",
 381            "\n", // context ellipsis
 382            // diagnostic group 2
 383            "\n", // primary message
 384            "\n", // padding
 385            "const a: i32 = 'a';\n",
 386            "const b: i32 = c;\n",
 387            "\n", // supporting diagnostic
 388            //
 389            // main.rs
 390            //
 391            "\n", // filename
 392            "\n", // padding
 393            // diagnostic group 1
 394            "\n", // primary message
 395            "\n", // padding
 396            "    let x = vec![];\n",
 397            "    let y = vec![];\n",
 398            "\n", // supporting diagnostic
 399            "    a(x);\n",
 400            "    b(y);\n",
 401            "\n", // supporting diagnostic
 402            "    // comment 1\n",
 403            "    // comment 2\n",
 404            "    c(y);\n",
 405            "\n", // supporting diagnostic
 406            "    d(x);\n",
 407            "\n", // context ellipsis
 408            // diagnostic group 2
 409            "\n", // primary message
 410            "\n", // filename
 411            "fn main() {\n",
 412            "    let x = vec![];\n",
 413            "\n", // supporting diagnostic
 414            "    let y = vec![];\n",
 415            "    a(x);\n",
 416            "\n", // supporting diagnostic
 417            "    b(y);\n",
 418            "\n", // context ellipsis
 419            "    c(y);\n",
 420            "    d(x);\n",
 421            "\n", // supporting diagnostic
 422            "}"
 423        )
 424    );
 425}
 426
 427#[gpui::test]
 428async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 429    init_test(cx);
 430
 431    let fs = FakeFs::new(cx.executor());
 432    fs.insert_tree(
 433        "/test",
 434        json!({
 435            "main.js": "
 436                a();
 437                b();
 438                c();
 439                d();
 440                e();
 441            ".unindent()
 442        }),
 443    )
 444    .await;
 445
 446    let server_id_1 = LanguageServerId(100);
 447    let server_id_2 = LanguageServerId(101);
 448    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 449    let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 450    let cx = &mut VisualTestContext::from_window(*window, cx);
 451    let workspace = window.root(cx).unwrap();
 452
 453    let view = window.build_view(cx, |cx| {
 454        ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
 455    });
 456    let editor = view.update(cx, |view, _| view.editor.clone());
 457
 458    // Two language servers start updating diagnostics
 459    project.update(cx, |project, cx| {
 460        project.disk_based_diagnostics_started(server_id_1, cx);
 461        project.disk_based_diagnostics_started(server_id_2, cx);
 462        project
 463            .update_diagnostic_entries(
 464                server_id_1,
 465                PathBuf::from("/test/main.js"),
 466                None,
 467                vec![DiagnosticEntry {
 468                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
 469                    diagnostic: Diagnostic {
 470                        message: "error 1".to_string(),
 471                        severity: DiagnosticSeverity::WARNING,
 472                        is_primary: true,
 473                        is_disk_based: true,
 474                        group_id: 1,
 475                        ..Default::default()
 476                    },
 477                }],
 478                cx,
 479            )
 480            .unwrap();
 481    });
 482
 483    // The first language server finishes
 484    project.update(cx, |project, cx| {
 485        project.disk_based_diagnostics_finished(server_id_1, cx);
 486    });
 487
 488    // Only the first language server's diagnostics are shown.
 489    cx.executor().run_until_parked();
 490    assert_eq!(
 491        editor_blocks(&editor, cx),
 492        [
 493            (0, "path header block".into()),
 494            (2, "diagnostic header".into()),
 495        ]
 496    );
 497    assert_eq!(
 498        editor.update(cx, |editor, cx| editor.display_text(cx)),
 499        concat!(
 500            "\n", // filename
 501            "\n", // padding
 502            // diagnostic group 1
 503            "\n",     // primary message
 504            "\n",     // padding
 505            "a();\n", //
 506            "b();",
 507        )
 508    );
 509
 510    // The second language server finishes
 511    project.update(cx, |project, cx| {
 512        project
 513            .update_diagnostic_entries(
 514                server_id_2,
 515                PathBuf::from("/test/main.js"),
 516                None,
 517                vec![DiagnosticEntry {
 518                    range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
 519                    diagnostic: Diagnostic {
 520                        message: "warning 1".to_string(),
 521                        severity: DiagnosticSeverity::ERROR,
 522                        is_primary: true,
 523                        is_disk_based: true,
 524                        group_id: 2,
 525                        ..Default::default()
 526                    },
 527                }],
 528                cx,
 529            )
 530            .unwrap();
 531        project.disk_based_diagnostics_finished(server_id_2, cx);
 532    });
 533
 534    // Both language server's diagnostics are shown.
 535    cx.executor().run_until_parked();
 536    assert_eq!(
 537        editor_blocks(&editor, cx),
 538        [
 539            (0, "path header block".into()),
 540            (2, "diagnostic header".into()),
 541            (6, "collapsed context".into()),
 542            (7, "diagnostic header".into()),
 543        ]
 544    );
 545    assert_eq!(
 546        editor.update(cx, |editor, cx| editor.display_text(cx)),
 547        concat!(
 548            "\n", // filename
 549            "\n", // padding
 550            // diagnostic group 1
 551            "\n",     // primary message
 552            "\n",     // padding
 553            "a();\n", // location
 554            "b();\n", //
 555            "\n",     // collapsed context
 556            // diagnostic group 2
 557            "\n",     // primary message
 558            "\n",     // padding
 559            "a();\n", // context
 560            "b();\n", //
 561            "c();",   // context
 562        )
 563    );
 564
 565    // Both language servers start updating diagnostics, and the first server finishes.
 566    project.update(cx, |project, cx| {
 567        project.disk_based_diagnostics_started(server_id_1, cx);
 568        project.disk_based_diagnostics_started(server_id_2, cx);
 569        project
 570            .update_diagnostic_entries(
 571                server_id_1,
 572                PathBuf::from("/test/main.js"),
 573                None,
 574                vec![DiagnosticEntry {
 575                    range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
 576                    diagnostic: Diagnostic {
 577                        message: "warning 2".to_string(),
 578                        severity: DiagnosticSeverity::WARNING,
 579                        is_primary: true,
 580                        is_disk_based: true,
 581                        group_id: 1,
 582                        ..Default::default()
 583                    },
 584                }],
 585                cx,
 586            )
 587            .unwrap();
 588        project
 589            .update_diagnostic_entries(
 590                server_id_2,
 591                PathBuf::from("/test/main.rs"),
 592                None,
 593                vec![],
 594                cx,
 595            )
 596            .unwrap();
 597        project.disk_based_diagnostics_finished(server_id_1, cx);
 598    });
 599
 600    // Only the first language server's diagnostics are updated.
 601    cx.executor().run_until_parked();
 602    assert_eq!(
 603        editor_blocks(&editor, cx),
 604        [
 605            (0, "path header block".into()),
 606            (2, "diagnostic header".into()),
 607            (7, "collapsed context".into()),
 608            (8, "diagnostic header".into()),
 609        ]
 610    );
 611    assert_eq!(
 612        editor.update(cx, |editor, cx| editor.display_text(cx)),
 613        concat!(
 614            "\n", // filename
 615            "\n", // padding
 616            // diagnostic group 1
 617            "\n",     // primary message
 618            "\n",     // padding
 619            "a();\n", // location
 620            "b();\n", //
 621            "c();\n", // context
 622            "\n",     // collapsed context
 623            // diagnostic group 2
 624            "\n",     // primary message
 625            "\n",     // padding
 626            "b();\n", // context
 627            "c();\n", //
 628            "d();",   // context
 629        )
 630    );
 631
 632    // The second language server finishes.
 633    project.update(cx, |project, cx| {
 634        project
 635            .update_diagnostic_entries(
 636                server_id_2,
 637                PathBuf::from("/test/main.js"),
 638                None,
 639                vec![DiagnosticEntry {
 640                    range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
 641                    diagnostic: Diagnostic {
 642                        message: "warning 2".to_string(),
 643                        severity: DiagnosticSeverity::WARNING,
 644                        is_primary: true,
 645                        is_disk_based: true,
 646                        group_id: 1,
 647                        ..Default::default()
 648                    },
 649                }],
 650                cx,
 651            )
 652            .unwrap();
 653        project.disk_based_diagnostics_finished(server_id_2, cx);
 654    });
 655
 656    // Both language servers' diagnostics are updated.
 657    cx.executor().run_until_parked();
 658    assert_eq!(
 659        editor_blocks(&editor, cx),
 660        [
 661            (0, "path header block".into()),
 662            (2, "diagnostic header".into()),
 663            (7, "collapsed context".into()),
 664            (8, "diagnostic header".into()),
 665        ]
 666    );
 667    assert_eq!(
 668        editor.update(cx, |editor, cx| editor.display_text(cx)),
 669        concat!(
 670            "\n", // filename
 671            "\n", // padding
 672            // diagnostic group 1
 673            "\n",     // primary message
 674            "\n",     // padding
 675            "b();\n", // location
 676            "c();\n", //
 677            "d();\n", // context
 678            "\n",     // collapsed context
 679            // diagnostic group 2
 680            "\n",     // primary message
 681            "\n",     // padding
 682            "c();\n", // context
 683            "d();\n", //
 684            "e();",   // context
 685        )
 686    );
 687}
 688
 689#[gpui::test(iterations = 20)]
 690async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
 691    init_test(cx);
 692
 693    let operations = env::var("OPERATIONS")
 694        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 695        .unwrap_or(10);
 696
 697    let fs = FakeFs::new(cx.executor());
 698    fs.insert_tree("/test", json!({})).await;
 699
 700    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 701    let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 702    let cx = &mut VisualTestContext::from_window(*window, cx);
 703    let workspace = window.root(cx).unwrap();
 704
 705    let mutated_view = window.build_view(cx, |cx| {
 706        ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
 707    });
 708
 709    workspace.update(cx, |workspace, cx| {
 710        workspace.add_item_to_center(Box::new(mutated_view.clone()), cx);
 711    });
 712    mutated_view.update(cx, |view, cx| {
 713        assert!(view.focus_handle.is_focused(cx));
 714    });
 715
 716    let mut next_group_id = 0;
 717    let mut next_filename = 0;
 718    let mut language_server_ids = vec![LanguageServerId(0)];
 719    let mut updated_language_servers = HashSet::default();
 720    let mut current_diagnostics: HashMap<
 721        (PathBuf, LanguageServerId),
 722        Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
 723    > = Default::default();
 724
 725    for _ in 0..operations {
 726        match rng.gen_range(0..100) {
 727            // language server completes its diagnostic check
 728            0..=20 if !updated_language_servers.is_empty() => {
 729                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 730                log::info!("finishing diagnostic check for language server {server_id}");
 731                project.update(cx, |project, cx| {
 732                    project.disk_based_diagnostics_finished(server_id, cx)
 733                });
 734
 735                if rng.gen_bool(0.5) {
 736                    cx.run_until_parked();
 737                }
 738            }
 739
 740            // language server updates diagnostics
 741            _ => {
 742                let (path, server_id, diagnostics) =
 743                    match current_diagnostics.iter_mut().choose(&mut rng) {
 744                        // update existing set of diagnostics
 745                        Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
 746                            (path.clone(), *server_id, diagnostics)
 747                        }
 748
 749                        // insert a set of diagnostics for a new path
 750                        _ => {
 751                            let path: PathBuf =
 752                                format!("/test/{}.rs", post_inc(&mut next_filename)).into();
 753                            let len = rng.gen_range(128..256);
 754                            let content =
 755                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 756                            fs.insert_file(&path, content.into_bytes()).await;
 757
 758                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 759                                Some(server_id) if rng.gen_bool(0.5) => *server_id,
 760                                _ => {
 761                                    let id = LanguageServerId(language_server_ids.len());
 762                                    language_server_ids.push(id);
 763                                    id
 764                                }
 765                            };
 766
 767                            (
 768                                path.clone(),
 769                                server_id,
 770                                current_diagnostics
 771                                    .entry((path, server_id))
 772                                    .or_insert(vec![]),
 773                            )
 774                        }
 775                    };
 776
 777                updated_language_servers.insert(server_id);
 778
 779                project.update(cx, |project, cx| {
 780                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 781                    randomly_update_diagnostics_for_path(
 782                        &fs,
 783                        &path,
 784                        diagnostics,
 785                        &mut next_group_id,
 786                        &mut rng,
 787                    );
 788                    project
 789                        .update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
 790                        .unwrap()
 791                });
 792
 793                cx.run_until_parked();
 794            }
 795        }
 796    }
 797
 798    log::info!("updating mutated diagnostics view");
 799    mutated_view.update(cx, |view, _| view.enqueue_update_stale_excerpts(None));
 800    cx.run_until_parked();
 801
 802    log::info!("constructing reference diagnostics view");
 803    let reference_view = window.build_view(cx, |cx| {
 804        ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx)
 805    });
 806    cx.run_until_parked();
 807
 808    let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
 809    let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
 810    assert_eq!(mutated_excerpts, reference_excerpts);
 811}
 812
 813fn init_test(cx: &mut TestAppContext) {
 814    cx.update(|cx| {
 815        let settings = SettingsStore::test(cx);
 816        cx.set_global(settings);
 817        theme::init(theme::LoadThemes::JustBase, cx);
 818        language::init(cx);
 819        client::init_settings(cx);
 820        workspace::init_settings(cx);
 821        Project::init_settings(cx);
 822        crate::init(cx);
 823        editor::init(cx);
 824    });
 825}
 826
 827#[derive(Debug, PartialEq, Eq)]
 828struct ExcerptInfo {
 829    path: PathBuf,
 830    range: ExcerptRange<Point>,
 831    group_id: usize,
 832    primary: bool,
 833    language_server: LanguageServerId,
 834}
 835
 836fn get_diagnostics_excerpts(
 837    view: &View<ProjectDiagnosticsEditor>,
 838    cx: &mut VisualTestContext,
 839) -> Vec<ExcerptInfo> {
 840    view.update(cx, |view, cx| {
 841        let mut result = vec![];
 842        let mut excerpt_indices_by_id = HashMap::default();
 843        view.excerpts.update(cx, |multibuffer, cx| {
 844            let snapshot = multibuffer.snapshot(cx);
 845            for (id, buffer, range) in snapshot.excerpts() {
 846                excerpt_indices_by_id.insert(id, result.len());
 847                result.push(ExcerptInfo {
 848                    path: buffer.file().unwrap().path().to_path_buf(),
 849                    range: ExcerptRange {
 850                        context: range.context.to_point(&buffer),
 851                        primary: range.primary.map(|range| range.to_point(&buffer)),
 852                    },
 853                    group_id: usize::MAX,
 854                    primary: false,
 855                    language_server: LanguageServerId(0),
 856                });
 857            }
 858        });
 859
 860        for state in &view.path_states {
 861            for group in &state.diagnostic_groups {
 862                for (ix, excerpt_id) in group.excerpts.iter().enumerate() {
 863                    let excerpt_ix = excerpt_indices_by_id[excerpt_id];
 864                    let excerpt = &mut result[excerpt_ix];
 865                    excerpt.group_id = group.primary_diagnostic.diagnostic.group_id;
 866                    excerpt.language_server = group.language_server_id;
 867                    excerpt.primary = ix == group.primary_excerpt_ix;
 868                }
 869            }
 870        }
 871
 872        result
 873    })
 874}
 875
 876fn randomly_update_diagnostics_for_path(
 877    fs: &FakeFs,
 878    path: &Path,
 879    diagnostics: &mut Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
 880    next_group_id: &mut usize,
 881    rng: &mut impl Rng,
 882) {
 883    let file_content = fs.read_file_sync(path).unwrap();
 884    let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
 885
 886    let mut group_ids = diagnostics
 887        .iter()
 888        .map(|d| d.diagnostic.group_id)
 889        .collect::<HashSet<_>>();
 890
 891    let mutation_count = rng.gen_range(1..=3);
 892    for _ in 0..mutation_count {
 893        if rng.gen_bool(0.5) && !group_ids.is_empty() {
 894            let group_id = *group_ids.iter().choose(rng).unwrap();
 895            log::info!("  removing diagnostic group {group_id}");
 896            diagnostics.retain(|d| d.diagnostic.group_id != group_id);
 897            group_ids.remove(&group_id);
 898        } else {
 899            let group_id = *next_group_id;
 900            *next_group_id += 1;
 901
 902            let mut new_diagnostics = vec![random_diagnostic(rng, &file_text, group_id, true)];
 903            for _ in 0..rng.gen_range(0..=1) {
 904                new_diagnostics.push(random_diagnostic(rng, &file_text, group_id, false));
 905            }
 906
 907            let ix = rng.gen_range(0..=diagnostics.len());
 908            log::info!(
 909                "  inserting diagnostic group {group_id} at index {ix}. ranges: {:?}",
 910                new_diagnostics
 911                    .iter()
 912                    .map(|d| (d.range.start.0, d.range.end.0))
 913                    .collect::<Vec<_>>()
 914            );
 915            diagnostics.splice(ix..ix, new_diagnostics);
 916        }
 917    }
 918}
 919
 920fn random_diagnostic(
 921    rng: &mut impl Rng,
 922    file_text: &Rope,
 923    group_id: usize,
 924    is_primary: bool,
 925) -> DiagnosticEntry<Unclipped<PointUtf16>> {
 926    // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
 927    // because language servers can potentially give us those, and we should handle them gracefully.
 928    const ERROR_MARGIN: usize = 10;
 929
 930    let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
 931    let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
 932    let range = Range {
 933        start: Unclipped(file_text.offset_to_point_utf16(start)),
 934        end: Unclipped(file_text.offset_to_point_utf16(end)),
 935    };
 936    let severity = if rng.gen_bool(0.5) {
 937        DiagnosticSeverity::WARNING
 938    } else {
 939        DiagnosticSeverity::ERROR
 940    };
 941    let message = format!("diagnostic group {group_id}");
 942
 943    DiagnosticEntry {
 944        range,
 945        diagnostic: Diagnostic {
 946            source: None, // (optional) service that created the diagnostic
 947            code: None,   // (optional) machine-readable code that identifies the diagnostic
 948            severity,
 949            message,
 950            group_id,
 951            is_primary,
 952            is_disk_based: false,
 953            is_unnecessary: false,
 954        },
 955    }
 956}
 957
 958fn editor_blocks(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<(u32, SharedString)> {
 959    let mut blocks = Vec::new();
 960    cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
 961        editor.update(cx, |editor, cx| {
 962            let snapshot = editor.snapshot(cx);
 963            blocks.extend(
 964                snapshot
 965                    .blocks_in_range(0..snapshot.max_point().row())
 966                    .enumerate()
 967                    .filter_map(|(ix, (row, block))| {
 968                        let name: SharedString = match block {
 969                            TransformBlock::Custom(block) => {
 970                                let mut element = block.render(&mut BlockContext {
 971                                    context: cx,
 972                                    anchor_x: px(0.),
 973                                    gutter_dimensions: &GutterDimensions::default(),
 974                                    line_height: px(0.),
 975                                    em_width: px(0.),
 976                                    max_width: px(0.),
 977                                    block_id: ix,
 978                                    editor_style: &editor::EditorStyle::default(),
 979                                });
 980                                let element = element.downcast_mut::<Stateful<Div>>().unwrap();
 981                                element
 982                                    .interactivity()
 983                                    .element_id
 984                                    .clone()?
 985                                    .try_into()
 986                                    .ok()?
 987                            }
 988
 989                            TransformBlock::ExcerptHeader {
 990                                starts_new_buffer, ..
 991                            } => {
 992                                if *starts_new_buffer {
 993                                    "path header block".into()
 994                                } else {
 995                                    "collapsed context".into()
 996                                }
 997                            }
 998                        };
 999
1000                        Some((row, name))
1001                    }),
1002            )
1003        });
1004
1005        div().into_any()
1006    });
1007    blocks
1008}