diagnostics_tests.rs

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