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), FILE_HEADER.into()),
 162            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 163            (DisplayRow(15), EXCERPT_HEADER.into()),
 164            (DisplayRow(16), DIAGNOSTIC_HEADER.into()),
 165            (DisplayRow(25), EXCERPT_HEADER.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), FILE_HEADER.into()),
 247            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 248            (DisplayRow(7), FILE_HEADER.into()),
 249            (DisplayRow(9), DIAGNOSTIC_HEADER.into()),
 250            (DisplayRow(22), EXCERPT_HEADER.into()),
 251            (DisplayRow(23), DIAGNOSTIC_HEADER.into()),
 252            (DisplayRow(32), EXCERPT_HEADER.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), FILE_HEADER.into()),
 359            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 360            (DisplayRow(7), EXCERPT_HEADER.into()),
 361            (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
 362            (DisplayRow(13), FILE_HEADER.into()),
 363            (DisplayRow(15), DIAGNOSTIC_HEADER.into()),
 364            (DisplayRow(28), EXCERPT_HEADER.into()),
 365            (DisplayRow(29), DIAGNOSTIC_HEADER.into()),
 366            (DisplayRow(38), EXCERPT_HEADER.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), FILE_HEADER.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), FILE_HEADER.into()),
 543            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 544            (DisplayRow(6), EXCERPT_HEADER.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), FILE_HEADER.into()),
 609            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 610            (DisplayRow(7), EXCERPT_HEADER.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), FILE_HEADER.into()),
 665            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
 666            (DisplayRow(7), EXCERPT_HEADER.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
 961const FILE_HEADER: &'static str = "file header";
 962const EXCERPT_HEADER: &'static str = "excerpt header";
 963const EXCERPT_FOOTER: &'static str = "excerpt footer";
 964
 965fn editor_blocks(
 966    editor: &View<Editor>,
 967    cx: &mut VisualTestContext,
 968) -> Vec<(DisplayRow, SharedString)> {
 969    let mut blocks = Vec::new();
 970    cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
 971        editor.update(cx, |editor, cx| {
 972            let snapshot = editor.snapshot(cx);
 973            blocks.extend(
 974                snapshot
 975                    .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
 976                    .enumerate()
 977                    .filter_map(|(ix, (row, block))| {
 978                        let name: SharedString = match block {
 979                            TransformBlock::Custom(block) => {
 980                                let mut element = block.render(&mut BlockContext {
 981                                    context: cx,
 982                                    anchor_x: px(0.),
 983                                    gutter_dimensions: &GutterDimensions::default(),
 984                                    line_height: px(0.),
 985                                    em_width: px(0.),
 986                                    max_width: px(0.),
 987                                    block_id: ix,
 988                                    editor_style: &editor::EditorStyle::default(),
 989                                });
 990                                let element = element.downcast_mut::<Stateful<Div>>().unwrap();
 991                                element
 992                                    .interactivity()
 993                                    .element_id
 994                                    .clone()?
 995                                    .try_into()
 996                                    .ok()?
 997                            }
 998
 999                            TransformBlock::ExcerptHeader {
1000                                starts_new_buffer, ..
1001                            } => {
1002                                if *starts_new_buffer {
1003                                    FILE_HEADER.into()
1004                                } else {
1005                                    EXCERPT_HEADER.into()
1006                                }
1007                            }
1008                            TransformBlock::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
1009                        };
1010
1011                        Some((row, name))
1012                    }),
1013            )
1014        });
1015
1016        div().into_any()
1017    });
1018    blocks
1019}