diagnostics_tests.rs

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