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