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