diagnostics_tests.rs

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