diagnostics_tests.rs

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