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