diagnostics_tests.rs

   1use super::*;
   2use collections::{HashMap, HashSet};
   3use editor::{
   4    DisplayPoint, InlayId,
   5    actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
   6    display_map::{DisplayRow, Inlay},
   7    test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
   8};
   9use gpui::{TestAppContext, VisualTestContext};
  10use indoc::indoc;
  11use language::Rope;
  12use lsp::LanguageServerId;
  13use pretty_assertions::assert_eq;
  14use project::FakeFs;
  15use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
  16use serde_json::json;
  17use settings::SettingsStore;
  18use std::{
  19    env,
  20    path::{Path, PathBuf},
  21};
  22use unindent::Unindent as _;
  23use util::{RandomCharIter, path, post_inc};
  24
  25#[ctor::ctor]
  26fn init_logger() {
  27    if env::var("RUST_LOG").is_ok() {
  28        env_logger::init();
  29    }
  30}
  31
  32#[gpui::test]
  33async fn test_diagnostics(cx: &mut TestAppContext) {
  34    init_test(cx);
  35
  36    let fs = FakeFs::new(cx.executor());
  37    fs.insert_tree(
  38        path!("/test"),
  39        json!({
  40            "consts.rs": "
  41                const a: i32 = 'a';
  42                const b: i32 = c;
  43            "
  44            .unindent(),
  45
  46            "main.rs": "
  47                fn main() {
  48                    let x = vec![];
  49                    let y = vec![];
  50                    a(x);
  51                    b(y);
  52                    // comment 1
  53                    // comment 2
  54                    c(y);
  55                    d(x);
  56                }
  57            "
  58            .unindent(),
  59        }),
  60    )
  61    .await;
  62
  63    let language_server_id = LanguageServerId(0);
  64    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
  65    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
  66    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
  67    let cx = &mut VisualTestContext::from_window(*window, cx);
  68    let workspace = window.root(cx).unwrap();
  69    let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
  70
  71    // Create some diagnostics
  72    lsp_store.update(cx, |lsp_store, cx| {
  73        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
  74            uri: uri.clone(),
  75            diagnostics: vec![lsp::Diagnostic{
  76                range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
  77                severity:Some(lsp::DiagnosticSeverity::ERROR),
  78                message: "use of moved value\nvalue used here after move".to_string(),
  79                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
  80                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
  81                    message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
  82                },
  83                lsp::DiagnosticRelatedInformation {
  84                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
  85                    message: "value moved here".to_string()
  86                },
  87                ]),
  88                ..Default::default()
  89            },
  90            lsp::Diagnostic{
  91                range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
  92                severity:Some(lsp::DiagnosticSeverity::ERROR),
  93                message: "use of moved value\nvalue used here after move".to_string(),
  94                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
  95                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
  96                    message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
  97                },
  98                lsp::DiagnosticRelatedInformation {
  99                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
 100                    message: "value moved here".to_string()
 101                },
 102                ]),
 103                ..Default::default()
 104            }
 105            ],
 106            version: None
 107        }, &[], cx).unwrap();
 108    });
 109
 110    // Open the project diagnostics view while there are already diagnostics.
 111    let diagnostics = window.build_entity(cx, |window, cx| {
 112        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 113    });
 114    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 115
 116    diagnostics
 117        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
 118        .await;
 119
 120    pretty_assertions::assert_eq!(
 121        editor_content_with_blocks(&editor, cx),
 122        indoc::indoc! {
 123            "§ main.rs
 124             § -----
 125             fn main() {
 126                 let x = vec![];
 127             § move occurs because `x` has type `Vec<char>`, which does not implement
 128             § the `Copy` trait (back)
 129                 let y = vec![];
 130             § move occurs because `y` has type `Vec<char>`, which does not implement
 131             § the `Copy` trait (back)
 132                 a(x); § value moved here (back)
 133                 b(y); § value moved here
 134                 // comment 1
 135                 // comment 2
 136                 c(y);
 137             § use of moved value value used here after move
 138             § hint: move occurs because `y` has type `Vec<char>`, which does not
 139             § implement the `Copy` trait
 140                 d(x);
 141             § use of moved value value used here after move
 142             § hint: move occurs because `x` has type `Vec<char>`, which does not
 143             § implement the `Copy` trait
 144             § hint: value moved here
 145             }"
 146        }
 147    );
 148
 149    // Cursor is at the first diagnostic
 150    editor.update(cx, |editor, cx| {
 151        assert_eq!(
 152            editor.selections.display_ranges(cx),
 153            [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
 154        );
 155    });
 156
 157    // Diagnostics are added for another earlier path.
 158    lsp_store.update(cx, |lsp_store, cx| {
 159        lsp_store.disk_based_diagnostics_started(language_server_id, cx);
 160        lsp_store
 161            .update_diagnostics(
 162                language_server_id,
 163                lsp::PublishDiagnosticsParams {
 164                    uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
 165                    diagnostics: vec![lsp::Diagnostic {
 166                        range: lsp::Range::new(
 167                            lsp::Position::new(0, 15),
 168                            lsp::Position::new(0, 15),
 169                        ),
 170                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 171                        message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 172                        ..Default::default()
 173                    }],
 174                    version: None,
 175                },
 176                &[],
 177                cx,
 178            )
 179            .unwrap();
 180        lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
 181    });
 182
 183    diagnostics
 184        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
 185        .await;
 186
 187    pretty_assertions::assert_eq!(
 188        editor_content_with_blocks(&editor, cx),
 189        indoc::indoc! {
 190            "§ consts.rs
 191             § -----
 192             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
 193             const b: i32 = c;
 194
 195             § main.rs
 196             § -----
 197             fn main() {
 198                 let x = vec![];
 199             § move occurs because `x` has type `Vec<char>`, which does not implement
 200             § the `Copy` trait (back)
 201                 let y = vec![];
 202             § move occurs because `y` has type `Vec<char>`, which does not implement
 203             § the `Copy` trait (back)
 204                 a(x); § value moved here (back)
 205                 b(y); § value moved here
 206                 // comment 1
 207                 // comment 2
 208                 c(y);
 209             § use of moved value value used here after move
 210             § hint: move occurs because `y` has type `Vec<char>`, which does not
 211             § implement the `Copy` trait
 212                 d(x);
 213             § use of moved value value used here after move
 214             § hint: move occurs because `x` has type `Vec<char>`, which does not
 215             § implement the `Copy` trait
 216             § hint: value moved here
 217             }"
 218        }
 219    );
 220
 221    // Cursor keeps its position.
 222    editor.update(cx, |editor, cx| {
 223        assert_eq!(
 224            editor.selections.display_ranges(cx),
 225            [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
 226        );
 227    });
 228
 229    // Diagnostics are added to the first 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_diagnostics(
 234                language_server_id,
 235                lsp::PublishDiagnosticsParams {
 236                    uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
 237                    diagnostics: vec![
 238                        lsp::Diagnostic {
 239                            range: lsp::Range::new(
 240                                lsp::Position::new(0, 15),
 241                                lsp::Position::new(0, 15),
 242                            ),
 243                            severity: Some(lsp::DiagnosticSeverity::ERROR),
 244                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 245                            ..Default::default()
 246                        },
 247                        lsp::Diagnostic {
 248                            range: lsp::Range::new(
 249                                lsp::Position::new(1, 15),
 250                                lsp::Position::new(1, 15),
 251                            ),
 252                            severity: Some(lsp::DiagnosticSeverity::ERROR),
 253                            message: "unresolved name `c`".to_string(),
 254                            ..Default::default()
 255                        },
 256                    ],
 257                    version: None,
 258                },
 259                &[],
 260                cx,
 261            )
 262            .unwrap();
 263        lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
 264    });
 265
 266    diagnostics
 267        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
 268        .await;
 269
 270    pretty_assertions::assert_eq!(
 271        editor_content_with_blocks(&editor, cx),
 272        indoc::indoc! {
 273            "§ consts.rs
 274             § -----
 275             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
 276             const b: i32 = c; § unresolved name `c`
 277
 278             § main.rs
 279             § -----
 280             fn main() {
 281                 let x = vec![];
 282             § move occurs because `x` has type `Vec<char>`, which does not implement
 283             § the `Copy` trait (back)
 284                 let y = vec![];
 285             § move occurs because `y` has type `Vec<char>`, which does not implement
 286             § the `Copy` trait (back)
 287                 a(x); § value moved here (back)
 288                 b(y); § value moved here
 289                 // comment 1
 290                 // comment 2
 291                 c(y);
 292             § use of moved value value used here after move
 293             § hint: move occurs because `y` has type `Vec<char>`, which does not
 294             § implement the `Copy` trait
 295                 d(x);
 296             § use of moved value value used here after move
 297             § hint: move occurs because `x` has type `Vec<char>`, which does not
 298             § implement the `Copy` trait
 299             § hint: value moved here
 300             }"
 301        }
 302    );
 303}
 304
 305#[gpui::test]
 306async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
 307    init_test(cx);
 308
 309    let fs = FakeFs::new(cx.executor());
 310    fs.insert_tree(
 311        path!("/test"),
 312        json!({
 313            "main.js": "
 314            function test() {
 315                return 1
 316            };
 317
 318            tset();
 319            ".unindent()
 320        }),
 321    )
 322    .await;
 323
 324    let server_id_1 = LanguageServerId(100);
 325    let server_id_2 = LanguageServerId(101);
 326    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 327    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 328    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 329    let cx = &mut VisualTestContext::from_window(*window, cx);
 330    let workspace = window.root(cx).unwrap();
 331
 332    let diagnostics = window.build_entity(cx, |window, cx| {
 333        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 334    });
 335    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 336
 337    // Two language servers start updating diagnostics
 338    lsp_store.update(cx, |lsp_store, cx| {
 339        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 340        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 341        lsp_store
 342            .update_diagnostics(
 343                server_id_1,
 344                lsp::PublishDiagnosticsParams {
 345                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 346                    diagnostics: vec![lsp::Diagnostic {
 347                        range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
 348                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 349                        message: "no method `tset`".to_string(),
 350                        related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 351                            location: lsp::Location::new(
 352                                lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 353                                lsp::Range::new(
 354                                    lsp::Position::new(0, 9),
 355                                    lsp::Position::new(0, 13),
 356                                ),
 357                            ),
 358                            message: "method `test` defined here".to_string(),
 359                        }]),
 360                        ..Default::default()
 361                    }],
 362                    version: None,
 363                },
 364                &[],
 365                cx,
 366            )
 367            .unwrap();
 368    });
 369
 370    // The first language server finishes
 371    lsp_store.update(cx, |lsp_store, cx| {
 372        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 373    });
 374
 375    // Only the first language server's diagnostics are shown.
 376    cx.executor()
 377        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 378    cx.executor().run_until_parked();
 379    editor.update_in(cx, |editor, window, cx| {
 380        editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
 381    });
 382
 383    pretty_assertions::assert_eq!(
 384        editor_content_with_blocks(&editor, cx),
 385        indoc::indoc! {
 386            "§ main.js
 387             § -----
 388 389
 390             tset(); § no method `tset`"
 391        }
 392    );
 393
 394    editor.update(cx, |editor, cx| {
 395        editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
 396    });
 397
 398    pretty_assertions::assert_eq!(
 399        editor_content_with_blocks(&editor, cx),
 400        indoc::indoc! {
 401            "§ main.js
 402             § -----
 403             function test() { § method `test` defined here
 404                 return 1
 405             };
 406
 407             tset(); § no method `tset`"
 408        }
 409    );
 410}
 411
 412#[gpui::test]
 413async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 414    init_test(cx);
 415
 416    let fs = FakeFs::new(cx.executor());
 417    fs.insert_tree(
 418        path!("/test"),
 419        json!({
 420            "main.js": "
 421                a();
 422                b();
 423                c();
 424                d();
 425                e();
 426            ".unindent()
 427        }),
 428    )
 429    .await;
 430
 431    let server_id_1 = LanguageServerId(100);
 432    let server_id_2 = LanguageServerId(101);
 433    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 434    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 435    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 436    let cx = &mut VisualTestContext::from_window(*window, cx);
 437    let workspace = window.root(cx).unwrap();
 438
 439    let diagnostics = window.build_entity(cx, |window, cx| {
 440        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 441    });
 442    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 443
 444    // Two language servers start updating diagnostics
 445    lsp_store.update(cx, |lsp_store, cx| {
 446        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 447        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 448        lsp_store
 449            .update_diagnostics(
 450                server_id_1,
 451                lsp::PublishDiagnosticsParams {
 452                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 453                    diagnostics: vec![lsp::Diagnostic {
 454                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
 455                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 456                        message: "error 1".to_string(),
 457                        ..Default::default()
 458                    }],
 459                    version: None,
 460                },
 461                &[],
 462                cx,
 463            )
 464            .unwrap();
 465    });
 466
 467    // The first language server finishes
 468    lsp_store.update(cx, |lsp_store, cx| {
 469        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 470    });
 471
 472    // Only the first language server's diagnostics are shown.
 473    cx.executor()
 474        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 475    cx.executor().run_until_parked();
 476
 477    pretty_assertions::assert_eq!(
 478        editor_content_with_blocks(&editor, cx),
 479        indoc::indoc! {
 480            "§ main.js
 481             § -----
 482             a(); § error 1
 483             b();
 484             c();"
 485        }
 486    );
 487
 488    // The second language server finishes
 489    lsp_store.update(cx, |lsp_store, cx| {
 490        lsp_store
 491            .update_diagnostics(
 492                server_id_2,
 493                lsp::PublishDiagnosticsParams {
 494                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 495                    diagnostics: vec![lsp::Diagnostic {
 496                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
 497                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 498                        message: "warning 1".to_string(),
 499                        ..Default::default()
 500                    }],
 501                    version: None,
 502                },
 503                &[],
 504                cx,
 505            )
 506            .unwrap();
 507        lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
 508    });
 509
 510    // Both language server's diagnostics are shown.
 511    cx.executor()
 512        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 513    cx.executor().run_until_parked();
 514
 515    pretty_assertions::assert_eq!(
 516        editor_content_with_blocks(&editor, cx),
 517        indoc::indoc! {
 518            "§ main.js
 519             § -----
 520             a(); § error 1
 521             b(); § warning 1
 522             c();
 523             d();"
 524        }
 525    );
 526
 527    // Both language servers start updating diagnostics, and the first server finishes.
 528    lsp_store.update(cx, |lsp_store, cx| {
 529        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 530        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 531        lsp_store
 532            .update_diagnostics(
 533                server_id_1,
 534                lsp::PublishDiagnosticsParams {
 535                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 536                    diagnostics: vec![lsp::Diagnostic {
 537                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
 538                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 539                        message: "warning 2".to_string(),
 540                        ..Default::default()
 541                    }],
 542                    version: None,
 543                },
 544                &[],
 545                cx,
 546            )
 547            .unwrap();
 548        lsp_store
 549            .update_diagnostics(
 550                server_id_2,
 551                lsp::PublishDiagnosticsParams {
 552                    uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
 553                    diagnostics: vec![],
 554                    version: None,
 555                },
 556                &[],
 557                cx,
 558            )
 559            .unwrap();
 560        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 561    });
 562
 563    // Only the first language server's diagnostics are updated.
 564    cx.executor()
 565        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 566    cx.executor().run_until_parked();
 567
 568    pretty_assertions::assert_eq!(
 569        editor_content_with_blocks(&editor, cx),
 570        indoc::indoc! {
 571            "§ main.js
 572             § -----
 573             a();
 574             b(); § warning 1
 575             c(); § warning 2
 576             d();
 577             e();"
 578        }
 579    );
 580
 581    // The second language server finishes.
 582    lsp_store.update(cx, |lsp_store, cx| {
 583        lsp_store
 584            .update_diagnostics(
 585                server_id_2,
 586                lsp::PublishDiagnosticsParams {
 587                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
 588                    diagnostics: vec![lsp::Diagnostic {
 589                        range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
 590                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 591                        message: "warning 2".to_string(),
 592                        ..Default::default()
 593                    }],
 594                    version: None,
 595                },
 596                &[],
 597                cx,
 598            )
 599            .unwrap();
 600        lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
 601    });
 602
 603    // Both language servers' diagnostics are updated.
 604    cx.executor()
 605        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 606    cx.executor().run_until_parked();
 607
 608    pretty_assertions::assert_eq!(
 609        editor_content_with_blocks(&editor, cx),
 610        indoc::indoc! {
 611            "§ main.js
 612                 § -----
 613                 a();
 614                 b();
 615                 c(); § warning 2
 616                 d(); § warning 2
 617                 e();"
 618        }
 619    );
 620}
 621
 622#[gpui::test(iterations = 20)]
 623async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
 624    init_test(cx);
 625
 626    let operations = env::var("OPERATIONS")
 627        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 628        .unwrap_or(10);
 629
 630    let fs = FakeFs::new(cx.executor());
 631    fs.insert_tree(path!("/test"), json!({})).await;
 632
 633    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 634    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 635    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 636    let cx = &mut VisualTestContext::from_window(*window, cx);
 637    let workspace = window.root(cx).unwrap();
 638
 639    let mutated_diagnostics = window.build_entity(cx, |window, cx| {
 640        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 641    });
 642
 643    workspace.update_in(cx, |workspace, window, cx| {
 644        workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
 645    });
 646    mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
 647        assert!(diagnostics.focus_handle.is_focused(window));
 648    });
 649
 650    let mut next_id = 0;
 651    let mut next_filename = 0;
 652    let mut language_server_ids = vec![LanguageServerId(0)];
 653    let mut updated_language_servers = HashSet::default();
 654    let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
 655        Default::default();
 656
 657    for _ in 0..operations {
 658        match rng.gen_range(0..100) {
 659            // language server completes its diagnostic check
 660            0..=20 if !updated_language_servers.is_empty() => {
 661                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 662                log::info!("finishing diagnostic check for language server {server_id}");
 663                lsp_store.update(cx, |lsp_store, cx| {
 664                    lsp_store.disk_based_diagnostics_finished(server_id, cx)
 665                });
 666
 667                if rng.gen_bool(0.5) {
 668                    cx.run_until_parked();
 669                }
 670            }
 671
 672            // language server updates diagnostics
 673            _ => {
 674                let (path, server_id, diagnostics) =
 675                    match current_diagnostics.iter_mut().choose(&mut rng) {
 676                        // update existing set of diagnostics
 677                        Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
 678                            (path.clone(), *server_id, diagnostics)
 679                        }
 680
 681                        // insert a set of diagnostics for a new path
 682                        _ => {
 683                            let path: PathBuf =
 684                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
 685                            let len = rng.gen_range(128..256);
 686                            let content =
 687                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 688                            fs.insert_file(&path, content.into_bytes()).await;
 689
 690                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 691                                Some(server_id) if rng.gen_bool(0.5) => *server_id,
 692                                _ => {
 693                                    let id = LanguageServerId(language_server_ids.len());
 694                                    language_server_ids.push(id);
 695                                    id
 696                                }
 697                            };
 698
 699                            (
 700                                path.clone(),
 701                                server_id,
 702                                current_diagnostics.entry((path, server_id)).or_default(),
 703                            )
 704                        }
 705                    };
 706
 707                updated_language_servers.insert(server_id);
 708
 709                lsp_store.update(cx, |lsp_store, cx| {
 710                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 711                    randomly_update_diagnostics_for_path(
 712                        &fs,
 713                        &path,
 714                        diagnostics,
 715                        &mut next_id,
 716                        &mut rng,
 717                    );
 718                    lsp_store
 719                        .update_diagnostics(
 720                            server_id,
 721                            lsp::PublishDiagnosticsParams {
 722                                uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
 723                                    lsp::Url::parse("file:///test/fallback.rs").unwrap()
 724                                }),
 725                                diagnostics: diagnostics.clone(),
 726                                version: None,
 727                            },
 728                            &[],
 729                            cx,
 730                        )
 731                        .unwrap()
 732                });
 733                cx.executor()
 734                    .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 735
 736                cx.run_until_parked();
 737            }
 738        }
 739    }
 740
 741    log::info!("updating mutated diagnostics view");
 742    mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 743        diagnostics.update_stale_excerpts(window, cx)
 744    });
 745
 746    log::info!("constructing reference diagnostics view");
 747    let reference_diagnostics = window.build_entity(cx, |window, cx| {
 748        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 749    });
 750    cx.executor()
 751        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 752    cx.run_until_parked();
 753
 754    let mutated_excerpts =
 755        editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx);
 756    let reference_excerpts = editor_content_with_blocks(
 757        &reference_diagnostics.update(cx, |d, _| d.editor.clone()),
 758        cx,
 759    );
 760
 761    // The mutated view may contain more than the reference view as
 762    // we don't currently shrink excerpts when diagnostics were removed.
 763    let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
 764    let mut next_ref_line = ref_iter.next();
 765    let mut skipped_block = false;
 766
 767    for mut_line in mutated_excerpts.lines() {
 768        if let Some(ref_line) = next_ref_line {
 769            if mut_line == ref_line {
 770                next_ref_line = ref_iter.next();
 771            } else if mut_line.contains('§') && mut_line != "§ -----" {
 772                skipped_block = true;
 773            }
 774        }
 775    }
 776
 777    if next_ref_line.is_some() || skipped_block {
 778        pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
 779    }
 780}
 781
 782// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
 783#[gpui::test]
 784async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
 785    init_test(cx);
 786
 787    let operations = env::var("OPERATIONS")
 788        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 789        .unwrap_or(10);
 790
 791    let fs = FakeFs::new(cx.executor());
 792    fs.insert_tree(path!("/test"), json!({})).await;
 793
 794    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 795    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 796    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 797    let cx = &mut VisualTestContext::from_window(*window, cx);
 798    let workspace = window.root(cx).unwrap();
 799
 800    let mutated_diagnostics = window.build_entity(cx, |window, cx| {
 801        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 802    });
 803
 804    workspace.update_in(cx, |workspace, window, cx| {
 805        workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
 806    });
 807    mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
 808        assert!(diagnostics.focus_handle.is_focused(window));
 809    });
 810
 811    let mut next_id = 0;
 812    let mut next_filename = 0;
 813    let mut language_server_ids = vec![LanguageServerId(0)];
 814    let mut updated_language_servers = HashSet::default();
 815    let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
 816        Default::default();
 817    let mut next_inlay_id = 0;
 818
 819    for _ in 0..operations {
 820        match rng.gen_range(0..100) {
 821            // language server completes its diagnostic check
 822            0..=20 if !updated_language_servers.is_empty() => {
 823                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 824                log::info!("finishing diagnostic check for language server {server_id}");
 825                lsp_store.update(cx, |lsp_store, cx| {
 826                    lsp_store.disk_based_diagnostics_finished(server_id, cx)
 827                });
 828
 829                if rng.gen_bool(0.5) {
 830                    cx.run_until_parked();
 831                }
 832            }
 833
 834            21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 835                diagnostics.editor.update(cx, |editor, cx| {
 836                    let snapshot = editor.snapshot(window, cx);
 837                    if snapshot.buffer_snapshot.len() > 0 {
 838                        let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
 839                        let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
 840                        log::info!(
 841                            "adding inlay at {position}/{}: {:?}",
 842                            snapshot.buffer_snapshot.len(),
 843                            snapshot.buffer_snapshot.text(),
 844                        );
 845
 846                        editor.splice_inlays(
 847                            &[],
 848                            vec![Inlay {
 849                                id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
 850                                position: snapshot.buffer_snapshot.anchor_before(position),
 851                                text: Rope::from(format!("Test inlay {next_inlay_id}")),
 852                            }],
 853                            cx,
 854                        );
 855                    }
 856                });
 857            }),
 858
 859            // language server updates diagnostics
 860            _ => {
 861                let (path, server_id, diagnostics) =
 862                    match current_diagnostics.iter_mut().choose(&mut rng) {
 863                        // update existing set of diagnostics
 864                        Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
 865                            (path.clone(), *server_id, diagnostics)
 866                        }
 867
 868                        // insert a set of diagnostics for a new path
 869                        _ => {
 870                            let path: PathBuf =
 871                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
 872                            let len = rng.gen_range(128..256);
 873                            let content =
 874                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 875                            fs.insert_file(&path, content.into_bytes()).await;
 876
 877                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 878                                Some(server_id) if rng.gen_bool(0.5) => *server_id,
 879                                _ => {
 880                                    let id = LanguageServerId(language_server_ids.len());
 881                                    language_server_ids.push(id);
 882                                    id
 883                                }
 884                            };
 885
 886                            (
 887                                path.clone(),
 888                                server_id,
 889                                current_diagnostics.entry((path, server_id)).or_default(),
 890                            )
 891                        }
 892                    };
 893
 894                updated_language_servers.insert(server_id);
 895
 896                lsp_store.update(cx, |lsp_store, cx| {
 897                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 898                    randomly_update_diagnostics_for_path(
 899                        &fs,
 900                        &path,
 901                        diagnostics,
 902                        &mut next_id,
 903                        &mut rng,
 904                    );
 905                    lsp_store
 906                        .update_diagnostics(
 907                            server_id,
 908                            lsp::PublishDiagnosticsParams {
 909                                uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
 910                                    lsp::Url::parse("file:///test/fallback.rs").unwrap()
 911                                }),
 912                                diagnostics: diagnostics.clone(),
 913                                version: None,
 914                            },
 915                            &[],
 916                            cx,
 917                        )
 918                        .unwrap()
 919                });
 920                cx.executor()
 921                    .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 922
 923                cx.run_until_parked();
 924            }
 925        }
 926    }
 927
 928    log::info!("updating mutated diagnostics view");
 929    mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 930        diagnostics.update_stale_excerpts(window, cx)
 931    });
 932
 933    cx.executor()
 934        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
 935    cx.run_until_parked();
 936}
 937
 938#[gpui::test]
 939async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
 940    init_test(cx);
 941
 942    let mut cx = EditorTestContext::new(cx).await;
 943    let lsp_store =
 944        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
 945
 946    cx.set_state(indoc! {"
 947        ˇfn func(abc def: i32) -> u32 {
 948        }
 949    "});
 950
 951    let message = "Something's wrong!";
 952    cx.update(|_, cx| {
 953        lsp_store.update(cx, |lsp_store, cx| {
 954            lsp_store
 955                .update_diagnostics(
 956                    LanguageServerId(0),
 957                    lsp::PublishDiagnosticsParams {
 958                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
 959                        version: None,
 960                        diagnostics: vec![lsp::Diagnostic {
 961                            range: lsp::Range::new(
 962                                lsp::Position::new(0, 11),
 963                                lsp::Position::new(0, 12),
 964                            ),
 965                            severity: Some(lsp::DiagnosticSeverity::ERROR),
 966                            message: message.to_string(),
 967                            ..Default::default()
 968                        }],
 969                    },
 970                    &[],
 971                    cx,
 972                )
 973                .unwrap()
 974        });
 975    });
 976    cx.run_until_parked();
 977
 978    cx.update_editor(|editor, window, cx| {
 979        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
 980        assert_eq!(
 981            editor
 982                .active_diagnostic_group()
 983                .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
 984            Some(message),
 985            "Should have a diagnostics group activated"
 986        );
 987    });
 988    cx.assert_editor_state(indoc! {"
 989        fn func(abcˇ def: i32) -> u32 {
 990        }
 991    "});
 992
 993    cx.update(|_, cx| {
 994        lsp_store.update(cx, |lsp_store, cx| {
 995            lsp_store
 996                .update_diagnostics(
 997                    LanguageServerId(0),
 998                    lsp::PublishDiagnosticsParams {
 999                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
1000                        version: None,
1001                        diagnostics: Vec::new(),
1002                    },
1003                    &[],
1004                    cx,
1005                )
1006                .unwrap()
1007        });
1008    });
1009    cx.run_until_parked();
1010    cx.update_editor(|editor, _, _| {
1011        assert_eq!(editor.active_diagnostic_group(), None);
1012    });
1013    cx.assert_editor_state(indoc! {"
1014        fn func(abcˇ def: i32) -> u32 {
1015        }
1016    "});
1017
1018    cx.update_editor(|editor, window, cx| {
1019        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1020        assert_eq!(editor.active_diagnostic_group(), None);
1021    });
1022    cx.assert_editor_state(indoc! {"
1023        fn func(abcˇ def: i32) -> u32 {
1024        }
1025    "});
1026}
1027
1028#[gpui::test]
1029async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1030    init_test(cx);
1031
1032    let mut cx = EditorTestContext::new(cx).await;
1033    let lsp_store =
1034        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
1035
1036    cx.set_state(indoc! {"
1037        ˇfn func(abc def: i32) -> u32 {
1038        }
1039    "});
1040
1041    cx.update(|_, cx| {
1042        lsp_store.update(cx, |lsp_store, cx| {
1043            lsp_store
1044                .update_diagnostics(
1045                    LanguageServerId(0),
1046                    lsp::PublishDiagnosticsParams {
1047                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
1048                        version: None,
1049                        diagnostics: vec![
1050                            lsp::Diagnostic {
1051                                range: lsp::Range::new(
1052                                    lsp::Position::new(0, 11),
1053                                    lsp::Position::new(0, 12),
1054                                ),
1055                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1056                                ..Default::default()
1057                            },
1058                            lsp::Diagnostic {
1059                                range: lsp::Range::new(
1060                                    lsp::Position::new(0, 12),
1061                                    lsp::Position::new(0, 15),
1062                                ),
1063                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1064                                ..Default::default()
1065                            },
1066                            lsp::Diagnostic {
1067                                range: lsp::Range::new(
1068                                    lsp::Position::new(0, 12),
1069                                    lsp::Position::new(0, 15),
1070                                ),
1071                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1072                                ..Default::default()
1073                            },
1074                            lsp::Diagnostic {
1075                                range: lsp::Range::new(
1076                                    lsp::Position::new(0, 25),
1077                                    lsp::Position::new(0, 28),
1078                                ),
1079                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1080                                ..Default::default()
1081                            },
1082                        ],
1083                    },
1084                    &[],
1085                    cx,
1086                )
1087                .unwrap()
1088        });
1089    });
1090    cx.run_until_parked();
1091
1092    //// Backward
1093
1094    // Fourth diagnostic
1095    cx.update_editor(|editor, window, cx| {
1096        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1097    });
1098    cx.assert_editor_state(indoc! {"
1099        fn func(abc def: i32) -> ˇu32 {
1100        }
1101    "});
1102
1103    // Third diagnostic
1104    cx.update_editor(|editor, window, cx| {
1105        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1106    });
1107    cx.assert_editor_state(indoc! {"
1108        fn func(abc ˇdef: i32) -> u32 {
1109        }
1110    "});
1111
1112    // Second diagnostic, same place
1113    cx.update_editor(|editor, window, cx| {
1114        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1115    });
1116    cx.assert_editor_state(indoc! {"
1117        fn func(abc ˇdef: i32) -> u32 {
1118        }
1119    "});
1120
1121    // First diagnostic
1122    cx.update_editor(|editor, window, cx| {
1123        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1124    });
1125    cx.assert_editor_state(indoc! {"
1126        fn func(abcˇ def: i32) -> u32 {
1127        }
1128    "});
1129
1130    // Wrapped over, fourth diagnostic
1131    cx.update_editor(|editor, window, cx| {
1132        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
1133    });
1134    cx.assert_editor_state(indoc! {"
1135        fn func(abc def: i32) -> ˇu32 {
1136        }
1137    "});
1138
1139    cx.update_editor(|editor, window, cx| {
1140        editor.move_to_beginning(&MoveToBeginning, window, cx);
1141    });
1142    cx.assert_editor_state(indoc! {"
1143        ˇfn func(abc def: i32) -> u32 {
1144        }
1145    "});
1146
1147    //// Forward
1148
1149    // First diagnostic
1150    cx.update_editor(|editor, window, cx| {
1151        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1152    });
1153    cx.assert_editor_state(indoc! {"
1154        fn func(abcˇ def: i32) -> u32 {
1155        }
1156    "});
1157
1158    // Second diagnostic
1159    cx.update_editor(|editor, window, cx| {
1160        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1161    });
1162    cx.assert_editor_state(indoc! {"
1163        fn func(abc ˇdef: i32) -> u32 {
1164        }
1165    "});
1166
1167    // Third diagnostic, same place
1168    cx.update_editor(|editor, window, cx| {
1169        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1170    });
1171    cx.assert_editor_state(indoc! {"
1172        fn func(abc ˇdef: i32) -> u32 {
1173        }
1174    "});
1175
1176    // Fourth diagnostic
1177    cx.update_editor(|editor, window, cx| {
1178        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1179    });
1180    cx.assert_editor_state(indoc! {"
1181        fn func(abc def: i32) -> ˇu32 {
1182        }
1183    "});
1184
1185    // Wrapped around, first diagnostic
1186    cx.update_editor(|editor, window, cx| {
1187        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
1188    });
1189    cx.assert_editor_state(indoc! {"
1190        fn func(abcˇ def: i32) -> u32 {
1191        }
1192    "});
1193}
1194
1195fn init_test(cx: &mut TestAppContext) {
1196    cx.update(|cx| {
1197        let settings = SettingsStore::test(cx);
1198        cx.set_global(settings);
1199        theme::init(theme::LoadThemes::JustBase, cx);
1200        language::init(cx);
1201        client::init_settings(cx);
1202        workspace::init_settings(cx);
1203        Project::init_settings(cx);
1204        crate::init(cx);
1205        editor::init(cx);
1206    });
1207}
1208
1209fn randomly_update_diagnostics_for_path(
1210    fs: &FakeFs,
1211    path: &Path,
1212    diagnostics: &mut Vec<lsp::Diagnostic>,
1213    next_id: &mut usize,
1214    rng: &mut impl Rng,
1215) {
1216    let mutation_count = rng.gen_range(1..=3);
1217    for _ in 0..mutation_count {
1218        if rng.gen_bool(0.3) && !diagnostics.is_empty() {
1219            let idx = rng.gen_range(0..diagnostics.len());
1220            log::info!("  removing diagnostic at index {idx}");
1221            diagnostics.remove(idx);
1222        } else {
1223            let unique_id = *next_id;
1224            *next_id += 1;
1225
1226            let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
1227
1228            let ix = rng.gen_range(0..=diagnostics.len());
1229            log::info!(
1230                "  inserting {} at index {ix}. {},{}..{},{}",
1231                new_diagnostic.message,
1232                new_diagnostic.range.start.line,
1233                new_diagnostic.range.start.character,
1234                new_diagnostic.range.end.line,
1235                new_diagnostic.range.end.character,
1236            );
1237            for related in new_diagnostic.related_information.iter().flatten() {
1238                log::info!(
1239                    "   {}. {},{}..{},{}",
1240                    related.message,
1241                    related.location.range.start.line,
1242                    related.location.range.start.character,
1243                    related.location.range.end.line,
1244                    related.location.range.end.character,
1245                );
1246            }
1247            diagnostics.insert(ix, new_diagnostic);
1248        }
1249    }
1250}
1251
1252fn random_lsp_diagnostic(
1253    rng: &mut impl Rng,
1254    fs: &FakeFs,
1255    path: &Path,
1256    unique_id: usize,
1257) -> lsp::Diagnostic {
1258    // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
1259    // because language servers can potentially give us those, and we should handle them gracefully.
1260    const ERROR_MARGIN: usize = 10;
1261
1262    let file_content = fs.read_file_sync(path).unwrap();
1263    let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
1264
1265    let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1266    let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
1267
1268    let start_point = file_text.offset_to_point_utf16(start);
1269    let end_point = file_text.offset_to_point_utf16(end);
1270
1271    let range = lsp::Range::new(
1272        lsp::Position::new(start_point.row, start_point.column),
1273        lsp::Position::new(end_point.row, end_point.column),
1274    );
1275
1276    let severity = if rng.gen_bool(0.5) {
1277        Some(lsp::DiagnosticSeverity::ERROR)
1278    } else {
1279        Some(lsp::DiagnosticSeverity::WARNING)
1280    };
1281
1282    let message = format!("diagnostic {unique_id}");
1283
1284    let related_information = if rng.gen_bool(0.3) {
1285        let info_count = rng.gen_range(1..=3);
1286        let mut related_info = Vec::with_capacity(info_count);
1287
1288        for i in 0..info_count {
1289            let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
1290            let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
1291
1292            let info_start_point = file_text.offset_to_point_utf16(info_start);
1293            let info_end_point = file_text.offset_to_point_utf16(info_end);
1294
1295            let info_range = lsp::Range::new(
1296                lsp::Position::new(info_start_point.row, info_start_point.column),
1297                lsp::Position::new(info_end_point.row, info_end_point.column),
1298            );
1299
1300            related_info.push(lsp::DiagnosticRelatedInformation {
1301                location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
1302                message: format!("related info {i} for diagnostic {unique_id}"),
1303            });
1304        }
1305
1306        Some(related_info)
1307    } else {
1308        None
1309    };
1310
1311    lsp::Diagnostic {
1312        range,
1313        severity,
1314        message,
1315        related_information,
1316        data: None,
1317        ..Default::default()
1318    }
1319}