diagnostics_tests.rs

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