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