diagnostics_tests.rs

   1use super::*;
   2use collections::{HashMap, HashSet};
   3use editor::{
   4    DisplayPoint, EditorSettings, Inlay, MultiBufferOffset,
   5    actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
   6    display_map::DisplayRow,
   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, rel_path::rel_path};
  31use workspace::MultiWorkspace;
  32
  33#[ctor::ctor]
  34fn init_logger() {
  35    zlog::init_test();
  36}
  37
  38#[gpui::test]
  39async fn test_diagnostics(cx: &mut TestAppContext) {
  40    init_test(cx);
  41
  42    let fs = FakeFs::new(cx.executor());
  43    fs.insert_tree(
  44        path!("/test"),
  45        json!({
  46            "consts.rs": "
  47                const a: i32 = 'a';
  48                const b: i32 = c;
  49            "
  50            .unindent(),
  51
  52            "main.rs": "
  53                fn main() {
  54                    let x = vec![];
  55                    let y = vec![];
  56                    a(x);
  57                    b(y);
  58                    // comment 1
  59                    // comment 2
  60                    c(y);
  61                    d(x);
  62                }
  63            "
  64            .unindent(),
  65        }),
  66    )
  67    .await;
  68
  69    let language_server_id = LanguageServerId(0);
  70    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
  71    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
  72    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
  73    let cx = &mut VisualTestContext::from_window(window.into(), cx);
  74    let workspace = window
  75        .read_with(cx, |mw, _| mw.workspace().clone())
  76        .unwrap();
  77    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
  78
  79    // Create some diagnostics
  80    lsp_store.update(cx, |lsp_store, cx| {
  81        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
  82            uri: uri.clone(),
  83            diagnostics: vec![lsp::Diagnostic{
  84                range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
  85                severity:Some(lsp::DiagnosticSeverity::ERROR),
  86                message: "use of moved value\nvalue used here after move".to_string(),
  87                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
  88                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
  89                    message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
  90                },
  91                lsp::DiagnosticRelatedInformation {
  92                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
  93                    message: "value moved here".to_string()
  94                },
  95                ]),
  96                ..Default::default()
  97            },
  98            lsp::Diagnostic{
  99                range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
 100                severity:Some(lsp::DiagnosticSeverity::ERROR),
 101                message: "use of moved value\nvalue used here after move".to_string(),
 102                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 103                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
 104                    message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
 105                },
 106                lsp::DiagnosticRelatedInformation {
 107                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
 108                    message: "value moved here".to_string()
 109                },
 110                ]),
 111                ..Default::default()
 112            }
 113            ],
 114            version: None
 115        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
 116    });
 117
 118    // Open the project diagnostics view while there are already diagnostics.
 119    let diagnostics = window.build_entity(cx, |window, cx| {
 120        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 121    });
 122    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 123
 124    diagnostics
 125        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
 126        .await;
 127
 128    pretty_assertions::assert_eq!(
 129        editor_content_with_blocks(&editor, cx),
 130        indoc::indoc! {
 131            "§ main.rs
 132             § -----
 133             fn main() {
 134                 let x = vec![];
 135             § move occurs because `x` has type `Vec<char>`, which does not implement
 136             § the `Copy` trait (back)
 137                 let y = vec![];
 138             § move occurs because `y` has type `Vec<char>`, which does not implement
 139             § the `Copy` trait (back)
 140                 a(x); § value moved here (back)
 141                 b(y); § value moved here
 142                 // comment 1
 143                 // comment 2
 144                 c(y);
 145             § use of moved value
 146             § value used here after move
 147             § hint: move occurs because `y` has type `Vec<char>`, which does not
 148             § implement the `Copy` trait
 149                 d(x);
 150             § use of moved value
 151             § value used here after move
 152             § hint: move occurs because `x` has type `Vec<char>`, which does not
 153             § implement the `Copy` trait
 154             § hint: value moved here
 155             }"
 156        }
 157    );
 158
 159    // Cursor is at the first diagnostic
 160    editor.update(cx, |editor, cx| {
 161        assert_eq!(
 162            editor
 163                .selections
 164                .display_ranges(&editor.display_snapshot(cx)),
 165            [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
 166        );
 167    });
 168
 169    // Diagnostics are added for another earlier path.
 170    lsp_store.update(cx, |lsp_store, cx| {
 171        lsp_store.disk_based_diagnostics_started(language_server_id, cx);
 172        lsp_store
 173            .update_diagnostics(
 174                language_server_id,
 175                lsp::PublishDiagnosticsParams {
 176                    uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
 177                    diagnostics: vec![lsp::Diagnostic {
 178                        range: lsp::Range::new(
 179                            lsp::Position::new(0, 15),
 180                            lsp::Position::new(0, 15),
 181                        ),
 182                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 183                        message: "mismatched types expected `usize`, found `char`".to_string(),
 184                        ..Default::default()
 185                    }],
 186                    version: None,
 187                },
 188                None,
 189                DiagnosticSourceKind::Pushed,
 190                &[],
 191                cx,
 192            )
 193            .unwrap();
 194        lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
 195    });
 196
 197    diagnostics
 198        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
 199        .await;
 200
 201    pretty_assertions::assert_eq!(
 202        editor_content_with_blocks(&editor, cx),
 203        indoc::indoc! {
 204            "§ consts.rs
 205             § -----
 206             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
 207             const b: i32 = c;
 208
 209             § main.rs
 210             § -----
 211             fn main() {
 212                 let x = vec![];
 213             § move occurs because `x` has type `Vec<char>`, which does not implement
 214             § the `Copy` trait (back)
 215                 let y = vec![];
 216             § move occurs because `y` has type `Vec<char>`, which does not implement
 217             § the `Copy` trait (back)
 218                 a(x); § value moved here (back)
 219                 b(y); § value moved here
 220                 // comment 1
 221                 // comment 2
 222                 c(y);
 223             § use of moved value
 224             § value used here after move
 225             § hint: move occurs because `y` has type `Vec<char>`, which does not
 226             § implement the `Copy` trait
 227                 d(x);
 228             § use of moved value
 229             § value used here after move
 230             § hint: move occurs because `x` has type `Vec<char>`, which does not
 231             § implement the `Copy` trait
 232             § hint: value moved here
 233             }"
 234        }
 235    );
 236
 237    // Cursor keeps its position.
 238    editor.update(cx, |editor, cx| {
 239        assert_eq!(
 240            editor
 241                .selections
 242                .display_ranges(&editor.display_snapshot(cx)),
 243            [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
 244        );
 245    });
 246
 247    // Diagnostics are added to the first path
 248    lsp_store.update(cx, |lsp_store, cx| {
 249        lsp_store.disk_based_diagnostics_started(language_server_id, cx);
 250        lsp_store
 251            .update_diagnostics(
 252                language_server_id,
 253                lsp::PublishDiagnosticsParams {
 254                    uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
 255                    diagnostics: vec![
 256                        lsp::Diagnostic {
 257                            range: lsp::Range::new(
 258                                lsp::Position::new(0, 15),
 259                                lsp::Position::new(0, 15),
 260                            ),
 261                            severity: Some(lsp::DiagnosticSeverity::ERROR),
 262                            message: "mismatched types expected `usize`, found `char`".to_string(),
 263                            ..Default::default()
 264                        },
 265                        lsp::Diagnostic {
 266                            range: lsp::Range::new(
 267                                lsp::Position::new(1, 15),
 268                                lsp::Position::new(1, 15),
 269                            ),
 270                            severity: Some(lsp::DiagnosticSeverity::ERROR),
 271                            message: "unresolved name `c`".to_string(),
 272                            ..Default::default()
 273                        },
 274                    ],
 275                    version: None,
 276                },
 277                None,
 278                DiagnosticSourceKind::Pushed,
 279                &[],
 280                cx,
 281            )
 282            .unwrap();
 283        lsp_store.disk_based_diagnostics_finished(language_server_id, cx);
 284    });
 285
 286    diagnostics
 287        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
 288        .await;
 289
 290    pretty_assertions::assert_eq!(
 291        editor_content_with_blocks(&editor, cx),
 292        indoc::indoc! {
 293            "§ consts.rs
 294             § -----
 295             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
 296             const b: i32 = c; § unresolved name `c`
 297
 298             § main.rs
 299             § -----
 300             fn main() {
 301                 let x = vec![];
 302             § move occurs because `x` has type `Vec<char>`, which does not implement
 303             § the `Copy` trait (back)
 304                 let y = vec![];
 305             § move occurs because `y` has type `Vec<char>`, which does not implement
 306             § the `Copy` trait (back)
 307                 a(x); § value moved here (back)
 308                 b(y); § value moved here
 309                 // comment 1
 310                 // comment 2
 311                 c(y);
 312             § use of moved value
 313             § value used here after move
 314             § hint: move occurs because `y` has type `Vec<char>`, which does not
 315             § implement the `Copy` trait
 316                 d(x);
 317             § use of moved value
 318             § value used here after move
 319             § hint: move occurs because `x` has type `Vec<char>`, which does not
 320             § implement the `Copy` trait
 321             § hint: value moved here
 322             }"
 323        }
 324    );
 325}
 326
 327#[gpui::test]
 328async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
 329    init_test(cx);
 330
 331    let fs = FakeFs::new(cx.executor());
 332    fs.insert_tree(
 333        path!("/test"),
 334        json!({
 335            "main.js": "
 336            function test() {
 337                return 1
 338            };
 339
 340            tset();
 341            ".unindent()
 342        }),
 343    )
 344    .await;
 345
 346    let server_id_1 = LanguageServerId(100);
 347    let server_id_2 = LanguageServerId(101);
 348    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 349    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 350    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 351    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 352    let workspace = window
 353        .read_with(cx, |mw, _| mw.workspace().clone())
 354        .unwrap();
 355
 356    let diagnostics = window.build_entity(cx, |window, cx| {
 357        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 358    });
 359    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 360
 361    // Two language servers start updating diagnostics
 362    lsp_store.update(cx, |lsp_store, cx| {
 363        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 364        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 365        lsp_store
 366            .update_diagnostics(
 367                server_id_1,
 368                lsp::PublishDiagnosticsParams {
 369                    uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 370                    diagnostics: vec![lsp::Diagnostic {
 371                        range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
 372                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 373                        message: "no method `tset`".to_string(),
 374                        related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 375                            location: lsp::Location::new(
 376                                lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 377                                lsp::Range::new(
 378                                    lsp::Position::new(0, 9),
 379                                    lsp::Position::new(0, 13),
 380                                ),
 381                            ),
 382                            message: "method `test` defined here".to_string(),
 383                        }]),
 384                        ..Default::default()
 385                    }],
 386                    version: None,
 387                },
 388                None,
 389                DiagnosticSourceKind::Pushed,
 390                &[],
 391                cx,
 392            )
 393            .unwrap();
 394    });
 395
 396    // The first language server finishes
 397    lsp_store.update(cx, |lsp_store, cx| {
 398        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 399    });
 400
 401    // Only the first language server's diagnostics are shown.
 402    cx.executor()
 403        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 404    cx.executor().run_until_parked();
 405    editor.update_in(cx, |editor, window, cx| {
 406        editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
 407    });
 408
 409    pretty_assertions::assert_eq!(
 410        editor_content_with_blocks(&editor, cx),
 411        indoc::indoc! {
 412            "§ main.js
 413             § -----
 414 415
 416             tset(); § no method `tset`"
 417        }
 418    );
 419
 420    editor.update(cx, |editor, cx| {
 421        editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
 422    });
 423
 424    pretty_assertions::assert_eq!(
 425        editor_content_with_blocks(&editor, cx),
 426        indoc::indoc! {
 427            "§ main.js
 428             § -----
 429             function test() { § method `test` defined here
 430                 return 1
 431             };
 432
 433             tset(); § no method `tset`"
 434        }
 435    );
 436}
 437
 438#[gpui::test]
 439async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 440    init_test(cx);
 441
 442    let fs = FakeFs::new(cx.executor());
 443    fs.insert_tree(
 444        path!("/test"),
 445        json!({
 446            "main.js": "
 447                a();
 448                b();
 449                c();
 450                d();
 451                e();
 452            ".unindent()
 453        }),
 454    )
 455    .await;
 456
 457    let server_id_1 = LanguageServerId(100);
 458    let server_id_2 = LanguageServerId(101);
 459    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 460    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 461    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 462    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 463    let workspace = window
 464        .read_with(cx, |mw, _| mw.workspace().clone())
 465        .unwrap();
 466
 467    let diagnostics = window.build_entity(cx, |window, cx| {
 468        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 469    });
 470    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 471
 472    // Two language servers start updating diagnostics
 473    lsp_store.update(cx, |lsp_store, cx| {
 474        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 475        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 476        lsp_store
 477            .update_diagnostics(
 478                server_id_1,
 479                lsp::PublishDiagnosticsParams {
 480                    uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 481                    diagnostics: vec![lsp::Diagnostic {
 482                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
 483                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 484                        message: "error 1".to_string(),
 485                        ..Default::default()
 486                    }],
 487                    version: None,
 488                },
 489                None,
 490                DiagnosticSourceKind::Pushed,
 491                &[],
 492                cx,
 493            )
 494            .unwrap();
 495    });
 496
 497    // The first language server finishes
 498    lsp_store.update(cx, |lsp_store, cx| {
 499        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 500    });
 501
 502    // Only the first language server's diagnostics are shown.
 503    cx.executor()
 504        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 505    cx.executor().run_until_parked();
 506
 507    pretty_assertions::assert_eq!(
 508        editor_content_with_blocks(&editor, cx),
 509        indoc::indoc! {
 510            "§ main.js
 511             § -----
 512             a(); § error 1
 513             b();
 514             c();"
 515        }
 516    );
 517
 518    // The second language server finishes
 519    lsp_store.update(cx, |lsp_store, cx| {
 520        lsp_store
 521            .update_diagnostics(
 522                server_id_2,
 523                lsp::PublishDiagnosticsParams {
 524                    uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 525                    diagnostics: vec![lsp::Diagnostic {
 526                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
 527                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 528                        message: "warning 1".to_string(),
 529                        ..Default::default()
 530                    }],
 531                    version: None,
 532                },
 533                None,
 534                DiagnosticSourceKind::Pushed,
 535                &[],
 536                cx,
 537            )
 538            .unwrap();
 539        lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
 540    });
 541
 542    // Both language server's diagnostics are shown.
 543    cx.executor()
 544        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 545    cx.executor().run_until_parked();
 546
 547    pretty_assertions::assert_eq!(
 548        editor_content_with_blocks(&editor, cx),
 549        indoc::indoc! {
 550            "§ main.js
 551             § -----
 552             a(); § error 1
 553             b(); § warning 1
 554             c();
 555             d();"
 556        }
 557    );
 558
 559    // Both language servers start updating diagnostics, and the first server finishes.
 560    lsp_store.update(cx, |lsp_store, cx| {
 561        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
 562        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
 563        lsp_store
 564            .update_diagnostics(
 565                server_id_1,
 566                lsp::PublishDiagnosticsParams {
 567                    uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 568                    diagnostics: vec![lsp::Diagnostic {
 569                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
 570                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 571                        message: "warning 2".to_string(),
 572                        ..Default::default()
 573                    }],
 574                    version: None,
 575                },
 576                None,
 577                DiagnosticSourceKind::Pushed,
 578                &[],
 579                cx,
 580            )
 581            .unwrap();
 582        lsp_store
 583            .update_diagnostics(
 584                server_id_2,
 585                lsp::PublishDiagnosticsParams {
 586                    uri: lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(),
 587                    diagnostics: vec![],
 588                    version: None,
 589                },
 590                None,
 591                DiagnosticSourceKind::Pushed,
 592                &[],
 593                cx,
 594            )
 595            .unwrap();
 596        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
 597    });
 598
 599    // Only the first language server's diagnostics are updated.
 600    cx.executor()
 601        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 602    cx.executor().run_until_parked();
 603
 604    pretty_assertions::assert_eq!(
 605        editor_content_with_blocks(&editor, cx),
 606        indoc::indoc! {
 607            "§ main.js
 608             § -----
 609             a();
 610             b(); § warning 1
 611             c(); § warning 2
 612             d();
 613             e();"
 614        }
 615    );
 616
 617    // The second language server finishes.
 618    lsp_store.update(cx, |lsp_store, cx| {
 619        lsp_store
 620            .update_diagnostics(
 621                server_id_2,
 622                lsp::PublishDiagnosticsParams {
 623                    uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
 624                    diagnostics: vec![lsp::Diagnostic {
 625                        range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
 626                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 627                        message: "warning 2".to_string(),
 628                        ..Default::default()
 629                    }],
 630                    version: None,
 631                },
 632                None,
 633                DiagnosticSourceKind::Pushed,
 634                &[],
 635                cx,
 636            )
 637            .unwrap();
 638        lsp_store.disk_based_diagnostics_finished(server_id_2, cx);
 639    });
 640
 641    // Both language servers' diagnostics are updated.
 642    cx.executor()
 643        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 644    cx.executor().run_until_parked();
 645
 646    pretty_assertions::assert_eq!(
 647        editor_content_with_blocks(&editor, cx),
 648        indoc::indoc! {
 649            "§ main.js
 650                 § -----
 651                 a();
 652                 b();
 653                 c(); § warning 2
 654                 d(); § warning 2
 655                 e();"
 656        }
 657    );
 658}
 659
 660#[gpui::test(iterations = 20)]
 661async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
 662    init_test(cx);
 663
 664    let operations = env::var("OPERATIONS")
 665        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 666        .unwrap_or(10);
 667
 668    let fs = FakeFs::new(cx.executor());
 669    fs.insert_tree(path!("/test"), json!({})).await;
 670
 671    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 672    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 673    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 674    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 675    let workspace = window
 676        .read_with(cx, |mw, _| mw.workspace().clone())
 677        .unwrap();
 678
 679    let mutated_diagnostics = window.build_entity(cx, |window, cx| {
 680        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 681    });
 682
 683    workspace.update_in(cx, |workspace, window, cx| {
 684        workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
 685    });
 686    mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
 687        assert!(diagnostics.focus_handle.is_focused(window));
 688    });
 689
 690    let mut next_id = 0;
 691    let mut next_filename = 0;
 692    let mut language_server_ids = vec![LanguageServerId(0)];
 693    let mut updated_language_servers = HashSet::default();
 694    let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
 695        Default::default();
 696
 697    for _ in 0..operations {
 698        match rng.random_range(0..100) {
 699            // language server completes its diagnostic check
 700            0..=20 if !updated_language_servers.is_empty() => {
 701                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 702                log::info!("finishing diagnostic check for language server {server_id}");
 703                lsp_store.update(cx, |lsp_store, cx| {
 704                    lsp_store.disk_based_diagnostics_finished(server_id, cx)
 705                });
 706
 707                if rng.random_bool(0.5) {
 708                    cx.run_until_parked();
 709                }
 710            }
 711
 712            // language server updates diagnostics
 713            _ => {
 714                let (path, server_id, diagnostics) =
 715                    match current_diagnostics.iter_mut().choose(&mut rng) {
 716                        // update existing set of diagnostics
 717                        Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
 718                            (path.clone(), *server_id, diagnostics)
 719                        }
 720
 721                        // insert a set of diagnostics for a new path
 722                        _ => {
 723                            let path: PathBuf =
 724                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
 725                            let len = rng.random_range(128..256);
 726                            let content =
 727                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 728                            fs.insert_file(&path, content.into_bytes()).await;
 729
 730                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 731                                Some(server_id) if rng.random_bool(0.5) => *server_id,
 732                                _ => {
 733                                    let id = LanguageServerId(language_server_ids.len());
 734                                    language_server_ids.push(id);
 735                                    id
 736                                }
 737                            };
 738
 739                            (
 740                                path.clone(),
 741                                server_id,
 742                                current_diagnostics.entry((path, server_id)).or_default(),
 743                            )
 744                        }
 745                    };
 746
 747                updated_language_servers.insert(server_id);
 748
 749                lsp_store.update(cx, |lsp_store, cx| {
 750                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 751                    randomly_update_diagnostics_for_path(
 752                        &fs,
 753                        &path,
 754                        diagnostics,
 755                        &mut next_id,
 756                        &mut rng,
 757                    );
 758                    lsp_store
 759                        .update_diagnostics(
 760                            server_id,
 761                            lsp::PublishDiagnosticsParams {
 762                                uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
 763                                    lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
 764                                }),
 765                                diagnostics: diagnostics.clone(),
 766                                version: None,
 767                            },
 768                            None,
 769                            DiagnosticSourceKind::Pushed,
 770                            &[],
 771                            cx,
 772                        )
 773                        .unwrap()
 774                });
 775                cx.executor()
 776                    .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 777
 778                cx.run_until_parked();
 779            }
 780        }
 781    }
 782
 783    log::info!("updating mutated diagnostics view");
 784    mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 785        diagnostics.update_stale_excerpts(window, cx)
 786    });
 787
 788    log::info!("constructing reference diagnostics view");
 789    let reference_diagnostics = window.build_entity(cx, |window, cx| {
 790        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 791    });
 792    cx.executor()
 793        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 794    cx.run_until_parked();
 795
 796    let mutated_excerpts =
 797        editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx);
 798    let reference_excerpts = editor_content_with_blocks(
 799        &reference_diagnostics.update(cx, |d, _| d.editor.clone()),
 800        cx,
 801    );
 802
 803    // The mutated view may contain more than the reference view as
 804    // we don't currently shrink excerpts when diagnostics were removed.
 805    let mut ref_iter = reference_excerpts.lines().filter(|line| {
 806        // ignore $ ---- and $ <file>.rs
 807        !line.starts_with('§')
 808            || line.starts_with("§ diagnostic")
 809            || line.starts_with("§ related info")
 810    });
 811    let mut next_ref_line = ref_iter.next();
 812    let mut skipped_block = false;
 813
 814    for mut_line in mutated_excerpts.lines() {
 815        if let Some(ref_line) = next_ref_line {
 816            if mut_line == ref_line {
 817                next_ref_line = ref_iter.next();
 818            } else if mut_line.contains('§')
 819                // ignore $ ---- and $ <file>.rs
 820                && (!mut_line.starts_with('§')
 821                    || mut_line.starts_with("§ diagnostic")
 822                    || mut_line.starts_with("§ related info"))
 823            {
 824                skipped_block = true;
 825            }
 826        }
 827    }
 828
 829    if next_ref_line.is_some() || skipped_block {
 830        pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
 831    }
 832}
 833
 834// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
 835#[gpui::test]
 836async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
 837    init_test(cx);
 838
 839    let operations = env::var("OPERATIONS")
 840        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 841        .unwrap_or(10);
 842
 843    let fs = FakeFs::new(cx.executor());
 844    fs.insert_tree(path!("/test"), json!({})).await;
 845
 846    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 847    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 848    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 849    let cx = &mut VisualTestContext::from_window(window.into(), cx);
 850    let workspace = window
 851        .read_with(cx, |mw, _| mw.workspace().clone())
 852        .unwrap();
 853
 854    let mutated_diagnostics = window.build_entity(cx, |window, cx| {
 855        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 856    });
 857
 858    workspace.update_in(cx, |workspace, window, cx| {
 859        workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
 860    });
 861    mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
 862        assert!(diagnostics.focus_handle.is_focused(window));
 863    });
 864
 865    let mut next_id = 0;
 866    let mut next_filename = 0;
 867    let mut language_server_ids = vec![LanguageServerId(0)];
 868    let mut updated_language_servers = HashSet::default();
 869    let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
 870        Default::default();
 871    let mut next_inlay_id = 0;
 872
 873    for _ in 0..operations {
 874        match rng.random_range(0..100) {
 875            // language server completes its diagnostic check
 876            0..=20 if !updated_language_servers.is_empty() => {
 877                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 878                log::info!("finishing diagnostic check for language server {server_id}");
 879                lsp_store.update(cx, |lsp_store, cx| {
 880                    lsp_store.disk_based_diagnostics_finished(server_id, cx)
 881                });
 882
 883                if rng.random_bool(0.5) {
 884                    cx.run_until_parked();
 885                }
 886            }
 887
 888            21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 889                diagnostics.editor.update(cx, |editor, cx| {
 890                    let snapshot = editor.snapshot(window, cx);
 891                    if !snapshot.buffer_snapshot().is_empty() {
 892                        let position = rng
 893                            .random_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len());
 894                        let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
 895                        log::info!(
 896                            "adding inlay at {position}/{}: {:?}",
 897                            snapshot.buffer_snapshot().len(),
 898                            snapshot.buffer_snapshot().text(),
 899                        );
 900
 901                        editor.splice_inlays(
 902                            &[],
 903                            vec![Inlay::edit_prediction(
 904                                post_inc(&mut next_inlay_id),
 905                                snapshot.buffer_snapshot().anchor_before(position),
 906                                Rope::from_iter(["Test inlay ", "next_inlay_id"]),
 907                            )],
 908                            cx,
 909                        );
 910                    }
 911                });
 912            }),
 913
 914            // language server updates diagnostics
 915            _ => {
 916                let (path, server_id, diagnostics) =
 917                    match current_diagnostics.iter_mut().choose(&mut rng) {
 918                        // update existing set of diagnostics
 919                        Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
 920                            (path.clone(), *server_id, diagnostics)
 921                        }
 922
 923                        // insert a set of diagnostics for a new path
 924                        _ => {
 925                            let path: PathBuf =
 926                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
 927                            let len = rng.random_range(128..256);
 928                            let content =
 929                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 930                            fs.insert_file(&path, content.into_bytes()).await;
 931
 932                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 933                                Some(server_id) if rng.random_bool(0.5) => *server_id,
 934                                _ => {
 935                                    let id = LanguageServerId(language_server_ids.len());
 936                                    language_server_ids.push(id);
 937                                    id
 938                                }
 939                            };
 940
 941                            (
 942                                path.clone(),
 943                                server_id,
 944                                current_diagnostics.entry((path, server_id)).or_default(),
 945                            )
 946                        }
 947                    };
 948
 949                updated_language_servers.insert(server_id);
 950
 951                lsp_store.update(cx, |lsp_store, cx| {
 952                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 953                    randomly_update_diagnostics_for_path(
 954                        &fs,
 955                        &path,
 956                        diagnostics,
 957                        &mut next_id,
 958                        &mut rng,
 959                    );
 960                    lsp_store
 961                        .update_diagnostics(
 962                            server_id,
 963                            lsp::PublishDiagnosticsParams {
 964                                uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
 965                                    lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
 966                                }),
 967                                diagnostics: diagnostics.clone(),
 968                                version: None,
 969                            },
 970                            None,
 971                            DiagnosticSourceKind::Pushed,
 972                            &[],
 973                            cx,
 974                        )
 975                        .unwrap()
 976                });
 977                cx.executor()
 978                    .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 979
 980                cx.run_until_parked();
 981            }
 982        }
 983    }
 984
 985    log::info!("updating mutated diagnostics view");
 986    mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 987        diagnostics.update_stale_excerpts(window, cx)
 988    });
 989
 990    cx.executor()
 991        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 992    cx.run_until_parked();
 993}
 994
 995#[gpui::test]
 996async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
 997    init_test(cx);
 998
 999    let mut cx = EditorTestContext::new(cx).await;
