diagnostics_tests.rs

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