diagnostics_tests.rs

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