diagnostics_tests.rs

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