diagnostics_tests.rs

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