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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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_DEBOUNCE + 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(RetainExcerpts::No, 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_DEBOUNCE + 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| {
 793        // ignore $ ---- and $ <file>.rs
 794        !line.starts_with('§')
 795            || line.starts_with("§ diagnostic")
 796            || line.starts_with("§ related info")
 797    });
 798    let mut next_ref_line = ref_iter.next();
 799    let mut skipped_block = false;
 800
 801    for mut_line in mutated_excerpts.lines() {
 802        if let Some(ref_line) = next_ref_line {
 803            if mut_line == ref_line {
 804                next_ref_line = ref_iter.next();
 805            } else if mut_line.contains('§')
 806                // ignore $ ---- and $ <file>.rs
 807                && (!mut_line.starts_with('§')
 808                    || mut_line.starts_with("§ diagnostic")
 809                    || mut_line.starts_with("§ related info"))
 810            {
 811                skipped_block = true;
 812            }
 813        }
 814    }
 815
 816    if next_ref_line.is_some() || skipped_block {
 817        pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts);
 818    }
 819}
 820
 821// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
 822#[gpui::test]
 823async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
 824    init_test(cx);
 825
 826    let operations = env::var("OPERATIONS")
 827        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 828        .unwrap_or(10);
 829
 830    let fs = FakeFs::new(cx.executor());
 831    fs.insert_tree(path!("/test"), json!({})).await;
 832
 833    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 834    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 835    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 836    let cx = &mut VisualTestContext::from_window(*window, cx);
 837    let workspace = window.root(cx).unwrap();
 838
 839    let mutated_diagnostics = window.build_entity(cx, |window, cx| {
 840        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
 841    });
 842
 843    workspace.update_in(cx, |workspace, window, cx| {
 844        workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
 845    });
 846    mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
 847        assert!(diagnostics.focus_handle.is_focused(window));
 848    });
 849
 850    let mut next_id = 0;
 851    let mut next_filename = 0;
 852    let mut language_server_ids = vec![LanguageServerId(0)];
 853    let mut updated_language_servers = HashSet::default();
 854    let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
 855        Default::default();
 856    let mut next_inlay_id = 0;
 857
 858    for _ in 0..operations {
 859        match rng.random_range(0..100) {
 860            // language server completes its diagnostic check
 861            0..=20 if !updated_language_servers.is_empty() => {
 862                let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
 863                log::info!("finishing diagnostic check for language server {server_id}");
 864                lsp_store.update(cx, |lsp_store, cx| {
 865                    lsp_store.disk_based_diagnostics_finished(server_id, cx)
 866                });
 867
 868                if rng.random_bool(0.5) {
 869                    cx.run_until_parked();
 870                }
 871            }
 872
 873            21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 874                diagnostics.editor.update(cx, |editor, cx| {
 875                    let snapshot = editor.snapshot(window, cx);
 876                    if !snapshot.buffer_snapshot().is_empty() {
 877                        let position = rng.random_range(0..snapshot.buffer_snapshot().len());
 878                        let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
 879                        log::info!(
 880                            "adding inlay at {position}/{}: {:?}",
 881                            snapshot.buffer_snapshot().len(),
 882                            snapshot.buffer_snapshot().text(),
 883                        );
 884
 885                        editor.splice_inlays(
 886                            &[],
 887                            vec![Inlay::edit_prediction(
 888                                post_inc(&mut next_inlay_id),
 889                                snapshot.buffer_snapshot().anchor_before(position),
 890                                Rope::from_iter_small(["Test inlay ", "next_inlay_id"]),
 891                            )],
 892                            cx,
 893                        );
 894                    }
 895                });
 896            }),
 897
 898            // language server updates diagnostics
 899            _ => {
 900                let (path, server_id, diagnostics) =
 901                    match current_diagnostics.iter_mut().choose(&mut rng) {
 902                        // update existing set of diagnostics
 903                        Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
 904                            (path.clone(), *server_id, diagnostics)
 905                        }
 906
 907                        // insert a set of diagnostics for a new path
 908                        _ => {
 909                            let path: PathBuf =
 910                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
 911                            let len = rng.random_range(128..256);
 912                            let content =
 913                                RandomCharIter::new(&mut rng).take(len).collect::<String>();
 914                            fs.insert_file(&path, content.into_bytes()).await;
 915
 916                            let server_id = match language_server_ids.iter().choose(&mut rng) {
 917                                Some(server_id) if rng.random_bool(0.5) => *server_id,
 918                                _ => {
 919                                    let id = LanguageServerId(language_server_ids.len());
 920                                    language_server_ids.push(id);
 921                                    id
 922                                }
 923                            };
 924
 925                            (
 926                                path.clone(),
 927                                server_id,
 928                                current_diagnostics.entry((path, server_id)).or_default(),
 929                            )
 930                        }
 931                    };
 932
 933                updated_language_servers.insert(server_id);
 934
 935                lsp_store.update(cx, |lsp_store, cx| {
 936                    log::info!("updating diagnostics. language server {server_id} path {path:?}");
 937                    randomly_update_diagnostics_for_path(
 938                        &fs,
 939                        &path,
 940                        diagnostics,
 941                        &mut next_id,
 942                        &mut rng,
 943                    );
 944                    lsp_store
 945                        .update_diagnostics(
 946                            server_id,
 947                            lsp::PublishDiagnosticsParams {
 948                                uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
 949                                    lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
 950                                }),
 951                                diagnostics: diagnostics.clone(),
 952                                version: None,
 953                            },
 954                            None,
 955                            DiagnosticSourceKind::Pushed,
 956                            &[],
 957                            cx,
 958                        )
 959                        .unwrap()
 960                });
 961                cx.executor()
 962                    .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 963
 964                cx.run_until_parked();
 965            }
 966        }
 967    }
 968
 969    log::info!("updating mutated diagnostics view");
 970    mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
 971        diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
 972    });
 973
 974    cx.executor()
 975        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 976    cx.run_until_parked();
 977}
 978
 979#[gpui::test]
 980async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
 981    init_test(cx);
 982
 983    let mut cx = EditorTestContext::new(cx).await;
 984    let lsp_store =
 985        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 986
 987    cx.set_state(indoc! {"
 988        ˇfn func(abc def: i32) -> u32 {
 989        }
 990    "});
 991
 992    let message = "Something's wrong!";
 993    cx.update(|_, cx| {
 994        lsp_store.update(cx, |lsp_store, cx| {
 995            lsp_store
 996                .update_diagnostics(
 997                    LanguageServerId(0),
 998                    lsp::PublishDiagnosticsParams {
 999                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1000                        version: None,
1001                        diagnostics: vec![lsp::Diagnostic {
1002                            range: lsp::Range::new(
1003                                lsp::Position::new(0, 11),
1004                                lsp::Position::new(0, 12),
1005                            ),
1006                            severity: Some(lsp::DiagnosticSeverity::ERROR),
1007                            message: message.to_string(),
1008                            ..Default::default()
1009                        }],
1010                    },
1011                    None,
1012                    DiagnosticSourceKind::Pushed,
1013                    &[],
1014                    cx,
1015                )
1016                .unwrap()
1017        });
1018    });
1019    cx.run_until_parked();
1020
1021    cx.update_editor(|editor, window, cx| {
1022        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1023        assert_eq!(
1024            editor
1025                .active_diagnostic_group()
1026                .map(|diagnostics_group| diagnostics_group.active_message.as_str()),
1027            Some(message),
1028            "Should have a diagnostics group activated"
1029        );
1030    });
1031    cx.assert_editor_state(indoc! {"
1032        fn func(abcˇ def: i32) -> u32 {
1033        }
1034    "});
1035
1036    cx.update(|_, cx| {
1037        lsp_store.update(cx, |lsp_store, cx| {
1038            lsp_store
1039                .update_diagnostics(
1040                    LanguageServerId(0),
1041                    lsp::PublishDiagnosticsParams {
1042                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1043                        version: None,
1044                        diagnostics: Vec::new(),
1045                    },
1046                    None,
1047                    DiagnosticSourceKind::Pushed,
1048                    &[],
1049                    cx,
1050                )
1051                .unwrap()
1052        });
1053    });
1054    cx.run_until_parked();
1055    cx.update_editor(|editor, _, _| {
1056        assert_eq!(editor.active_diagnostic_group(), None);
1057    });
1058    cx.assert_editor_state(indoc! {"
1059        fn func(abcˇ def: i32) -> u32 {
1060        }
1061    "});
1062
1063    cx.update_editor(|editor, window, cx| {
1064        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1065        assert_eq!(editor.active_diagnostic_group(), None);
1066    });
1067    cx.assert_editor_state(indoc! {"
1068        fn func(abcˇ def: i32) -> u32 {
1069        }
1070    "});
1071}
1072
1073#[gpui::test]
1074async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
1075    init_test(cx);
1076
1077    let mut cx = EditorTestContext::new(cx).await;
1078    let lsp_store =
1079        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1080
1081    cx.set_state(indoc! {"
1082        ˇfn func(abc def: i32) -> u32 {
1083        }
1084    "});
1085
1086    cx.update(|_, cx| {
1087        lsp_store.update(cx, |lsp_store, cx| {
1088            lsp_store
1089                .update_diagnostics(
1090                    LanguageServerId(0),
1091                    lsp::PublishDiagnosticsParams {
1092                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1093                        version: None,
1094                        diagnostics: vec![
1095                            lsp::Diagnostic {
1096                                range: lsp::Range::new(
1097                                    lsp::Position::new(0, 11),
1098                                    lsp::Position::new(0, 12),
1099                                ),
1100                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1101                                ..Default::default()
1102                            },
1103                            lsp::Diagnostic {
1104                                range: lsp::Range::new(
1105                                    lsp::Position::new(0, 12),
1106                                    lsp::Position::new(0, 15),
1107                                ),
1108                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1109                                ..Default::default()
1110                            },
1111                            lsp::Diagnostic {
1112                                range: lsp::Range::new(
1113                                    lsp::Position::new(0, 12),
1114                                    lsp::Position::new(0, 15),
1115                                ),
1116                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1117                                ..Default::default()
1118                            },
1119                            lsp::Diagnostic {
1120                                range: lsp::Range::new(
1121                                    lsp::Position::new(0, 25),
1122                                    lsp::Position::new(0, 28),
1123                                ),
1124                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1125                                ..Default::default()
1126                            },
1127                        ],
1128                    },
1129                    None,
1130                    DiagnosticSourceKind::Pushed,
1131                    &[],
1132                    cx,
1133                )
1134                .unwrap()
1135        });
1136    });
1137    cx.run_until_parked();
1138
1139    //// Backward
1140
1141    // Fourth diagnostic
1142    cx.update_editor(|editor, window, cx| {
1143        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1144    });
1145    cx.assert_editor_state(indoc! {"
1146        fn func(abc def: i32) -> ˇu32 {
1147        }
1148    "});
1149
1150    // Third diagnostic
1151    cx.update_editor(|editor, window, cx| {
1152        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1153    });
1154    cx.assert_editor_state(indoc! {"
1155        fn func(abc ˇdef: i32) -> u32 {
1156        }
1157    "});
1158
1159    // Second diagnostic, same place
1160    cx.update_editor(|editor, window, cx| {
1161        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1162    });
1163    cx.assert_editor_state(indoc! {"
1164        fn func(abc ˇdef: i32) -> u32 {
1165        }
1166    "});
1167
1168    // First diagnostic
1169    cx.update_editor(|editor, window, cx| {
1170        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1171    });
1172    cx.assert_editor_state(indoc! {"
1173        fn func(abcˇ def: i32) -> u32 {
1174        }
1175    "});
1176
1177    // Wrapped over, fourth diagnostic
1178    cx.update_editor(|editor, window, cx| {
1179        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
1180    });
1181    cx.assert_editor_state(indoc! {"
1182        fn func(abc def: i32) -> ˇu32 {
1183        }
1184    "});
1185
1186    cx.update_editor(|editor, window, cx| {
1187        editor.move_to_beginning(&MoveToBeginning, window, cx);
1188    });
1189    cx.assert_editor_state(indoc! {"
1190        ˇfn func(abc def: i32) -> u32 {
1191        }
1192    "});
1193
1194    //// Forward
1195
1196    // First diagnostic
1197    cx.update_editor(|editor, window, cx| {
1198        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1199    });
1200    cx.assert_editor_state(indoc! {"
1201        fn func(abcˇ def: i32) -> u32 {
1202        }
1203    "});
1204
1205    // Second diagnostic
1206    cx.update_editor(|editor, window, cx| {
1207        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1208    });
1209    cx.assert_editor_state(indoc! {"
1210        fn func(abc ˇdef: i32) -> u32 {
1211        }
1212    "});
1213
1214    // Third diagnostic, same place
1215    cx.update_editor(|editor, window, cx| {
1216        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1217    });
1218    cx.assert_editor_state(indoc! {"
1219        fn func(abc ˇdef: i32) -> u32 {
1220        }
1221    "});
1222
1223    // Fourth diagnostic
1224    cx.update_editor(|editor, window, cx| {
1225        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1226    });
1227    cx.assert_editor_state(indoc! {"
1228        fn func(abc def: i32) -> ˇu32 {
1229        }
1230    "});
1231
1232    // Wrapped around, first diagnostic
1233    cx.update_editor(|editor, window, cx| {
1234        editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
1235    });
1236    cx.assert_editor_state(indoc! {"
1237        fn func(abcˇ def: i32) -> u32 {
1238        }
1239    "});
1240}
1241
1242#[gpui::test]
1243async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
1244    init_test(cx);
1245
1246    let mut cx = EditorTestContext::new(cx).await;
1247
1248    cx.set_state(indoc! {"
1249        fn func(abˇc def: i32) -> u32 {
1250        }
1251    "});
1252    let lsp_store =
1253        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1254
1255    cx.update(|_, cx| {
1256        lsp_store.update(cx, |lsp_store, cx| {
1257            lsp_store.update_diagnostics(
1258                LanguageServerId(0),
1259                lsp::PublishDiagnosticsParams {
1260                    uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1261                    version: None,
1262                    diagnostics: vec![lsp::Diagnostic {
1263                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
1264                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1265                        message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
1266                        ..Default::default()
1267                    }],
1268                },
1269                None,
1270                DiagnosticSourceKind::Pushed,
1271                &[],
1272                cx,
1273            )
1274        })
1275    }).unwrap();
1276    cx.run_until_parked();
1277    cx.update_editor(|editor, window, cx| {
1278        editor::hover_popover::hover(editor, &Default::default(), window, cx)
1279    });
1280    cx.run_until_parked();
1281    cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
1282}
1283
1284#[gpui::test]
1285async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
1286    init_test(cx);
1287
1288    let mut cx = EditorLspTestContext::new_rust(
1289        lsp::ServerCapabilities {
1290            hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1291            ..Default::default()
1292        },
1293        cx,
1294    )
1295    .await;
1296
1297    // Hover with just diagnostic, pops DiagnosticPopover immediately and then
1298    // info popover once request completes
1299    cx.set_state(indoc! {"
1300        fn teˇst() { println!(); }
1301    "});
1302    // Send diagnostic to client
1303    let range = cx.lsp_range(indoc! {"
1304        fn «test»() { println!(); }
1305    "});
1306    let lsp_store =
1307        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1308    cx.update(|_, cx| {
1309        lsp_store.update(cx, |lsp_store, cx| {
1310            lsp_store.update_diagnostics(
1311                LanguageServerId(0),
1312                lsp::PublishDiagnosticsParams {
1313                    uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
1314                    version: None,
1315                    diagnostics: vec![lsp::Diagnostic {
1316                        range,
1317                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1318                        message: "A test diagnostic message.".to_string(),
1319                        ..Default::default()
1320                    }],
1321                },
1322                None,
1323                DiagnosticSourceKind::Pushed,
1324                &[],
1325                cx,
1326            )
1327        })
1328    })
1329    .unwrap();
1330    cx.run_until_parked();
1331
1332    // Hover pops diagnostic immediately
1333    cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx));
1334    cx.background_executor.run_until_parked();
1335
1336    cx.editor(|Editor { hover_state, .. }, _, _| {
1337        assert!(hover_state.diagnostic_popover.is_some());
1338        assert!(hover_state.info_popovers.is_empty());
1339    });
1340
1341    // Info Popover shows after request responded to
1342    let range = cx.lsp_range(indoc! {"
1343            fn «test»() { println!(); }
1344        "});
1345    cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
1346        Ok(Some(lsp::Hover {
1347            contents: lsp::HoverContents::Markup(lsp::MarkupContent {
1348                kind: lsp::MarkupKind::Markdown,
1349                value: "some new docs".to_string(),
1350            }),
1351            range: Some(range),
1352        }))
1353    });
1354    let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1);
1355    cx.background_executor
1356        .advance_clock(Duration::from_millis(delay));
1357
1358    cx.background_executor.run_until_parked();
1359    cx.editor(|Editor { hover_state, .. }, _, _| {
1360        hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
1361    });
1362}
1363#[gpui::test]
1364async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
1365    init_test(cx);
1366
1367    let fs = FakeFs::new(cx.executor());
1368    fs.insert_tree(
1369        path!("/root"),
1370        json!({
1371            "main.js": "
1372                function test() {
1373                    const x = 10;
1374                    const y = 20;
1375                    return 1;
1376                }
1377                test();
1378            "
1379            .unindent(),
1380        }),
1381    )
1382    .await;
1383
1384    let language_server_id = LanguageServerId(0);
1385    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1386    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1387    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1388    let cx = &mut VisualTestContext::from_window(*window, cx);
1389    let workspace = window.root(cx).unwrap();
1390    let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
1391
1392    // Create diagnostics with code fields
1393    lsp_store.update(cx, |lsp_store, cx| {
1394        lsp_store
1395            .update_diagnostics(
1396                language_server_id,
1397                lsp::PublishDiagnosticsParams {
1398                    uri: uri.clone(),
1399                    diagnostics: vec![
1400                        lsp::Diagnostic {
1401                            range: lsp::Range::new(
1402                                lsp::Position::new(1, 4),
1403                                lsp::Position::new(1, 14),
1404                            ),
1405                            severity: Some(lsp::DiagnosticSeverity::WARNING),
1406                            code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1407                            source: Some("eslint".to_string()),
1408                            message: "'x' is assigned a value but never used".to_string(),
1409                            ..Default::default()
1410                        },
1411                        lsp::Diagnostic {
1412                            range: lsp::Range::new(
1413                                lsp::Position::new(2, 4),
1414                                lsp::Position::new(2, 14),
1415                            ),
1416                            severity: Some(lsp::DiagnosticSeverity::WARNING),
1417                            code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())),
1418                            source: Some("eslint".to_string()),
1419                            message: "'y' is assigned a value but never used".to_string(),
1420                            ..Default::default()
1421                        },
1422                    ],
1423                    version: None,
1424                },
1425                None,
1426                DiagnosticSourceKind::Pushed,
1427                &[],
1428                cx,
1429            )
1430            .unwrap();
1431    });
1432
1433    // Open the project diagnostics view
1434    let diagnostics = window.build_entity(cx, |window, cx| {
1435        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
1436    });
1437    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
1438
1439    diagnostics
1440        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
1441        .await;
1442
1443    // Verify that the diagnostic codes are displayed correctly
1444    pretty_assertions::assert_eq!(
1445        editor_content_with_blocks(&editor, cx),
1446        indoc::indoc! {
1447            "§ main.js
1448             § -----
1449             function test() {
1450                 const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars)
1451                 const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars)
1452                 return 1;
1453             }"
1454        }
1455    );
1456}
1457
1458#[gpui::test]
1459async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
1460    init_test(cx);
1461
1462    let mut cx = EditorTestContext::new(cx).await;
1463    let lsp_store =
1464        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
1465
1466    cx.set_state(indoc! {"error warning info hiˇnt"});
1467
1468    cx.update(|_, cx| {
1469        lsp_store.update(cx, |lsp_store, cx| {
1470            lsp_store
1471                .update_diagnostics(
1472                    LanguageServerId(0),
1473                    lsp::PublishDiagnosticsParams {
1474                        uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
1475                        version: None,
1476                        diagnostics: vec![
1477                            lsp::Diagnostic {
1478                                range: lsp::Range::new(
1479                                    lsp::Position::new(0, 0),
1480                                    lsp::Position::new(0, 5),
1481                                ),
1482                                severity: Some(lsp::DiagnosticSeverity::ERROR),
1483                                ..Default::default()
1484                            },
1485                            lsp::Diagnostic {
1486                                range: lsp::Range::new(
1487                                    lsp::Position::new(0, 6),
1488                                    lsp::Position::new(0, 13),
1489                                ),
1490                                severity: Some(lsp::DiagnosticSeverity::WARNING),
1491                                ..Default::default()
1492                            },
1493                            lsp::Diagnostic {
1494                                range: lsp::Range::new(
1495                                    lsp::Position::new(0, 14),
1496                                    lsp::Position::new(0, 18),
1497                                ),
1498                                severity: Some(lsp::DiagnosticSeverity::INFORMATION),
1499                                ..Default::default()
1500                            },
1501                            lsp::Diagnostic {
1502                                range: lsp::Range::new(
1503                                    lsp::Position::new(0, 19),
1504                                    lsp::Position::new(0, 23),
1505                                ),
1506                                severity: Some(lsp::DiagnosticSeverity::HINT),
1507                                ..Default::default()
1508                            },
1509                        ],
1510                    },
1511                    None,
1512                    DiagnosticSourceKind::Pushed,
1513                    &[],
1514                    cx,
1515                )
1516                .unwrap()
1517        });
1518    });
1519    cx.run_until_parked();
1520
1521    macro_rules! go {
1522        ($severity:expr) => {
1523            cx.update_editor(|editor, window, cx| {
1524                editor.go_to_diagnostic(
1525                    &GoToDiagnostic {
1526                        severity: $severity,
1527                    },
1528                    window,
1529                    cx,
1530                );
1531            });
1532        };
1533    }
1534
1535    // Default, should cycle through all diagnostics
1536    go!(GoToDiagnosticSeverityFilter::default());
1537    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1538    go!(GoToDiagnosticSeverityFilter::default());
1539    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1540    go!(GoToDiagnosticSeverityFilter::default());
1541    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1542    go!(GoToDiagnosticSeverityFilter::default());
1543    cx.assert_editor_state(indoc! {"error warning info ˇhint"});
1544    go!(GoToDiagnosticSeverityFilter::default());
1545    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1546
1547    let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
1548    go!(only_info);
1549    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1550    go!(only_info);
1551    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1552
1553    let no_hints = GoToDiagnosticSeverityFilter::Range {
1554        min: GoToDiagnosticSeverity::Information,
1555        max: GoToDiagnosticSeverity::Error,
1556    };
1557
1558    go!(no_hints);
1559    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1560    go!(no_hints);
1561    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1562    go!(no_hints);
1563    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1564    go!(no_hints);
1565    cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
1566
1567    let warning_info = GoToDiagnosticSeverityFilter::Range {
1568        min: GoToDiagnosticSeverity::Information,
1569        max: GoToDiagnosticSeverity::Warning,
1570    };
1571
1572    go!(warning_info);
1573    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1574    go!(warning_info);
1575    cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
1576    go!(warning_info);
1577    cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
1578}
1579
1580#[gpui::test]
1581async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
1582    init_test(cx);
1583
1584    // We'll be creating two different files, both with diagnostics, so we can
1585    // later verify that, since the `BufferDiagnosticsEditor` only shows
1586    // diagnostics for the provided path, the diagnostics for the other file
1587    // will not be shown, contrary to what happens with
1588    // `ProjectDiagnosticsEditor`.
1589    let fs = FakeFs::new(cx.executor());
1590    fs.insert_tree(
1591        path!("/test"),
1592        json!({
1593            "main.rs": "
1594                fn main() {
1595                    let x = vec![];
1596                    let y = vec![];
1597                    a(x);
1598                    b(y);
1599                    c(y);
1600                    d(x);
1601                }
1602            "
1603            .unindent(),
1604            "other.rs": "
1605                fn other() {
1606                    let unused = 42;
1607                    undefined_function();
1608                }
1609            "
1610            .unindent(),
1611        }),
1612    )
1613    .await;
1614
1615    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1616    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1617    let cx = &mut VisualTestContext::from_window(*window, cx);
1618    let project_path = project::ProjectPath {
1619        worktree_id: project.read_with(cx, |project, cx| {
1620            project.worktrees(cx).next().unwrap().read(cx).id()
1621        }),
1622        path: rel_path("main.rs").into(),
1623    };
1624    let buffer = project
1625        .update(cx, |project, cx| {
1626            project.open_buffer(project_path.clone(), cx)
1627        })
1628        .await
1629        .ok();
1630
1631    // Create the diagnostics for `main.rs`.
1632    let language_server_id = LanguageServerId(0);
1633    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1634    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1635
1636    lsp_store.update(cx, |lsp_store, cx| {
1637        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1638            uri: uri.clone(),
1639            diagnostics: vec![
1640                lsp::Diagnostic{
1641                    range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1642                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1643                    message: "use of moved value\nvalue used here after move".to_string(),
1644                    related_information: Some(vec![
1645                        lsp::DiagnosticRelatedInformation {
1646                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1647                            message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1648                        },
1649                        lsp::DiagnosticRelatedInformation {
1650                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1651                            message: "value moved here".to_string()
1652                        },
1653                    ]),
1654                    ..Default::default()
1655                },
1656                lsp::Diagnostic{
1657                    range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1658                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1659                    message: "use of moved value\nvalue used here after move".to_string(),
1660                    related_information: Some(vec![
1661                        lsp::DiagnosticRelatedInformation {
1662                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1663                            message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1664                        },
1665                        lsp::DiagnosticRelatedInformation {
1666                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1667                            message: "value moved here".to_string()
1668                        },
1669                    ]),
1670                    ..Default::default()
1671                }
1672            ],
1673            version: None
1674        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1675
1676        // Create diagnostics for other.rs to ensure that the file and
1677        // diagnostics are not included in `BufferDiagnosticsEditor` when it is
1678        // deployed for main.rs.
1679        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1680            uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
1681            diagnostics: vec![
1682                lsp::Diagnostic{
1683                    range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
1684                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1685                    message: "unused variable: `unused`".to_string(),
1686                    ..Default::default()
1687                },
1688                lsp::Diagnostic{
1689                    range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
1690                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1691                    message: "cannot find function `undefined_function` in this scope".to_string(),
1692                    ..Default::default()
1693                }
1694            ],
1695            version: None
1696        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1697    });
1698
1699    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1700        BufferDiagnosticsEditor::new(
1701            project_path.clone(),
1702            project.clone(),
1703            buffer,
1704            true,
1705            window,
1706            cx,
1707        )
1708    });
1709    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1710        buffer_diagnostics.editor().clone()
1711    });
1712
1713    // Since the excerpt updates is handled by a background task, we need to
1714    // wait a little bit to ensure that the buffer diagnostic's editor content
1715    // is rendered.
1716    cx.executor()
1717        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1718
1719    pretty_assertions::assert_eq!(
1720        editor_content_with_blocks(&editor, cx),
1721        indoc::indoc! {
1722            "§ main.rs
1723             § -----
1724             fn main() {
1725                 let x = vec![];
1726             § move occurs because `x` has type `Vec<char>`, which does not implement
1727             § the `Copy` trait (back)
1728                 let y = vec![];
1729             § move occurs because `y` has type `Vec<char>`, which does not implement
1730             § the `Copy` trait
1731                 a(x); § value moved here
1732                 b(y); § value moved here
1733                 c(y);
1734             § use of moved value
1735             § value used here after move
1736                 d(x);
1737             § use of moved value
1738             § value used here after move
1739             § hint: move occurs because `x` has type `Vec<char>`, which does not
1740             § implement the `Copy` trait
1741             }"
1742        }
1743    );
1744}
1745
1746#[gpui::test]
1747async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
1748    init_test(cx);
1749
1750    let fs = FakeFs::new(cx.executor());
1751    fs.insert_tree(
1752        path!("/test"),
1753        json!({
1754            "main.rs": "
1755                fn main() {
1756                    let x = vec![];
1757                    let y = vec![];
1758                    a(x);
1759                    b(y);
1760                    c(y);
1761                    d(x);
1762                }
1763            "
1764            .unindent(),
1765        }),
1766    )
1767    .await;
1768
1769    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1770    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1771    let cx = &mut VisualTestContext::from_window(*window, cx);
1772    let project_path = project::ProjectPath {
1773        worktree_id: project.read_with(cx, |project, cx| {
1774            project.worktrees(cx).next().unwrap().read(cx).id()
1775        }),
1776        path: rel_path("main.rs").into(),
1777    };
1778    let buffer = project
1779        .update(cx, |project, cx| {
1780            project.open_buffer(project_path.clone(), cx)
1781        })
1782        .await
1783        .ok();
1784
1785    let language_server_id = LanguageServerId(0);
1786    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1787    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1788
1789    lsp_store.update(cx, |lsp_store, cx| {
1790        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
1791            uri: uri.clone(),
1792            diagnostics: vec![
1793                lsp::Diagnostic{
1794                    range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1795                    severity: Some(lsp::DiagnosticSeverity::WARNING),
1796                    message: "use of moved value\nvalue used here after move".to_string(),
1797                    related_information: Some(vec![
1798                        lsp::DiagnosticRelatedInformation {
1799                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
1800                            message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1801                        },
1802                        lsp::DiagnosticRelatedInformation {
1803                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
1804                            message: "value moved here".to_string()
1805                        },
1806                    ]),
1807                    ..Default::default()
1808                },
1809                lsp::Diagnostic{
1810                    range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1811                    severity: Some(lsp::DiagnosticSeverity::ERROR),
1812                    message: "use of moved value\nvalue used here after move".to_string(),
1813                    related_information: Some(vec![
1814                        lsp::DiagnosticRelatedInformation {
1815                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
1816                            message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
1817                        },
1818                        lsp::DiagnosticRelatedInformation {
1819                            location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
1820                            message: "value moved here".to_string()
1821                        },
1822                    ]),
1823                    ..Default::default()
1824                }
1825            ],
1826            version: None
1827        }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
1828    });
1829
1830    let include_warnings = false;
1831    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1832        BufferDiagnosticsEditor::new(
1833            project_path.clone(),
1834            project.clone(),
1835            buffer,
1836            include_warnings,
1837            window,
1838            cx,
1839        )
1840    });
1841
1842    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
1843        buffer_diagnostics.editor().clone()
1844    });
1845
1846    // Since the excerpt updates is handled by a background task, we need to
1847    // wait a little bit to ensure that the buffer diagnostic's editor content
1848    // is rendered.
1849    cx.executor()
1850        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1851
1852    pretty_assertions::assert_eq!(
1853        editor_content_with_blocks(&editor, cx),
1854        indoc::indoc! {
1855            "§ main.rs
1856             § -----
1857             fn main() {
1858                 let x = vec![];
1859             § move occurs because `x` has type `Vec<char>`, which does not implement
1860             § the `Copy` trait (back)
1861                 let y = vec![];
1862                 a(x); § value moved here
1863                 b(y);
1864                 c(y);
1865                 d(x);
1866             § use of moved value
1867             § value used here after move
1868             § hint: move occurs because `x` has type `Vec<char>`, which does not
1869             § implement the `Copy` trait
1870             }"
1871        }
1872    );
1873}
1874
1875#[gpui::test]
1876async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1877    init_test(cx);
1878
1879    let fs = FakeFs::new(cx.executor());
1880    fs.insert_tree(
1881        path!("/test"),
1882        json!({
1883            "main.rs": "
1884                fn main() {
1885                    let x = vec![];
1886                    let y = vec![];
1887                    a(x);
1888                    b(y);
1889                    c(y);
1890                    d(x);
1891                }
1892            "
1893            .unindent(),
1894        }),
1895    )
1896    .await;
1897
1898    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
1899    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1900    let cx = &mut VisualTestContext::from_window(*window, cx);
1901    let project_path = project::ProjectPath {
1902        worktree_id: project.read_with(cx, |project, cx| {
1903            project.worktrees(cx).next().unwrap().read(cx).id()
1904        }),
1905        path: rel_path("main.rs").into(),
1906    };
1907    let buffer = project
1908        .update(cx, |project, cx| {
1909            project.open_buffer(project_path.clone(), cx)
1910        })
1911        .await
1912        .ok();
1913
1914    // Create the diagnostics for `main.rs`.
1915    // Two warnings are being created, one for each language server, in order to
1916    // assert that both warnings are rendered in the editor.
1917    let language_server_id_a = LanguageServerId(0);
1918    let language_server_id_b = LanguageServerId(1);
1919    let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
1920    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1921
1922    lsp_store.update(cx, |lsp_store, cx| {
1923        lsp_store
1924            .update_diagnostics(
1925                language_server_id_a,
1926                lsp::PublishDiagnosticsParams {
1927                    uri: uri.clone(),
1928                    diagnostics: vec![lsp::Diagnostic {
1929                        range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
1930                        severity: Some(lsp::DiagnosticSeverity::WARNING),
1931                        message: "use of moved value\nvalue used here after move".to_string(),
1932                        related_information: None,
1933                        ..Default::default()
1934                    }],
1935                    version: None,
1936                },
1937                None,
1938                DiagnosticSourceKind::Pushed,
1939                &[],
1940                cx,
1941            )
1942            .unwrap();
1943
1944        lsp_store
1945            .update_diagnostics(
1946                language_server_id_b,
1947                lsp::PublishDiagnosticsParams {
1948                    uri: uri.clone(),
1949                    diagnostics: vec![lsp::Diagnostic {
1950                        range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
1951                        severity: Some(lsp::DiagnosticSeverity::WARNING),
1952                        message: "use of moved value\nvalue used here after move".to_string(),
1953                        related_information: None,
1954                        ..Default::default()
1955                    }],
1956                    version: None,
1957                },
1958                None,
1959                DiagnosticSourceKind::Pushed,
1960                &[],
1961                cx,
1962            )
1963            .unwrap();
1964    });
1965
1966    let buffer_diagnostics = window.build_entity(cx, |window, cx| {
1967        BufferDiagnosticsEditor::new(
1968            project_path.clone(),
1969            project.clone(),
1970            buffer,
1971            true,
1972            window,
1973            cx,
1974        )
1975    });
1976    let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
1977        buffer_diagnostics.editor().clone()
1978    });
1979
1980    // Since the excerpt updates is handled by a background task, we need to
1981    // wait a little bit to ensure that the buffer diagnostic's editor content
1982    // is rendered.
1983    cx.executor()
1984        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
1985
1986    pretty_assertions::assert_eq!(
1987        editor_content_with_blocks(&editor, cx),
1988        indoc::indoc! {
1989            "§ main.rs
1990             § -----
1991                 a(x);
1992                 b(y);
1993                 c(y);
1994             § use of moved value
1995             § value used here after move
1996                 d(x);
1997             § use of moved value
1998             § value used here after move
1999             }"
2000        }
2001    );
2002
2003    buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
2004        assert_eq!(
2005            *buffer_diagnostics.summary(),
2006            DiagnosticSummary {
2007                warning_count: 2,
2008                error_count: 0
2009            }
2010        );
2011    })
2012}
2013
2014fn init_test(cx: &mut TestAppContext) {
2015    cx.update(|cx| {
2016        zlog::init_test();
2017        let settings = SettingsStore::test(cx);
2018        cx.set_global(settings);
2019        theme::init(theme::LoadThemes::JustBase, cx);
2020        language::init(cx);
2021        client::init_settings(cx);
2022        workspace::init_settings(cx);
2023        Project::init_settings(cx);
2024        crate::init(cx);
2025        editor::init(cx);
2026    });
2027}
2028
2029fn randomly_update_diagnostics_for_path(
2030    fs: &FakeFs,
2031    path: &Path,
2032    diagnostics: &mut Vec<lsp::Diagnostic>,
2033    next_id: &mut usize,
2034    rng: &mut impl Rng,
2035) {
2036    let mutation_count = rng.random_range(1..=3);
2037    for _ in 0..mutation_count {
2038        if rng.random_bool(0.3) && !diagnostics.is_empty() {
2039            let idx = rng.random_range(0..diagnostics.len());
2040            log::info!("  removing diagnostic at index {idx}");
2041            diagnostics.remove(idx);
2042        } else {
2043            let unique_id = *next_id;
2044            *next_id += 1;
2045
2046            let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
2047
2048            let ix = rng.random_range(0..=diagnostics.len());
2049            log::info!(
2050                "  inserting {} at index {ix}. {},{}..{},{}",
2051                new_diagnostic.message,
2052                new_diagnostic.range.start.line,
2053                new_diagnostic.range.start.character,
2054                new_diagnostic.range.end.line,
2055                new_diagnostic.range.end.character,
2056            );
2057            for related in new_diagnostic.related_information.iter().flatten() {
2058                log::info!(
2059                    "   {}. {},{}..{},{}",
2060                    related.message,
2061                    related.location.range.start.line,
2062                    related.location.range.start.character,
2063                    related.location.range.end.line,
2064                    related.location.range.end.character,
2065                );
2066            }
2067            diagnostics.insert(ix, new_diagnostic);
2068        }
2069    }
2070}
2071
2072fn random_lsp_diagnostic(
2073    rng: &mut impl Rng,
2074    fs: &FakeFs,
2075    path: &Path,
2076    unique_id: usize,
2077) -> lsp::Diagnostic {
2078    // Intentionally allow erroneous ranges some of the time (that run off the end of the file),
2079    // because language servers can potentially give us those, and we should handle them gracefully.
2080    const ERROR_MARGIN: usize = 10;
2081
2082    let file_content = fs.read_file_sync(path).unwrap();
2083    let file_text = Rope::from_str_small(String::from_utf8_lossy(&file_content).as_ref());
2084
2085    let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2086    let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));
2087
2088    let start_point = file_text.offset_to_point_utf16(start);
2089    let end_point = file_text.offset_to_point_utf16(end);
2090
2091    let range = lsp::Range::new(
2092        lsp::Position::new(start_point.row, start_point.column),
2093        lsp::Position::new(end_point.row, end_point.column),
2094    );
2095
2096    let severity = if rng.random_bool(0.5) {
2097        Some(lsp::DiagnosticSeverity::ERROR)
2098    } else {
2099        Some(lsp::DiagnosticSeverity::WARNING)
2100    };
2101
2102    let message = format!("diagnostic {unique_id}");
2103
2104    let related_information = if rng.random_bool(0.3) {
2105        let info_count = rng.random_range(1..=3);
2106        let mut related_info = Vec::with_capacity(info_count);
2107
2108        for i in 0..info_count {
2109            let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
2110            let info_end =
2111                rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
2112
2113            let info_start_point = file_text.offset_to_point_utf16(info_start);
2114            let info_end_point = file_text.offset_to_point_utf16(info_end);
2115
2116            let info_range = lsp::Range::new(
2117                lsp::Position::new(info_start_point.row, info_start_point.column),
2118                lsp::Position::new(info_end_point.row, info_end_point.column),
2119            );
2120
2121            related_info.push(lsp::DiagnosticRelatedInformation {
2122                location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range),
2123                message: format!("related info {i} for diagnostic {unique_id}"),
2124            });
2125        }
2126
2127        Some(related_info)
2128    } else {
2129        None
2130    };
2131
2132    lsp::Diagnostic {
2133        range,
2134        severity,
2135        message,
2136        related_information,
2137        data: None,
2138        ..Default::default()
2139    }
2140}