diagnostics_tests.rs

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