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