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