1000    let lsp_store =
1001        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1002
1003    cx.set_state(indoc! {"
1004        ˇfn func(abc def: i32) -> u32 {
1005        }
1006    "});
1007
1008    let message = "Something's wrong!";
1009    cx.update(|_, cx| {
1010        lsp_store.update(cx, |lsp_store, cx| {
1011            lsp_store
1012                .update_diagnostics(
1013                    LanguageServerId(0),
1014                    lsp::PublishDiagnosticsParams {
1015                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1016                        version: None,
1017                        diagnostics: vec![lsp::Diagnostic {
1018                            range: lsp::Range::new(
1019                                lsp::Position::new(0, 11),
1020                                lsp::Position::new(0, 12),
1021                            ),
1022                            severity: Some(lsp::DiagnosticSeverity::ERROR),
1023                            message: message.to_string(),
1024                            ..Default::default()
1025                        }],
1026                    },
1027                    None,
1028                    DiagnosticSourceKind::Pushed,
1029                    &[],
1030                    cx,
1031                )
1032                .unwrap()
1033        });
1034    });
1035    cx.run_until_parked();
1036
1037    cx.update_editor(|editor, window, cx| {
1038        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1039        assert_eq!(
1040            editor
1041                .active_diagnostic_group()
1042                .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
1043            Some(message),
1044            "Should have a diagnostics group activated"
1045        );
1046    });
1047    cx.assert_editor_state(indoc! {"
1048        fn func(abcˇ def: i32) -> u32 {
1049        }
1050    "});
1051
1052    cx.update(|_, cx| {
1053        lsp_store.update(cx, |lsp_store, cx| {
1054            lsp_store
1055                .update_diagnostics(
1056                    LanguageServerId(0),
1057                    lsp::PublishDiagnosticsParams {
1058                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1059                        version: None,
1060                        diagnostics: Vec::new(),
1061                    },
1062                    None,
1063                    DiagnosticSourceKind::Pushed,
1064                    &[],
1065                    cx,
1066                )
1067                .unwrap()
1068        });
1069    });
1070    cx.run_until_parked();
1071    cx.update_editor(|editor, _, _| {
1072        assert_eq!(editor.active_diagnostic_group(), None);
1073    });
1074    cx.assert_editor_state(indoc! {"
1075        fn func(abcˇ def: i32) -> u32 {
1076        }
1077    "});
1078
1079    cx.update_editor(|editor, window, cx| {
1080        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1081        assert_eq!(editor.active_diagnostic_group(), None);
1082    });
1083    cx.assert_editor_state(indoc! {"
1084        fn func(abcˇ def: i32) -> u32 {
1085        }
1086    "});
1087}
1088
1089#[gpui::test]
1090async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1091    init_test(cx);
1092
1093    let mut cx = EditorTestContext::new(cx).await;
1094    let lsp_store =
1095        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1096
1097    cx.set_state(indoc! {"
1098        ˇfn func(abc def: i32) -> u32 {
1099        }
1100    "});
1101
1102    cx.update(|_, cx| {
1103        lsp_store.update(cx, |lsp_store, cx| {
1104            lsp_store
1105                .update_diagnostics(
1106                    LanguageServerId(0),
1107                    lsp::PublishDiagnosticsParams {
1108                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1109                        version: None,
1110                        diagnostics: vec![
1111                            lsp::Diagnostic {
1112                                range: lsp::Range::new(
1113                                    lsp::Position::new(0, 11),
1114                                    lsp::Position::new(0, 12),
1115                                ),
1116                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1117                                ..Default::default()
1118                            },
1119                            lsp::Diagnostic {
1120                                range: lsp::Range::new(
1121                                    lsp::Position::new(0, 12),
1122                                    lsp::Position::new(0, 15),
1123                                ),
1124                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1125                                ..Default::default()
1126                            },
1127                            lsp::Diagnostic {
1128                                range: lsp::Range::new(
1129                                    lsp::Position::new(0, 12),
1130                                    lsp::Position::new(0, 15),
1131                                ),
1132                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1133                                ..Default::default()
1134                            },
1135                            lsp::Diagnostic {
1136                                range: lsp::Range::new(
1137                                    lsp::Position::new(0, 25),
1138                                    lsp::Position::new(0, 28),
1139                                ),
1140                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1141                                ..Default::default()
1142                            },
1143                        ],
1144                    },
1145                    None,
1146                    DiagnosticSourceKind::Pushed,
1147                    &[],
1148                    cx,
1149                )
1150                .unwrap()
1151        });
1152    });
1153    cx.run_until_parked();
1154
1155    //// Backward
1156
1157    // Fourth 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    // Third 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    // Second diagnostic, same place
1176    cx.update_editor(|editor, window, cx| {
1177        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1178    });
1179    cx.assert_editor_state(indoc! {"
1180        fn func(abc ˇdef: i32) -> u32 {
1181        }
1182    "});
1183
1184    // First diagnostic
1185    cx.update_editor(|editor, window, cx| {
1186        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1187    });
1188    cx.assert_editor_state(indoc! {"
1189        fn func(abcˇ def: i32) -> u32 {
1190        }
1191    "});
1192
1193    // Wrapped over, fourth diagnostic
1194    cx.update_editor(|editor, window, cx| {
1195        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1196    });
1197    cx.assert_editor_state(indoc! {"
1198        fn func(abc def: i32) -> ˇu32 {
1199        }
1200    "});
1201
1202    cx.update_editor(|editor, window, cx| {
1203        editor.move_to_beginning(&MoveToBeginning, window, cx);
1204    });
1205    cx.assert_editor_state(indoc! {"
1206        ˇfn func(abc def: i32) -> u32 {
1207        }
1208    "});
1209
1210    //// Forward
1211
1212    // First 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    // Second 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    // Third diagnostic, same place
1231    cx.update_editor(|editor, window, cx| {
1232        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1233    });
1234    cx.assert_editor_state(indoc! {"
1235        fn func(abc ˇdef: i32) -> u32 {
1236        }
1237    "});
1238
1239    // Fourth diagnostic
1240    cx.update_editor(|editor, window, cx| {
1241        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1242    });
1243    cx.assert_editor_state(indoc! {"
1244        fn func(abc def: i32) -> ˇu32 {
1245        }
1246    "});
1247
1248    // Wrapped around, first diagnostic
1249    cx.update_editor(|editor, window, cx| {
1250        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1251    });
1252    cx.assert_editor_state(indoc! {"
1253        fn func(abcˇ def: i32) -> u32 {
1254        }
1255    "});
1256}
1257
1258#[gpui::test]
1259async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
1260    init_test(cx);
1261
1262    let mut cx = EditorTestContext::new(cx).await;
1263
1264    cx.set_state(indoc! {"
1265        fn func(abˇc def: i32) -> u32 {
1266        }
1267    "});
1268    let lsp_store =
1269        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1270
1271    cx.update(|_, cx| {
1272        lsp_store.update(cx, |lsp_store, cx| {
1273            lsp_store.update_diagnostics(
1274                LanguageServerId(0),
1275                lsp::PublishDiagnosticsParams {
1276                    uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1277                    version: None,
1278                    diagnostics: vec![lsp::Diagnostic {
1279                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
1280                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1281                        message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
1282                        ..Default::default()
1283                    }],
1284                },
1285                None,
1286                DiagnosticSourceKind::Pushed,
1287                &[],
1288                cx,
1289            )
1290        })
1291    }).unwrap();
1292    cx.run_until_parked();
1293    cx.update_editor(|editor, window, cx| {
1294        editor::hover_popover::hover(editor, &Default::default(), window, cx)
1295    });
1296    cx.run_until_parked();
1297    cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
1298}
1299
1300#[gpui::test]
1301async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1302    init_test(cx);
1303
1304    let mut cx = EditorLspTestContext::new_rust(
1305        lsp::ServerCapabilities {
1306            hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1307            ..Default::default()
1308        },
1309        cx,
1310    )
1311    .await;
1312
1313    // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1314    // info popover once request completes
1315    cx.set_state(indoc! {"
1316        fn teˇst() { println!(); }
1317    "});
1318    // Send diagnostic to client
1319    let range = cx.lsp_range(indoc! {"
1320        fn «test»() { println!(); }
1321    "});
1322    let lsp_store =
1323        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1324    cx.update(|_, cx| {
1325        lsp_store.update(cx, |lsp_store, cx| {
1326            lsp_store.update_diagnostics(
1327                LanguageServerId(0),
1328                lsp::PublishDiagnosticsParams {
1329                    uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1330                    version: None,
1331                    diagnostics: vec![lsp::Diagnostic {
1332                        range,
1333                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1334                        message: "A test diagnostic message.".to_string(),
1335                        ..Default::default()
1336                    }],
1337                },
1338                None,
1339                DiagnosticSourceKind::Pushed,
1340                &[],
1341                cx,
1342            )
1343        })
1344    })
1345    .unwrap();
1346    cx.run_until_parked();
1347
1348    // Hover pops diagnostic immediately
1349    cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
1350    cx.background_executor.run_until_parked();
1351
1352    cx.editor(|Editor { hover_state, .. }, _, _| {
1353        assert!(hover_state.diagnostic_popover.is_some());
1354        assert!(hover_state.info_popovers.is_empty());
1355    });
1356
1357    // Info Popover shows after request responded to
1358    let range = cx.lsp_range(indoc! {"
1359            fn «test»() { println!(); }
1360        "});
1361    cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1362        Ok(Some(lsp::Hover {
1363            contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1364                kind: lsp::MarkupKind::Markdown,
1365                value: "some new docs".to_string(),
1366            }),
1367            range: Some(range),
1368        }))
1369    });
1370    let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1);
1371    cx.background_executor
1372        .advance_clock(Duration::from_millis(delay));
1373
1374    cx.background_executor.run_until_parked();
1375    cx.editor(|Editor { hover_state, .. }, _, _| {
1376        hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1377    });
1378}
1379#[gpui::test]
1380async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
1381    init_test(cx);
1382
1383    let fs = FakeFs::new(cx.executor());
1384    fs.insert_tree(
1385        path!("/root"),
1386        json!({
1387            "main.js": "
1388                function test() {
1389                    const x = 10;
1390                    const y = 20;
1391                    return 1;
1392                }
1393                test();
1394            "
1395            .unindent(),
1396        }),
1397    )
1398    .await;
1399
1400    let language_server_id = LanguageServerId(0);
1401    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1402    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1403    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1404    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1405    let workspace = window
1406        .read_with(cx, |mw, _| mw.workspace().clone())
1407        .unwrap();
1408    let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
1409
1410    // Create diagnostics with code fields
1411    lsp_store.update(cx, |lsp_store, cx| {
1412        lsp_store
1413            .update_diagnostics(
1414                language_server_id,
1415                lsp::PublishDiagnosticsParams {
1416                    uri: uri.clone(),
1417                    diagnostics: vec![
1418                        lsp::Diagnostic {
1419                            range: lsp::Range::new(
1420                                lsp::Position::new(1, 4),
1421                                lsp::Position::new(1, 14),
1422                            ),
1423                            severity: Some(lsp::DiagnosticSeverity::WARNING),
1424                            code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1425                            source: Some("eslint".to_string()),
1426                            message: "'x' is assigned a value but never used".to_string(),
1427                            ..Default::default()
1428                        },
1429                        lsp::Diagnostic {
1430                            range: lsp::Range::new(
1431                                lsp::Position::new(2, 4),
1432                                lsp::Position::new(2, 14),
1433                            ),
1434                            severity: Some(lsp::DiagnosticSeverity::WARNING),
1435                            code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1436                            source: Some("eslint".to_string()),
1437                            message: "'y' is assigned a value but never used".to_string(),
1438                            ..Default::default()
1439                        },
1440                    ],
1441                    version: None,
1442                },
1443                None,
1444                DiagnosticSourceKind::Pushed,
1445                &[],
1446                cx,
1447            )
1448            .unwrap();
1449    });
1450
1451    // Open the project diagnostics view
1452    let diagnostics = window.build_entity(cx, |window, cx| {
1453        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
1454    });
1455    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
1456
1457    diagnostics
1458        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
1459        .await;
1460
1461    // Verify that the diagnostic codes are displayed correctly
1462    pretty_assertions::assert_eq!(
1463        editor_content_with_blocks(&editor, cx),
1464        indoc::indoc! {
1465            "§ main.js
1466             § -----
1467             function test() {
1468                 const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
1469                 const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
1470                 return 1;
1471             }"
1472        }
1473    );
1474}
1475
1476#[gpui::test]
1477async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
1478    init_test(cx);
1479
1480    let mut cx = EditorTestContext::new(cx).await;
1481    let lsp_store =
1482        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1483
1484    cx.set_state(indoc! {"error warning info hiˇnt"});
1485
1486    cx.update(|_, cx| {
1487        lsp_store.update(cx, |lsp_store, cx| {
1488            lsp_store
1489                .update_diagnostics(
1490                    LanguageServerId(0),
1491                    lsp::PublishDiagnosticsParams {
1492                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1493                        version: None,
1494                        diagnostics: vec![
1495                            lsp::Diagnostic {
1496                                range: lsp::Range::new(
1497                                    lsp::Position::new(0, 0),
1498                                    lsp::Position::new(0, 5),
1499                                ),
1500                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1501                                ..Default::default()
1502                            },
1503                            lsp::Diagnostic {
1504                                range: lsp::Range::new(
1505                                    lsp::Position::new(0, 6),
1506                                    lsp::Position::new(0, 13),
1507                                ),
1508                                severity: Some(lsp::DiagnosticSeverity::WARNING),
1509                                ..Default::default()
1510                            },
1511                            lsp::Diagnostic {
1512                                range: lsp::Range::new(
1513                                    lsp::Position::new(0, 14),
1514                                    lsp::Position::new(0, 18),
1515                                ),
1516                                severity: Some(lsp::DiagnosticSeverity::INFORMATION),
1517                                ..Default::default()
1518                            },
1519                            lsp::Diagnostic {
1520                                range: lsp::Range::new(
1521                                    lsp::Position::new(0, 19),
1522                                    lsp::Position::new(0, 23),
1523                                ),
1524                                severity: Some(lsp::DiagnosticSeverity::HINT),
1525                                ..Default::default()
1526                            },
1527                        ],
1528                    },
1529                    None,
1530                    DiagnosticSourceKind::Pushed,
1531                    &[],
1532                    cx,
1533                )
1534                .unwrap()
1535        });
1536    });
1537    cx.run_until_parked();
1538
1539    macro_rules! go {
1540        ($severity:expr) => {
1541            cx.update_editor(|editor, window, cx| {
1542                editor.go_to_diagnostic(
1543                    &GoToDiagnostic {
1544                        severity: $severity,
1545                    },
1546                    window,
1547                    cx,
1548                );
1549            });
1550        };
1551    }
1552
1553    // Default, should cycle through all diagnostics
1554    go!(GoToDiagnosticSeverityFilter::default());
1555    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1556    go!(GoToDiagnosticSeverityFilter::default());
1557    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1558    go!(GoToDiagnosticSeverityFilter::default());
1559    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1560    go!(GoToDiagnosticSeverityFilter::default());
1561    cx.assert_editor_state(indoc! {"error warning info ˇhint"});
1562    go!(GoToDiagnosticSeverityFilter::default());
1563    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1564
1565    let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
1566    go!(only_info);
1567    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1568    go!(only_info);
1569    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1570
1571    let no_hints = GoToDiagnosticSeverityFilter::Range {
1572        min: GoToDiagnosticSeverity::Information,
1573        max: GoToDiagnosticSeverity::Error,
1574    };
1575
1576    go!(no_hints);
1577    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1578    go!(no_hints);
1579    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1580    go!(no_hints);
1581    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1582    go!(no_hints);
1583    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1584
1585    let warning_info = GoToDiagnosticSeverityFilter::Range {
1586        min: GoToDiagnosticSeverity::Information,
1587        max: GoToDiagnosticSeverity::Warning,
1588    };
1589
1590    go!(warning_info);
1591    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1592    go!(warning_info);
1593    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1594    go!(warning_info);
1595    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1596}
1597
1598#[gpui::test]
1599async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
1600    init_test(cx);
1601
1602    // We'll be creating two different files, both with diagnostics, so we can
1603    // later verify that, since the `BufferDiagnosticsEditor` only shows
1604    // diagnostics for the provided path, the diagnostics for the other file
1605    // will not be shown, contrary to what happens with
1606    // `ProjectDiagnosticsEditor`.
1607    let fs = FakeFs::new(cx.executor());
1608    fs.insert_tree(
1609        path!("/test"),
1610        json!({
1611            "main.rs": "
1612                fn main() {
1613                    let x = vec![];
1614                    let y = vec![];
1615                    a(x);
1616                    b(y);
1617                    c(y);
1618                    d(x);
1619                }
1620            "
1621            .unindent(),
1622            "other.rs": "
1623                fn other() {
1624                    let unused = 42;
1625                    undefined_function();
1626                }
1627            "
1628            .unindent(),
1629        }),
1630    )
1631    .await;
1632
1633    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1634    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1635    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1636    let project_path = project::ProjectPath {
1637        worktree_id: project.read_with(cx, |project, cx| {
1638            project.worktrees(cx).next().unwrap().read(cx).id()
1639        }),
1640        path: rel_path("main.rs").into(),
1641    };
1642    let buffer = project
1643        .update(cx, |project, cx| {
1644            project.open_buffer(project_path.clone(), cx)
1645        })
1646        .await
1647        .ok();
1648
1649    // Create the diagnostics for `main.rs`.
1650    let language_server_id = LanguageServerId(0);
1651    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1652    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1653
1654    lsp_store.update(cx, |lsp_store, cx| {
1655        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1656            uri: uri.clone(),
1657            diagnostics: vec![
1658                lsp::Diagnostic{
1659                    range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1660                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1661                    message: "use of moved value\nvalue used here after move".to_string(),
1662                    related_information: Some(vec![
1663                        lsp::DiagnosticRelatedInformation {
1664                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1665                            message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1666                        },
1667                        lsp::DiagnosticRelatedInformation {
1668                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1669                            message: "value moved here".to_string()
1670                        },
1671                    ]),
1672                    ..Default::default()
1673                },
1674                lsp::Diagnostic{
1675                    range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1676                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1677                    message: "use of moved value\nvalue used here after move".to_string(),
1678                    related_information: Some(vec![
1679                        lsp::DiagnosticRelatedInformation {
1680                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1681                            message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1682                        },
1683                        lsp::DiagnosticRelatedInformation {
1684                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1685                            message: "value moved here".to_string()
1686                        },
1687                    ]),
1688                    ..Default::default()
1689                }
1690            ],
1691            version: None
1692        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1693
1694        // Create diagnostics for other.rs to ensure that the file and
1695        // diagnostics are not included in `BufferDiagnosticsEditor` when it is
1696        // deployed for main.rs.
1697        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1698            uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
1699            diagnostics: vec![
1700                lsp::Diagnostic{
1701                    range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
1702                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1703                    message: "unused variable: `unused`".to_string(),
1704                    ..Default::default()
1705                },
1706                lsp::Diagnostic{
1707                    range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
1708                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1709                    message: "cannot find function `undefined_function` in this scope".to_string(),
1710                    ..Default::default()
1711                }
1712            ],
1713            version: None
1714        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1715    });
1716
1717    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1718        BufferDiagnosticsEditor::new(
1719            project_path.clone(),
1720            project.clone(),
1721            buffer,
1722            true,
1723            window,
1724            cx,
1725        )
1726    });
1727    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1728        buffer_diagnostics.editor().clone()
1729    });
1730
1731    // Since the excerpt updates is handled by a background task, we need to
1732    // wait a little bit to ensure that the buffer diagnostic's editor content
1733    // is rendered.
1734    cx.executor()
1735        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1736
1737    pretty_assertions::assert_eq!(
1738        editor_content_with_blocks(&editor, cx),
1739        indoc::indoc! {
1740            "§ main.rs
1741             § -----
1742             fn main() {
1743                 let x = vec![];
1744             § move occurs because `x` has type `Vec<char>`, which does not implement
1745             § the `Copy` trait (back)
1746                 let y = vec![];
1747             § move occurs because `y` has type `Vec<char>`, which does not implement
1748             § the `Copy` trait
1749                 a(x); § value moved here
1750                 b(y); § value moved here
1751                 c(y);
1752             § use of moved value
1753             § value used here after move
1754                 d(x);
1755             § use of moved value
1756             § value used here after move
1757             § hint: move occurs because `x` has type `Vec<char>`, which does not
1758             § implement the `Copy` trait
1759             }"
1760        }
1761    );
1762}
1763
1764#[gpui::test]
1765async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
1766    init_test(cx);
1767
1768    let fs = FakeFs::new(cx.executor());
1769    fs.insert_tree(
1770        path!("/test"),
1771        json!({
1772            "main.rs": "
1773                fn main() {
1774                    let x = vec![];
1775                    let y = vec![];
1776                    a(x);
1777                    b(y);
1778                    c(y);
1779                    d(x);
1780                }
1781            "
1782            .unindent(),
1783        }),
1784    )
1785    .await;
1786
1787    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1788    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1789    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1790    let project_path = project::ProjectPath {
1791        worktree_id: project.read_with(cx, |project, cx| {
1792            project.worktrees(cx).next().unwrap().read(cx).id()
1793        }),
1794        path: rel_path("main.rs").into(),
1795    };
1796    let buffer = project
1797        .update(cx, |project, cx| {
1798            project.open_buffer(project_path.clone(), cx)
1799        })
1800        .await
1801        .ok();
1802
1803    let language_server_id = LanguageServerId(0);
1804    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1805    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1806
1807    lsp_store.update(cx, |lsp_store, cx| {
1808        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1809            uri: uri.clone(),
1810            diagnostics: vec![
1811                lsp::Diagnostic{
1812                    range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1813                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1814                    message: "use of moved value\nvalue used here after move".to_string(),
1815                    related_information: Some(vec![
1816                        lsp::DiagnosticRelatedInformation {
1817                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1818                            message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1819                        },
1820                        lsp::DiagnosticRelatedInformation {
1821                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1822                            message: "value moved here".to_string()
1823                        },
1824                    ]),
1825                    ..Default::default()
1826                },
1827                lsp::Diagnostic{
1828                    range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1829                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1830                    message: "use of moved value\nvalue used here after move".to_string(),
1831                    related_information: Some(vec![
1832                        lsp::DiagnosticRelatedInformation {
1833                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1834                            message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1835                        },
1836                        lsp::DiagnosticRelatedInformation {
1837                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1838                            message: "value moved here".to_string()
1839                        },
1840                    ]),
1841                    ..Default::default()
1842                }
1843            ],
1844            version: None
1845        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1846    });
1847
1848    let include_warnings = false;
1849    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1850        BufferDiagnosticsEditor::new(
1851            project_path.clone(),
1852            project.clone(),
1853            buffer,
1854            include_warnings,
1855            window,
1856            cx,
1857        )
1858    });
1859
1860    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
1861        buffer_diagnostics.editor().clone()
1862    });
1863
1864    // Since the excerpt updates is handled by a background task, we need to
1865    // wait a little bit to ensure that the buffer diagnostic's editor content
1866    // is rendered.
1867    cx.executor()
1868        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1869
1870    pretty_assertions::assert_eq!(
1871        editor_content_with_blocks(&editor, cx),
1872        indoc::indoc! {
1873            "§ main.rs
1874             § -----
1875             fn main() {
1876                 let x = vec![];
1877             § move occurs because `x` has type `Vec<char>`, which does not implement
1878             § the `Copy` trait (back)
1879                 let y = vec![];
1880                 a(x); § value moved here
1881                 b(y);
1882                 c(y);
1883                 d(x);
1884             § use of moved value
1885             § value used here after move
1886             § hint: move occurs because `x` has type `Vec<char>`, which does not
1887             § implement the `Copy` trait
1888             }"
1889        }
1890    );
1891}
1892
1893#[gpui::test]
1894async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1895    init_test(cx);
1896
1897    let fs = FakeFs::new(cx.executor());
1898    fs.insert_tree(
1899        path!("/test"),
1900        json!({
1901            "main.rs": "
1902                fn main() {
1903                    let x = vec![];
1904                    let y = vec![];
1905                    a(x);
1906                    b(y);
1907                    c(y);
1908                    d(x);
1909                }
1910            "
1911            .unindent(),
1912        }),
1913    )
1914    .await;
1915
1916    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1917    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1918    let cx = &mut VisualTestContext::from_window(window.into(), cx);
1919    let project_path = project::ProjectPath {
1920        worktree_id: project.read_with(cx, |project, cx| {
1921            project.worktrees(cx).next().unwrap().read(cx).id()
1922        }),
1923        path: rel_path("main.rs").into(),
1924    };
1925    let buffer = project
1926        .update(cx, |project, cx| {
1927            project.open_buffer(project_path.clone(), cx)
1928        })
1929        .await
1930        .ok();
1931
1932    // Create the diagnostics for `main.rs`.
1933    // Two warnings are being created, one for each language server, in order to
1934    // assert that both warnings are rendered in the editor.
1935    let language_server_id_a = LanguageServerId(0);
1936    let language_server_id_b = LanguageServerId(1);
1937    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1938    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1939
1940    lsp_store.update(cx, |lsp_store, cx| {
1941        lsp_store
1942            .update_diagnostics(
1943                language_server_id_a,
1944                lsp::PublishDiagnosticsParams {
1945                    uri: uri.clone(),
1946                    diagnostics: vec![lsp::Diagnostic {
1947                        range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1948                        severity: Some(lsp::DiagnosticSeverity::WARNING),
1949                        message: "use of moved value\nvalue used here after move".to_string(),
1950                        related_information: None,
1951                        ..Default::default()
1952                    }],
1953                    version: None,
1954                },
1955                None,
1956                DiagnosticSourceKind::Pushed,
1957                &[],
1958                cx,
1959            )
1960            .unwrap();
1961
1962        lsp_store
1963            .update_diagnostics(
1964                language_server_id_b,
1965                lsp::PublishDiagnosticsParams {
1966                    uri: uri.clone(),
1967                    diagnostics: vec![lsp::Diagnostic {
1968                        range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1969                        severity: Some(lsp::DiagnosticSeverity::WARNING),
1970                        message: "use of moved value\nvalue used here after move".to_string(),
1971                        related_information: None,
1972                        ..Default::default()
1973                    }],
1974                    version: None,
1975                },
1976                None,
1977                DiagnosticSourceKind::Pushed,
1978                &[],
1979                cx,
1980            )
1981            .unwrap();
1982    });
1983
1984    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1985        BufferDiagnosticsEditor::new(
1986            project_path.clone(),
1987            project.clone(),
1988            buffer,
1989            true,
1990            window,
1991            cx,
1992        )
1993    });
1994    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1995        buffer_diagnostics.editor().clone()
1996    });
1997
1998    // Since the excerpt updates is handled by a background task, we need to
1999    // wait a little bit to ensure that the buffer diagnostic's editor content
2000    // is rendered.
2001    cx.executor()
2002        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
2003
2004    pretty_assertions::assert_eq!(
2005        editor_content_with_blocks(&editor, cx),
2006        indoc::indoc! {
2007            "§ main.rs
2008             § -----
2009                 a(x);
2010                 b(y);
2011                 c(y);
2012             § use of moved value
2013             § value used here after move
2014                 d(x);
2015             § use of moved value
2016             § value used here after move
2017             }"
2018        }
2019    );
2020
2021    buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
2022        assert_eq!(
2023            *buffer_diagnostics.summary(),
2024            DiagnosticSummary {
2025                warning_count: 2,
2026                error_count: 0
2027            }
2028        );
2029    })
2030}
2031
2032fn init_test(cx: &mut TestAppContext) {
2033    cx.update(|cx| {
2034        zlog::init_test();
2035        let settings = SettingsStore::test(cx);
2036        cx.set_global(settings);
2037        theme::init(theme::LoadThemes::JustBase, cx);
2038        crate::init(cx);
2039        editor::init(cx);
2040    });
2041}
2042
2043fn randomly_update_diagnostics_for_path(
2044    fs: &FakeFs,
2045    path: &Path,
2046    diagnostics: &mut Vec<lsp::Diagnostic>,
2047    next_id: &mut usize,
2048    rng: &mut impl Rng,
2049) {
2050    let mutation_count = rng.random_range(1..=3);
2051    for _ in 0..mutation_count {
2052        if rng.random_bool(0.3) && !diagnostics.is_empty() {
2053            let idx = rng.random_range(0..diagnostics.len());
2054            log::info!("  removing diagnostic at index {idx}");
2055            diagnostics.remove(idx);
2056        } else {
2057            let unique_id = *next_id;
2058            *next_id += 1;
2059
2060            let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
2061
2062            let ix = rng.random_range(0..=diagnostics.len());
2063            log::info!(
2064                "  inserting {} at index {ix}. {},{}..{},{}",
2065                new_diagnostic.message,
2066                new_diagnostic.range.start.line,
2067                new_diagnostic.range.start.character,
2068                new_diagnostic.range.end.line,
2069                new_diagnostic.range.end.character,
2070            );
2071            for related in new_diagnostic.related_information.iter().flatten() {
2072                log::info!(
2073                    "   {}. {},{}..{},{}",
2074                    related.message,
2075                    related.location.range.start.line,
2076                    related.location.range.start.character,
2077                    related.location.range.end.line,
2078                    related.location.range.end.character,
2079                );
2080            }
2081            diagnostics.insert(ix, new_diagnostic);
2082        }
2083    }
2084}
2085
2086fn random_lsp_diagnostic(
2087    rng: &mut impl Rng,
2088    fs: &FakeFs,
2089    path: &Path,
2090    unique_id: usize,
2091) -> lsp::Diagnostic {
2092    // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
2093    // because language servers can potentially give us those, and we should handle them gracefully.
2094    const ERROR_MARGIN: usize = 10;
2095
2096    let file_content = fs.read_file_sync(path).unwrap();
2097    let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
2098
2099    let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2100    let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));
2101
2102    let start_point = file_text.offset_to_point_utf16(start);
2103    let end_point = file_text.offset_to_point_utf16(end);
2104
2105    let range = lsp::Range::new(
2106        lsp::Position::new(start_point.row, start_point.column),
2107        lsp::Position::new(end_point.row, end_point.column),
2108    );
2109
2110    let severity = if rng.random_bool(0.5) {
2111        Some(lsp::DiagnosticSeverity::ERROR)
2112    } else {
2113        Some(lsp::DiagnosticSeverity::WARNING)
2114    };
2115
2116    let message = format!("diagnostic {unique_id}");
2117
2118    let related_information = if rng.random_bool(0.3) {
2119        let info_count = rng.random_range(1..=3);
2120        let mut related_info = Vec::with_capacity(info_count);
2121
2122        for i in 0..info_count {
2123            let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2124            let info_end =
2125                rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
2126
2127            let info_start_point = file_text.offset_to_point_utf16(info_start);
2128            let info_end_point = file_text.offset_to_point_utf16(info_end);
2129
2130            let info_range = lsp::Range::new(
2131                lsp::Position::new(info_start_point.row, info_start_point.column),
2132                lsp::Position::new(info_end_point.row, info_end_point.column),
2133            );
2134
2135            related_info.push(lsp::DiagnosticRelatedInformation {
2136                location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range),
2137                message: format!("related info {i} for diagnostic {unique_id}"),
2138            });
2139        }
2140
2141        Some(related_info)
2142    } else {
2143        None
2144    };
2145
2146    lsp::Diagnostic {
2147        range,
2148        severity,
2149        message,
2150        related_information,
2151        data: None,
2152        ..Default::default()
2153    }
2154}