project_tests.rs

   1use crate::{worktree::WorktreeHandle, Event, *};
   2use fs::LineEnding;
   3use fs::{FakeFs, RealFs};
   4use futures::{future, StreamExt};
   5use gpui::{executor::Deterministic, test::subscribe};
   6use language::{
   7    tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
   8    OffsetRangeExt, Point, ToPoint,
   9};
  10use lsp::Url;
  11use parking_lot::Mutex;
  12use pretty_assertions::assert_eq;
  13use serde_json::json;
  14use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
  15use unindent::Unindent as _;
  16use util::{assert_set_eq, test::temp_tree};
  17
  18#[cfg(test)]
  19#[ctor::ctor]
  20fn init_logger() {
  21    if std::env::var("RUST_LOG").is_ok() {
  22        env_logger::init();
  23    }
  24}
  25
  26#[gpui::test]
  27async fn test_symlinks(cx: &mut gpui::TestAppContext) {
  28    let dir = temp_tree(json!({
  29        "root": {
  30            "apple": "",
  31            "banana": {
  32                "carrot": {
  33                    "date": "",
  34                    "endive": "",
  35                }
  36            },
  37            "fennel": {
  38                "grape": "",
  39            }
  40        }
  41    }));
  42
  43    let root_link_path = dir.path().join("root_link");
  44    unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
  45    unix::fs::symlink(
  46        &dir.path().join("root/fennel"),
  47        &dir.path().join("root/finnochio"),
  48    )
  49    .unwrap();
  50
  51    let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
  52    project.read_with(cx, |project, cx| {
  53        let tree = project.worktrees(cx).next().unwrap().read(cx);
  54        assert_eq!(tree.file_count(), 5);
  55        assert_eq!(
  56            tree.inode_for_path("fennel/grape"),
  57            tree.inode_for_path("finnochio/grape")
  58        );
  59    });
  60}
  61
  62#[gpui::test]
  63async fn test_managing_language_servers(
  64    deterministic: Arc<Deterministic>,
  65    cx: &mut gpui::TestAppContext,
  66) {
  67    cx.foreground().forbid_parking();
  68
  69    let mut rust_language = Language::new(
  70        LanguageConfig {
  71            name: "Rust".into(),
  72            path_suffixes: vec!["rs".to_string()],
  73            ..Default::default()
  74        },
  75        Some(tree_sitter_rust::language()),
  76    );
  77    let mut json_language = Language::new(
  78        LanguageConfig {
  79            name: "JSON".into(),
  80            path_suffixes: vec!["json".to_string()],
  81            ..Default::default()
  82        },
  83        None,
  84    );
  85    let mut fake_rust_servers = rust_language
  86        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
  87            name: "the-rust-language-server",
  88            capabilities: lsp::ServerCapabilities {
  89                completion_provider: Some(lsp::CompletionOptions {
  90                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
  91                    ..Default::default()
  92                }),
  93                ..Default::default()
  94            },
  95            ..Default::default()
  96        }))
  97        .await;
  98    let mut fake_json_servers = json_language
  99        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 100            name: "the-json-language-server",
 101            capabilities: lsp::ServerCapabilities {
 102                completion_provider: Some(lsp::CompletionOptions {
 103                    trigger_characters: Some(vec![":".to_string()]),
 104                    ..Default::default()
 105                }),
 106                ..Default::default()
 107            },
 108            ..Default::default()
 109        }))
 110        .await;
 111
 112    let fs = FakeFs::new(cx.background());
 113    fs.insert_tree(
 114        "/the-root",
 115        json!({
 116            "test.rs": "const A: i32 = 1;",
 117            "test2.rs": "",
 118            "Cargo.toml": "a = 1",
 119            "package.json": "{\"a\": 1}",
 120        }),
 121    )
 122    .await;
 123
 124    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 125
 126    // Open a buffer without an associated language server.
 127    let toml_buffer = project
 128        .update(cx, |project, cx| {
 129            project.open_local_buffer("/the-root/Cargo.toml", cx)
 130        })
 131        .await
 132        .unwrap();
 133
 134    // Open a buffer with an associated language server before the language for it has been loaded.
 135    let rust_buffer = project
 136        .update(cx, |project, cx| {
 137            project.open_local_buffer("/the-root/test.rs", cx)
 138        })
 139        .await
 140        .unwrap();
 141    rust_buffer.read_with(cx, |buffer, _| {
 142        assert_eq!(buffer.language().map(|l| l.name()), None);
 143    });
 144
 145    // Now we add the languages to the project, and ensure they get assigned to all
 146    // the relevant open buffers.
 147    project.update(cx, |project, _| {
 148        project.languages.add(Arc::new(json_language));
 149        project.languages.add(Arc::new(rust_language));
 150    });
 151    deterministic.run_until_parked();
 152    rust_buffer.read_with(cx, |buffer, _| {
 153        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
 154    });
 155
 156    // A server is started up, and it is notified about Rust files.
 157    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 158    assert_eq!(
 159        fake_rust_server
 160            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 161            .await
 162            .text_document,
 163        lsp::TextDocumentItem {
 164            uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
 165            version: 0,
 166            text: "const A: i32 = 1;".to_string(),
 167            language_id: Default::default()
 168        }
 169    );
 170
 171    // The buffer is configured based on the language server's capabilities.
 172    rust_buffer.read_with(cx, |buffer, _| {
 173        assert_eq!(
 174            buffer.completion_triggers(),
 175            &[".".to_string(), "::".to_string()]
 176        );
 177    });
 178    toml_buffer.read_with(cx, |buffer, _| {
 179        assert!(buffer.completion_triggers().is_empty());
 180    });
 181
 182    // Edit a buffer. The changes are reported to the language server.
 183    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
 184    assert_eq!(
 185        fake_rust_server
 186            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 187            .await
 188            .text_document,
 189        lsp::VersionedTextDocumentIdentifier::new(
 190            lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
 191            1
 192        )
 193    );
 194
 195    // Open a third buffer with a different associated language server.
 196    let json_buffer = project
 197        .update(cx, |project, cx| {
 198            project.open_local_buffer("/the-root/package.json", cx)
 199        })
 200        .await
 201        .unwrap();
 202
 203    // A json language server is started up and is only notified about the json buffer.
 204    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 205    assert_eq!(
 206        fake_json_server
 207            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 208            .await
 209            .text_document,
 210        lsp::TextDocumentItem {
 211            uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
 212            version: 0,
 213            text: "{\"a\": 1}".to_string(),
 214            language_id: Default::default()
 215        }
 216    );
 217
 218    // This buffer is configured based on the second language server's
 219    // capabilities.
 220    json_buffer.read_with(cx, |buffer, _| {
 221        assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
 222    });
 223
 224    // When opening another buffer whose language server is already running,
 225    // it is also configured based on the existing language server's capabilities.
 226    let rust_buffer2 = project
 227        .update(cx, |project, cx| {
 228            project.open_local_buffer("/the-root/test2.rs", cx)
 229        })
 230        .await
 231        .unwrap();
 232    rust_buffer2.read_with(cx, |buffer, _| {
 233        assert_eq!(
 234            buffer.completion_triggers(),
 235            &[".".to_string(), "::".to_string()]
 236        );
 237    });
 238
 239    // Changes are reported only to servers matching the buffer's language.
 240    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
 241    rust_buffer2.update(cx, |buffer, cx| {
 242        buffer.edit([(0..0, "let x = 1;")], None, cx)
 243    });
 244    assert_eq!(
 245        fake_rust_server
 246            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 247            .await
 248            .text_document,
 249        lsp::VersionedTextDocumentIdentifier::new(
 250            lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
 251            1
 252        )
 253    );
 254
 255    // Save notifications are reported to all servers.
 256    project
 257        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
 258        .await
 259        .unwrap();
 260    assert_eq!(
 261        fake_rust_server
 262            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 263            .await
 264            .text_document,
 265        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap())
 266    );
 267    assert_eq!(
 268        fake_json_server
 269            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 270            .await
 271            .text_document,
 272        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap())
 273    );
 274
 275    // Renames are reported only to servers matching the buffer's language.
 276    fs.rename(
 277        Path::new("/the-root/test2.rs"),
 278        Path::new("/the-root/test3.rs"),
 279        Default::default(),
 280    )
 281    .await
 282    .unwrap();
 283    assert_eq!(
 284        fake_rust_server
 285            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 286            .await
 287            .text_document,
 288        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test2.rs").unwrap()),
 289    );
 290    assert_eq!(
 291        fake_rust_server
 292            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 293            .await
 294            .text_document,
 295        lsp::TextDocumentItem {
 296            uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
 297            version: 0,
 298            text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
 299            language_id: Default::default()
 300        },
 301    );
 302
 303    rust_buffer2.update(cx, |buffer, cx| {
 304        buffer.update_diagnostics(
 305            DiagnosticSet::from_sorted_entries(
 306                vec![DiagnosticEntry {
 307                    diagnostic: Default::default(),
 308                    range: Anchor::MIN..Anchor::MAX,
 309                }],
 310                &buffer.snapshot(),
 311            ),
 312            cx,
 313        );
 314        assert_eq!(
 315            buffer
 316                .snapshot()
 317                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 318                .count(),
 319            1
 320        );
 321    });
 322
 323    // When the rename changes the extension of the file, the buffer gets closed on the old
 324    // language server and gets opened on the new one.
 325    fs.rename(
 326        Path::new("/the-root/test3.rs"),
 327        Path::new("/the-root/test3.json"),
 328        Default::default(),
 329    )
 330    .await
 331    .unwrap();
 332    assert_eq!(
 333        fake_rust_server
 334            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 335            .await
 336            .text_document,
 337        lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),),
 338    );
 339    assert_eq!(
 340        fake_json_server
 341            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 342            .await
 343            .text_document,
 344        lsp::TextDocumentItem {
 345            uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
 346            version: 0,
 347            text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
 348            language_id: Default::default()
 349        },
 350    );
 351
 352    // We clear the diagnostics, since the language has changed.
 353    rust_buffer2.read_with(cx, |buffer, _| {
 354        assert_eq!(
 355            buffer
 356                .snapshot()
 357                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 358                .count(),
 359            0
 360        );
 361    });
 362
 363    // The renamed file's version resets after changing language server.
 364    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
 365    assert_eq!(
 366        fake_json_server
 367            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 368            .await
 369            .text_document,
 370        lsp::VersionedTextDocumentIdentifier::new(
 371            lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
 372            1
 373        )
 374    );
 375
 376    // Restart language servers
 377    project.update(cx, |project, cx| {
 378        project.restart_language_servers_for_buffers(
 379            vec![rust_buffer.clone(), json_buffer.clone()],
 380            cx,
 381        );
 382    });
 383
 384    let mut rust_shutdown_requests = fake_rust_server
 385        .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 386    let mut json_shutdown_requests = fake_json_server
 387        .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 388    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
 389
 390    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 391    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 392
 393    // Ensure rust document is reopened in new rust language server
 394    assert_eq!(
 395        fake_rust_server
 396            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 397            .await
 398            .text_document,
 399        lsp::TextDocumentItem {
 400            uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
 401            version: 1,
 402            text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
 403            language_id: Default::default()
 404        }
 405    );
 406
 407    // Ensure json documents are reopened in new json language server
 408    assert_set_eq!(
 409        [
 410            fake_json_server
 411                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 412                .await
 413                .text_document,
 414            fake_json_server
 415                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 416                .await
 417                .text_document,
 418        ],
 419        [
 420            lsp::TextDocumentItem {
 421                uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
 422                version: 0,
 423                text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
 424                language_id: Default::default()
 425            },
 426            lsp::TextDocumentItem {
 427                uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
 428                version: 1,
 429                text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
 430                language_id: Default::default()
 431            }
 432        ]
 433    );
 434
 435    // Close notifications are reported only to servers matching the buffer's language.
 436    cx.update(|_| drop(json_buffer));
 437    let close_message = lsp::DidCloseTextDocumentParams {
 438        text_document: lsp::TextDocumentIdentifier::new(
 439            lsp::Url::from_file_path("/the-root/package.json").unwrap(),
 440        ),
 441    };
 442    assert_eq!(
 443        fake_json_server
 444            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 445            .await,
 446        close_message,
 447    );
 448}
 449
 450#[gpui::test]
 451async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
 452    cx.foreground().forbid_parking();
 453
 454    let mut language = Language::new(
 455        LanguageConfig {
 456            name: "Rust".into(),
 457            path_suffixes: vec!["rs".to_string()],
 458            ..Default::default()
 459        },
 460        Some(tree_sitter_rust::language()),
 461    );
 462    let mut fake_servers = language
 463        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 464            name: "the-language-server",
 465            ..Default::default()
 466        }))
 467        .await;
 468
 469    let fs = FakeFs::new(cx.background());
 470    fs.insert_tree(
 471        "/the-root",
 472        json!({
 473            "a.rs": "",
 474            "b.rs": "",
 475        }),
 476    )
 477    .await;
 478
 479    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 480    project.update(cx, |project, _| {
 481        project.languages.add(Arc::new(language));
 482    });
 483    cx.foreground().run_until_parked();
 484
 485    // Start the language server by opening a buffer with a compatible file extension.
 486    let _buffer = project
 487        .update(cx, |project, cx| {
 488            project.open_local_buffer("/the-root/a.rs", cx)
 489        })
 490        .await
 491        .unwrap();
 492
 493    // Keep track of the FS events reported to the language server.
 494    let fake_server = fake_servers.next().await.unwrap();
 495    let file_changes = Arc::new(Mutex::new(Vec::new()));
 496    fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
 497        let file_changes = file_changes.clone();
 498        move |params, _| {
 499            let mut file_changes = file_changes.lock();
 500            file_changes.extend(params.changes);
 501            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
 502        }
 503    });
 504
 505    cx.foreground().run_until_parked();
 506    assert_eq!(file_changes.lock().len(), 0);
 507
 508    // Perform some file system mutations.
 509    fs.create_file("/the-root/c.rs".as_ref(), Default::default())
 510        .await
 511        .unwrap();
 512    fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
 513        .await
 514        .unwrap();
 515
 516    // The language server receives events for both FS mutations.
 517    cx.foreground().run_until_parked();
 518    assert_eq!(
 519        &*file_changes.lock(),
 520        &[
 521            lsp::FileEvent {
 522                uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
 523                typ: lsp::FileChangeType::DELETED,
 524            },
 525            lsp::FileEvent {
 526                uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
 527                typ: lsp::FileChangeType::CREATED,
 528            },
 529        ]
 530    );
 531}
 532
 533#[gpui::test]
 534async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 535    cx.foreground().forbid_parking();
 536
 537    let fs = FakeFs::new(cx.background());
 538    fs.insert_tree(
 539        "/dir",
 540        json!({
 541            "a.rs": "let a = 1;",
 542            "b.rs": "let b = 2;"
 543        }),
 544    )
 545    .await;
 546
 547    let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
 548
 549    let buffer_a = project
 550        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 551        .await
 552        .unwrap();
 553    let buffer_b = project
 554        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
 555        .await
 556        .unwrap();
 557
 558    project.update(cx, |project, cx| {
 559        project
 560            .update_diagnostics(
 561                0,
 562                lsp::PublishDiagnosticsParams {
 563                    uri: Url::from_file_path("/dir/a.rs").unwrap(),
 564                    version: None,
 565                    diagnostics: vec![lsp::Diagnostic {
 566                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 567                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 568                        message: "error 1".to_string(),
 569                        ..Default::default()
 570                    }],
 571                },
 572                &[],
 573                cx,
 574            )
 575            .unwrap();
 576        project
 577            .update_diagnostics(
 578                0,
 579                lsp::PublishDiagnosticsParams {
 580                    uri: Url::from_file_path("/dir/b.rs").unwrap(),
 581                    version: None,
 582                    diagnostics: vec![lsp::Diagnostic {
 583                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 584                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 585                        message: "error 2".to_string(),
 586                        ..Default::default()
 587                    }],
 588                },
 589                &[],
 590                cx,
 591            )
 592            .unwrap();
 593    });
 594
 595    buffer_a.read_with(cx, |buffer, _| {
 596        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 597        assert_eq!(
 598            chunks
 599                .iter()
 600                .map(|(s, d)| (s.as_str(), *d))
 601                .collect::<Vec<_>>(),
 602            &[
 603                ("let ", None),
 604                ("a", Some(DiagnosticSeverity::ERROR)),
 605                (" = 1;", None),
 606            ]
 607        );
 608    });
 609    buffer_b.read_with(cx, |buffer, _| {
 610        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 611        assert_eq!(
 612            chunks
 613                .iter()
 614                .map(|(s, d)| (s.as_str(), *d))
 615                .collect::<Vec<_>>(),
 616            &[
 617                ("let ", None),
 618                ("b", Some(DiagnosticSeverity::WARNING)),
 619                (" = 2;", None),
 620            ]
 621        );
 622    });
 623}
 624
 625#[gpui::test]
 626async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 627    cx.foreground().forbid_parking();
 628
 629    let fs = FakeFs::new(cx.background());
 630    fs.insert_tree(
 631        "/root",
 632        json!({
 633            "dir": {
 634                "a.rs": "let a = 1;",
 635            },
 636            "other.rs": "let b = c;"
 637        }),
 638    )
 639    .await;
 640
 641    let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
 642
 643    let (worktree, _) = project
 644        .update(cx, |project, cx| {
 645            project.find_or_create_local_worktree("/root/other.rs", false, cx)
 646        })
 647        .await
 648        .unwrap();
 649    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 650
 651    project.update(cx, |project, cx| {
 652        project
 653            .update_diagnostics(
 654                0,
 655                lsp::PublishDiagnosticsParams {
 656                    uri: Url::from_file_path("/root/other.rs").unwrap(),
 657                    version: None,
 658                    diagnostics: vec![lsp::Diagnostic {
 659                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
 660                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 661                        message: "unknown variable 'c'".to_string(),
 662                        ..Default::default()
 663                    }],
 664                },
 665                &[],
 666                cx,
 667            )
 668            .unwrap();
 669    });
 670
 671    let buffer = project
 672        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
 673        .await
 674        .unwrap();
 675    buffer.read_with(cx, |buffer, _| {
 676        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 677        assert_eq!(
 678            chunks
 679                .iter()
 680                .map(|(s, d)| (s.as_str(), *d))
 681                .collect::<Vec<_>>(),
 682            &[
 683                ("let b = ", None),
 684                ("c", Some(DiagnosticSeverity::ERROR)),
 685                (";", None),
 686            ]
 687        );
 688    });
 689
 690    project.read_with(cx, |project, cx| {
 691        assert_eq!(project.diagnostic_summaries(cx).next(), None);
 692        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
 693    });
 694}
 695
 696#[gpui::test]
 697async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
 698    cx.foreground().forbid_parking();
 699
 700    let progress_token = "the-progress-token";
 701    let mut language = Language::new(
 702        LanguageConfig {
 703            name: "Rust".into(),
 704            path_suffixes: vec!["rs".to_string()],
 705            ..Default::default()
 706        },
 707        Some(tree_sitter_rust::language()),
 708    );
 709    let mut fake_servers = language
 710        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 711            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 712            disk_based_diagnostics_sources: vec!["disk".into()],
 713            ..Default::default()
 714        }))
 715        .await;
 716
 717    let fs = FakeFs::new(cx.background());
 718    fs.insert_tree(
 719        "/dir",
 720        json!({
 721            "a.rs": "fn a() { A }",
 722            "b.rs": "const y: i32 = 1",
 723        }),
 724    )
 725    .await;
 726
 727    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 728    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 729    let worktree_id = project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
 730
 731    // Cause worktree to start the fake language server
 732    let _buffer = project
 733        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
 734        .await
 735        .unwrap();
 736
 737    let mut events = subscribe(&project, cx);
 738
 739    let fake_server = fake_servers.next().await.unwrap();
 740    fake_server
 741        .start_progress(format!("{}/0", progress_token))
 742        .await;
 743    assert_eq!(
 744        events.next().await.unwrap(),
 745        Event::DiskBasedDiagnosticsStarted {
 746            language_server_id: 0,
 747        }
 748    );
 749
 750    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 751        uri: Url::from_file_path("/dir/a.rs").unwrap(),
 752        version: None,
 753        diagnostics: vec![lsp::Diagnostic {
 754            range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 755            severity: Some(lsp::DiagnosticSeverity::ERROR),
 756            message: "undefined variable 'A'".to_string(),
 757            ..Default::default()
 758        }],
 759    });
 760    assert_eq!(
 761        events.next().await.unwrap(),
 762        Event::DiagnosticsUpdated {
 763            language_server_id: 0,
 764            path: (worktree_id, Path::new("a.rs")).into()
 765        }
 766    );
 767
 768    fake_server.end_progress(format!("{}/0", progress_token));
 769    assert_eq!(
 770        events.next().await.unwrap(),
 771        Event::DiskBasedDiagnosticsFinished {
 772            language_server_id: 0
 773        }
 774    );
 775
 776    let buffer = project
 777        .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
 778        .await
 779        .unwrap();
 780
 781    buffer.read_with(cx, |buffer, _| {
 782        let snapshot = buffer.snapshot();
 783        let diagnostics = snapshot
 784            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
 785            .collect::<Vec<_>>();
 786        assert_eq!(
 787            diagnostics,
 788            &[DiagnosticEntry {
 789                range: Point::new(0, 9)..Point::new(0, 10),
 790                diagnostic: Diagnostic {
 791                    severity: lsp::DiagnosticSeverity::ERROR,
 792                    message: "undefined variable 'A'".to_string(),
 793                    group_id: 0,
 794                    is_primary: true,
 795                    ..Default::default()
 796                }
 797            }]
 798        )
 799    });
 800
 801    // Ensure publishing empty diagnostics twice only results in one update event.
 802    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 803        uri: Url::from_file_path("/dir/a.rs").unwrap(),
 804        version: None,
 805        diagnostics: Default::default(),
 806    });
 807    assert_eq!(
 808        events.next().await.unwrap(),
 809        Event::DiagnosticsUpdated {
 810            language_server_id: 0,
 811            path: (worktree_id, Path::new("a.rs")).into()
 812        }
 813    );
 814
 815    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 816        uri: Url::from_file_path("/dir/a.rs").unwrap(),
 817        version: None,
 818        diagnostics: Default::default(),
 819    });
 820    cx.foreground().run_until_parked();
 821    assert_eq!(futures::poll!(events.next()), Poll::Pending);
 822}
 823
 824#[gpui::test]
 825async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
 826    cx.foreground().forbid_parking();
 827
 828    let progress_token = "the-progress-token";
 829    let mut language = Language::new(
 830        LanguageConfig {
 831            path_suffixes: vec!["rs".to_string()],
 832            ..Default::default()
 833        },
 834        None,
 835    );
 836    let mut fake_servers = language
 837        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 838            disk_based_diagnostics_sources: vec!["disk".into()],
 839            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 840            ..Default::default()
 841        }))
 842        .await;
 843
 844    let fs = FakeFs::new(cx.background());
 845    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 846
 847    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 848    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 849
 850    let buffer = project
 851        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 852        .await
 853        .unwrap();
 854
 855    // Simulate diagnostics starting to update.
 856    let fake_server = fake_servers.next().await.unwrap();
 857    fake_server.start_progress(progress_token).await;
 858
 859    // Restart the server before the diagnostics finish updating.
 860    project.update(cx, |project, cx| {
 861        project.restart_language_servers_for_buffers([buffer], cx);
 862    });
 863    let mut events = subscribe(&project, cx);
 864
 865    // Simulate the newly started server sending more diagnostics.
 866    let fake_server = fake_servers.next().await.unwrap();
 867    fake_server.start_progress(progress_token).await;
 868    assert_eq!(
 869        events.next().await.unwrap(),
 870        Event::DiskBasedDiagnosticsStarted {
 871            language_server_id: 1
 872        }
 873    );
 874    project.read_with(cx, |project, _| {
 875        assert_eq!(
 876            project
 877                .language_servers_running_disk_based_diagnostics()
 878                .collect::<Vec<_>>(),
 879            [1]
 880        );
 881    });
 882
 883    // All diagnostics are considered done, despite the old server's diagnostic
 884    // task never completing.
 885    fake_server.end_progress(progress_token);
 886    assert_eq!(
 887        events.next().await.unwrap(),
 888        Event::DiskBasedDiagnosticsFinished {
 889            language_server_id: 1
 890        }
 891    );
 892    project.read_with(cx, |project, _| {
 893        assert_eq!(
 894            project
 895                .language_servers_running_disk_based_diagnostics()
 896                .collect::<Vec<_>>(),
 897            [0; 0]
 898        );
 899    });
 900}
 901
 902#[gpui::test]
 903async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
 904    cx.foreground().forbid_parking();
 905
 906    let mut language = Language::new(
 907        LanguageConfig {
 908            path_suffixes: vec!["rs".to_string()],
 909            ..Default::default()
 910        },
 911        None,
 912    );
 913    let mut fake_servers = language
 914        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 915            name: "the-lsp",
 916            ..Default::default()
 917        }))
 918        .await;
 919
 920    let fs = FakeFs::new(cx.background());
 921    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 922
 923    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 924    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 925
 926    let buffer = project
 927        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 928        .await
 929        .unwrap();
 930
 931    // Before restarting the server, report diagnostics with an unknown buffer version.
 932    let fake_server = fake_servers.next().await.unwrap();
 933    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 934        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
 935        version: Some(10000),
 936        diagnostics: Vec::new(),
 937    });
 938    cx.foreground().run_until_parked();
 939
 940    project.update(cx, |project, cx| {
 941        project.restart_language_servers_for_buffers([buffer.clone()], cx);
 942    });
 943    let mut fake_server = fake_servers.next().await.unwrap();
 944    let notification = fake_server
 945        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 946        .await
 947        .text_document;
 948    assert_eq!(notification.version, 0);
 949}
 950
 951#[gpui::test]
 952async fn test_toggling_enable_language_server(
 953    deterministic: Arc<Deterministic>,
 954    cx: &mut gpui::TestAppContext,
 955) {
 956    deterministic.forbid_parking();
 957
 958    let mut rust = Language::new(
 959        LanguageConfig {
 960            name: Arc::from("Rust"),
 961            path_suffixes: vec!["rs".to_string()],
 962            ..Default::default()
 963        },
 964        None,
 965    );
 966    let mut fake_rust_servers = rust
 967        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 968            name: "rust-lsp",
 969            ..Default::default()
 970        }))
 971        .await;
 972    let mut js = Language::new(
 973        LanguageConfig {
 974            name: Arc::from("JavaScript"),
 975            path_suffixes: vec!["js".to_string()],
 976            ..Default::default()
 977        },
 978        None,
 979    );
 980    let mut fake_js_servers = js
 981        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
 982            name: "js-lsp",
 983            ..Default::default()
 984        }))
 985        .await;
 986
 987    let fs = FakeFs::new(cx.background());
 988    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
 989        .await;
 990
 991    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 992    project.update(cx, |project, _| {
 993        project.languages.add(Arc::new(rust));
 994        project.languages.add(Arc::new(js));
 995    });
 996
 997    let _rs_buffer = project
 998        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 999        .await
1000        .unwrap();
1001    let _js_buffer = project
1002        .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1003        .await
1004        .unwrap();
1005
1006    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1007    assert_eq!(
1008        fake_rust_server_1
1009            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1010            .await
1011            .text_document
1012            .uri
1013            .as_str(),
1014        "file:///dir/a.rs"
1015    );
1016
1017    let mut fake_js_server = fake_js_servers.next().await.unwrap();
1018    assert_eq!(
1019        fake_js_server
1020            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1021            .await
1022            .text_document
1023            .uri
1024            .as_str(),
1025        "file:///dir/b.js"
1026    );
1027
1028    // Disable Rust language server, ensuring only that server gets stopped.
1029    cx.update(|cx| {
1030        cx.update_global(|settings: &mut Settings, _| {
1031            settings.language_overrides.insert(
1032                Arc::from("Rust"),
1033                settings::EditorSettings {
1034                    enable_language_server: Some(false),
1035                    ..Default::default()
1036                },
1037            );
1038        })
1039    });
1040    fake_rust_server_1
1041        .receive_notification::<lsp::notification::Exit>()
1042        .await;
1043
1044    // Enable Rust and disable JavaScript language servers, ensuring that the
1045    // former gets started again and that the latter stops.
1046    cx.update(|cx| {
1047        cx.update_global(|settings: &mut Settings, _| {
1048            settings.language_overrides.insert(
1049                Arc::from("Rust"),
1050                settings::EditorSettings {
1051                    enable_language_server: Some(true),
1052                    ..Default::default()
1053                },
1054            );
1055            settings.language_overrides.insert(
1056                Arc::from("JavaScript"),
1057                settings::EditorSettings {
1058                    enable_language_server: Some(false),
1059                    ..Default::default()
1060                },
1061            );
1062        })
1063    });
1064    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1065    assert_eq!(
1066        fake_rust_server_2
1067            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1068            .await
1069            .text_document
1070            .uri
1071            .as_str(),
1072        "file:///dir/a.rs"
1073    );
1074    fake_js_server
1075        .receive_notification::<lsp::notification::Exit>()
1076        .await;
1077}
1078
1079#[gpui::test]
1080async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
1081    cx.foreground().forbid_parking();
1082
1083    let mut language = Language::new(
1084        LanguageConfig {
1085            name: "Rust".into(),
1086            path_suffixes: vec!["rs".to_string()],
1087            ..Default::default()
1088        },
1089        Some(tree_sitter_rust::language()),
1090    );
1091    let mut fake_servers = language
1092        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1093            disk_based_diagnostics_sources: vec!["disk".into()],
1094            ..Default::default()
1095        }))
1096        .await;
1097
1098    let text = "
1099        fn a() { A }
1100        fn b() { BB }
1101        fn c() { CCC }
1102    "
1103    .unindent();
1104
1105    let fs = FakeFs::new(cx.background());
1106    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1107
1108    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1109    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1110
1111    let buffer = project
1112        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1113        .await
1114        .unwrap();
1115
1116    let mut fake_server = fake_servers.next().await.unwrap();
1117    let open_notification = fake_server
1118        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1119        .await;
1120
1121    // Edit the buffer, moving the content down
1122    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1123    let change_notification_1 = fake_server
1124        .receive_notification::<lsp::notification::DidChangeTextDocument>()
1125        .await;
1126    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1127
1128    // Report some diagnostics for the initial version of the buffer
1129    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1130        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1131        version: Some(open_notification.text_document.version),
1132        diagnostics: vec![
1133            lsp::Diagnostic {
1134                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1135                severity: Some(DiagnosticSeverity::ERROR),
1136                message: "undefined variable 'A'".to_string(),
1137                source: Some("disk".to_string()),
1138                ..Default::default()
1139            },
1140            lsp::Diagnostic {
1141                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1142                severity: Some(DiagnosticSeverity::ERROR),
1143                message: "undefined variable 'BB'".to_string(),
1144                source: Some("disk".to_string()),
1145                ..Default::default()
1146            },
1147            lsp::Diagnostic {
1148                range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
1149                severity: Some(DiagnosticSeverity::ERROR),
1150                source: Some("disk".to_string()),
1151                message: "undefined variable 'CCC'".to_string(),
1152                ..Default::default()
1153            },
1154        ],
1155    });
1156
1157    // The diagnostics have moved down since they were created.
1158    buffer.next_notification(cx).await;
1159    buffer.read_with(cx, |buffer, _| {
1160        assert_eq!(
1161            buffer
1162                .snapshot()
1163                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1164                .collect::<Vec<_>>(),
1165            &[
1166                DiagnosticEntry {
1167                    range: Point::new(3, 9)..Point::new(3, 11),
1168                    diagnostic: Diagnostic {
1169                        severity: DiagnosticSeverity::ERROR,
1170                        message: "undefined variable 'BB'".to_string(),
1171                        is_disk_based: true,
1172                        group_id: 1,
1173                        is_primary: true,
1174                        ..Default::default()
1175                    },
1176                },
1177                DiagnosticEntry {
1178                    range: Point::new(4, 9)..Point::new(4, 12),
1179                    diagnostic: Diagnostic {
1180                        severity: DiagnosticSeverity::ERROR,
1181                        message: "undefined variable 'CCC'".to_string(),
1182                        is_disk_based: true,
1183                        group_id: 2,
1184                        is_primary: true,
1185                        ..Default::default()
1186                    }
1187                }
1188            ]
1189        );
1190        assert_eq!(
1191            chunks_with_diagnostics(buffer, 0..buffer.len()),
1192            [
1193                ("\n\nfn a() { ".to_string(), None),
1194                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1195                (" }\nfn b() { ".to_string(), None),
1196                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1197                (" }\nfn c() { ".to_string(), None),
1198                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1199                (" }\n".to_string(), None),
1200            ]
1201        );
1202        assert_eq!(
1203            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1204            [
1205                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1206                (" }\nfn c() { ".to_string(), None),
1207                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1208            ]
1209        );
1210    });
1211
1212    // Ensure overlapping diagnostics are highlighted correctly.
1213    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1214        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1215        version: Some(open_notification.text_document.version),
1216        diagnostics: vec![
1217            lsp::Diagnostic {
1218                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1219                severity: Some(DiagnosticSeverity::ERROR),
1220                message: "undefined variable 'A'".to_string(),
1221                source: Some("disk".to_string()),
1222                ..Default::default()
1223            },
1224            lsp::Diagnostic {
1225                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1226                severity: Some(DiagnosticSeverity::WARNING),
1227                message: "unreachable statement".to_string(),
1228                source: Some("disk".to_string()),
1229                ..Default::default()
1230            },
1231        ],
1232    });
1233
1234    buffer.next_notification(cx).await;
1235    buffer.read_with(cx, |buffer, _| {
1236        assert_eq!(
1237            buffer
1238                .snapshot()
1239                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1240                .collect::<Vec<_>>(),
1241            &[
1242                DiagnosticEntry {
1243                    range: Point::new(2, 9)..Point::new(2, 12),
1244                    diagnostic: Diagnostic {
1245                        severity: DiagnosticSeverity::WARNING,
1246                        message: "unreachable statement".to_string(),
1247                        is_disk_based: true,
1248                        group_id: 4,
1249                        is_primary: true,
1250                        ..Default::default()
1251                    }
1252                },
1253                DiagnosticEntry {
1254                    range: Point::new(2, 9)..Point::new(2, 10),
1255                    diagnostic: Diagnostic {
1256                        severity: DiagnosticSeverity::ERROR,
1257                        message: "undefined variable 'A'".to_string(),
1258                        is_disk_based: true,
1259                        group_id: 3,
1260                        is_primary: true,
1261                        ..Default::default()
1262                    },
1263                }
1264            ]
1265        );
1266        assert_eq!(
1267            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1268            [
1269                ("fn a() { ".to_string(), None),
1270                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1271                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1272                ("\n".to_string(), None),
1273            ]
1274        );
1275        assert_eq!(
1276            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1277            [
1278                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1279                ("\n".to_string(), None),
1280            ]
1281        );
1282    });
1283
1284    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1285    // changes since the last save.
1286    buffer.update(cx, |buffer, cx| {
1287        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
1288        buffer.edit(
1289            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1290            None,
1291            cx,
1292        );
1293        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1294    });
1295    let change_notification_2 = fake_server
1296        .receive_notification::<lsp::notification::DidChangeTextDocument>()
1297        .await;
1298    assert!(
1299        change_notification_2.text_document.version > change_notification_1.text_document.version
1300    );
1301
1302    // Handle out-of-order diagnostics
1303    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1304        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1305        version: Some(change_notification_2.text_document.version),
1306        diagnostics: vec![
1307            lsp::Diagnostic {
1308                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1309                severity: Some(DiagnosticSeverity::ERROR),
1310                message: "undefined variable 'BB'".to_string(),
1311                source: Some("disk".to_string()),
1312                ..Default::default()
1313            },
1314            lsp::Diagnostic {
1315                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1316                severity: Some(DiagnosticSeverity::WARNING),
1317                message: "undefined variable 'A'".to_string(),
1318                source: Some("disk".to_string()),
1319                ..Default::default()
1320            },
1321        ],
1322    });
1323
1324    buffer.next_notification(cx).await;
1325    buffer.read_with(cx, |buffer, _| {
1326        assert_eq!(
1327            buffer
1328                .snapshot()
1329                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1330                .collect::<Vec<_>>(),
1331            &[
1332                DiagnosticEntry {
1333                    range: Point::new(2, 21)..Point::new(2, 22),
1334                    diagnostic: Diagnostic {
1335                        severity: DiagnosticSeverity::WARNING,
1336                        message: "undefined variable 'A'".to_string(),
1337                        is_disk_based: true,
1338                        group_id: 6,
1339                        is_primary: true,
1340                        ..Default::default()
1341                    }
1342                },
1343                DiagnosticEntry {
1344                    range: Point::new(3, 9)..Point::new(3, 14),
1345                    diagnostic: Diagnostic {
1346                        severity: DiagnosticSeverity::ERROR,
1347                        message: "undefined variable 'BB'".to_string(),
1348                        is_disk_based: true,
1349                        group_id: 5,
1350                        is_primary: true,
1351                        ..Default::default()
1352                    },
1353                }
1354            ]
1355        );
1356    });
1357}
1358
1359#[gpui::test]
1360async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1361    cx.foreground().forbid_parking();
1362
1363    let text = concat!(
1364        "let one = ;\n", //
1365        "let two = \n",
1366        "let three = 3;\n",
1367    );
1368
1369    let fs = FakeFs::new(cx.background());
1370    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1371
1372    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1373    let buffer = project
1374        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1375        .await
1376        .unwrap();
1377
1378    project.update(cx, |project, cx| {
1379        project
1380            .update_buffer_diagnostics(
1381                &buffer,
1382                vec![
1383                    DiagnosticEntry {
1384                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1385                        diagnostic: Diagnostic {
1386                            severity: DiagnosticSeverity::ERROR,
1387                            message: "syntax error 1".to_string(),
1388                            ..Default::default()
1389                        },
1390                    },
1391                    DiagnosticEntry {
1392                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1393                        diagnostic: Diagnostic {
1394                            severity: DiagnosticSeverity::ERROR,
1395                            message: "syntax error 2".to_string(),
1396                            ..Default::default()
1397                        },
1398                    },
1399                ],
1400                None,
1401                cx,
1402            )
1403            .unwrap();
1404    });
1405
1406    // An empty range is extended forward to include the following character.
1407    // At the end of a line, an empty range is extended backward to include
1408    // the preceding character.
1409    buffer.read_with(cx, |buffer, _| {
1410        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1411        assert_eq!(
1412            chunks
1413                .iter()
1414                .map(|(s, d)| (s.as_str(), *d))
1415                .collect::<Vec<_>>(),
1416            &[
1417                ("let one = ", None),
1418                (";", Some(DiagnosticSeverity::ERROR)),
1419                ("\nlet two =", None),
1420                (" ", Some(DiagnosticSeverity::ERROR)),
1421                ("\nlet three = 3;\n", None)
1422            ]
1423        );
1424    });
1425}
1426
1427#[gpui::test]
1428async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
1429    cx.foreground().forbid_parking();
1430
1431    let mut language = Language::new(
1432        LanguageConfig {
1433            name: "Rust".into(),
1434            path_suffixes: vec!["rs".to_string()],
1435            ..Default::default()
1436        },
1437        Some(tree_sitter_rust::language()),
1438    );
1439    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1440
1441    let text = "
1442        fn a() {
1443            f1();
1444        }
1445        fn b() {
1446            f2();
1447        }
1448        fn c() {
1449            f3();
1450        }
1451    "
1452    .unindent();
1453
1454    let fs = FakeFs::new(cx.background());
1455    fs.insert_tree(
1456        "/dir",
1457        json!({
1458            "a.rs": text.clone(),
1459        }),
1460    )
1461    .await;
1462
1463    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1464    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1465    let buffer = project
1466        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1467        .await
1468        .unwrap();
1469
1470    let mut fake_server = fake_servers.next().await.unwrap();
1471    let lsp_document_version = fake_server
1472        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1473        .await
1474        .text_document
1475        .version;
1476
1477    // Simulate editing the buffer after the language server computes some edits.
1478    buffer.update(cx, |buffer, cx| {
1479        buffer.edit(
1480            [(
1481                Point::new(0, 0)..Point::new(0, 0),
1482                "// above first function\n",
1483            )],
1484            None,
1485            cx,
1486        );
1487        buffer.edit(
1488            [(
1489                Point::new(2, 0)..Point::new(2, 0),
1490                "    // inside first function\n",
1491            )],
1492            None,
1493            cx,
1494        );
1495        buffer.edit(
1496            [(
1497                Point::new(6, 4)..Point::new(6, 4),
1498                "// inside second function ",
1499            )],
1500            None,
1501            cx,
1502        );
1503
1504        assert_eq!(
1505            buffer.text(),
1506            "
1507                // above first function
1508                fn a() {
1509                    // inside first function
1510                    f1();
1511                }
1512                fn b() {
1513                    // inside second function f2();
1514                }
1515                fn c() {
1516                    f3();
1517                }
1518            "
1519            .unindent()
1520        );
1521    });
1522
1523    let edits = project
1524        .update(cx, |project, cx| {
1525            project.edits_from_lsp(
1526                &buffer,
1527                vec![
1528                    // replace body of first function
1529                    lsp::TextEdit {
1530                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
1531                        new_text: "
1532                            fn a() {
1533                                f10();
1534                            }
1535                            "
1536                        .unindent(),
1537                    },
1538                    // edit inside second function
1539                    lsp::TextEdit {
1540                        range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
1541                        new_text: "00".into(),
1542                    },
1543                    // edit inside third function via two distinct edits
1544                    lsp::TextEdit {
1545                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
1546                        new_text: "4000".into(),
1547                    },
1548                    lsp::TextEdit {
1549                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
1550                        new_text: "".into(),
1551                    },
1552                ],
1553                Some(lsp_document_version),
1554                cx,
1555            )
1556        })
1557        .await
1558        .unwrap();
1559
1560    buffer.update(cx, |buffer, cx| {
1561        for (range, new_text) in edits {
1562            buffer.edit([(range, new_text)], None, cx);
1563        }
1564        assert_eq!(
1565            buffer.text(),
1566            "
1567                // above first function
1568                fn a() {
1569                    // inside first function
1570                    f10();
1571                }
1572                fn b() {
1573                    // inside second function f200();
1574                }
1575                fn c() {
1576                    f4000();
1577                }
1578                "
1579            .unindent()
1580        );
1581    });
1582}
1583
1584#[gpui::test]
1585async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
1586    cx.foreground().forbid_parking();
1587
1588    let text = "
1589        use a::b;
1590        use a::c;
1591
1592        fn f() {
1593            b();
1594            c();
1595        }
1596    "
1597    .unindent();
1598
1599    let fs = FakeFs::new(cx.background());
1600    fs.insert_tree(
1601        "/dir",
1602        json!({
1603            "a.rs": text.clone(),
1604        }),
1605    )
1606    .await;
1607
1608    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1609    let buffer = project
1610        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1611        .await
1612        .unwrap();
1613
1614    // Simulate the language server sending us a small edit in the form of a very large diff.
1615    // Rust-analyzer does this when performing a merge-imports code action.
1616    let edits = project
1617        .update(cx, |project, cx| {
1618            project.edits_from_lsp(
1619                &buffer,
1620                [
1621                    // Replace the first use statement without editing the semicolon.
1622                    lsp::TextEdit {
1623                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
1624                        new_text: "a::{b, c}".into(),
1625                    },
1626                    // Reinsert the remainder of the file between the semicolon and the final
1627                    // newline of the file.
1628                    lsp::TextEdit {
1629                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1630                        new_text: "\n\n".into(),
1631                    },
1632                    lsp::TextEdit {
1633                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1634                        new_text: "
1635                            fn f() {
1636                                b();
1637                                c();
1638                            }"
1639                        .unindent(),
1640                    },
1641                    // Delete everything after the first newline of the file.
1642                    lsp::TextEdit {
1643                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
1644                        new_text: "".into(),
1645                    },
1646                ],
1647                None,
1648                cx,
1649            )
1650        })
1651        .await
1652        .unwrap();
1653
1654    buffer.update(cx, |buffer, cx| {
1655        let edits = edits
1656            .into_iter()
1657            .map(|(range, text)| {
1658                (
1659                    range.start.to_point(buffer)..range.end.to_point(buffer),
1660                    text,
1661                )
1662            })
1663            .collect::<Vec<_>>();
1664
1665        assert_eq!(
1666            edits,
1667            [
1668                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1669                (Point::new(1, 0)..Point::new(2, 0), "".into())
1670            ]
1671        );
1672
1673        for (range, new_text) in edits {
1674            buffer.edit([(range, new_text)], None, cx);
1675        }
1676        assert_eq!(
1677            buffer.text(),
1678            "
1679                use a::{b, c};
1680
1681                fn f() {
1682                    b();
1683                    c();
1684                }
1685            "
1686            .unindent()
1687        );
1688    });
1689}
1690
1691#[gpui::test]
1692async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
1693    cx.foreground().forbid_parking();
1694
1695    let text = "
1696        use a::b;
1697        use a::c;
1698
1699        fn f() {
1700            b();
1701            c();
1702        }
1703    "
1704    .unindent();
1705
1706    let fs = FakeFs::new(cx.background());
1707    fs.insert_tree(
1708        "/dir",
1709        json!({
1710            "a.rs": text.clone(),
1711        }),
1712    )
1713    .await;
1714
1715    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1716    let buffer = project
1717        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1718        .await
1719        .unwrap();
1720
1721    // Simulate the language server sending us edits in a non-ordered fashion,
1722    // with ranges sometimes being inverted or pointing to invalid locations.
1723    let edits = project
1724        .update(cx, |project, cx| {
1725            project.edits_from_lsp(
1726                &buffer,
1727                [
1728                    lsp::TextEdit {
1729                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1730                        new_text: "\n\n".into(),
1731                    },
1732                    lsp::TextEdit {
1733                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
1734                        new_text: "a::{b, c}".into(),
1735                    },
1736                    lsp::TextEdit {
1737                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
1738                        new_text: "".into(),
1739                    },
1740                    lsp::TextEdit {
1741                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1742                        new_text: "
1743                            fn f() {
1744                                b();
1745                                c();
1746                            }"
1747                        .unindent(),
1748                    },
1749                ],
1750                None,
1751                cx,
1752            )
1753        })
1754        .await
1755        .unwrap();
1756
1757    buffer.update(cx, |buffer, cx| {
1758        let edits = edits
1759            .into_iter()
1760            .map(|(range, text)| {
1761                (
1762                    range.start.to_point(buffer)..range.end.to_point(buffer),
1763                    text,
1764                )
1765            })
1766            .collect::<Vec<_>>();
1767
1768        assert_eq!(
1769            edits,
1770            [
1771                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1772                (Point::new(1, 0)..Point::new(2, 0), "".into())
1773            ]
1774        );
1775
1776        for (range, new_text) in edits {
1777            buffer.edit([(range, new_text)], None, cx);
1778        }
1779        assert_eq!(
1780            buffer.text(),
1781            "
1782                use a::{b, c};
1783
1784                fn f() {
1785                    b();
1786                    c();
1787                }
1788            "
1789            .unindent()
1790        );
1791    });
1792}
1793
1794fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
1795    buffer: &Buffer,
1796    range: Range<T>,
1797) -> Vec<(String, Option<DiagnosticSeverity>)> {
1798    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1799    for chunk in buffer.snapshot().chunks(range, true) {
1800        if chunks.last().map_or(false, |prev_chunk| {
1801            prev_chunk.1 == chunk.diagnostic_severity
1802        }) {
1803            chunks.last_mut().unwrap().0.push_str(chunk.text);
1804        } else {
1805            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
1806        }
1807    }
1808    chunks
1809}
1810
1811#[gpui::test(iterations = 10)]
1812async fn test_definition(cx: &mut gpui::TestAppContext) {
1813    let mut language = Language::new(
1814        LanguageConfig {
1815            name: "Rust".into(),
1816            path_suffixes: vec!["rs".to_string()],
1817            ..Default::default()
1818        },
1819        Some(tree_sitter_rust::language()),
1820    );
1821    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1822
1823    let fs = FakeFs::new(cx.background());
1824    fs.insert_tree(
1825        "/dir",
1826        json!({
1827            "a.rs": "const fn a() { A }",
1828            "b.rs": "const y: i32 = crate::a()",
1829        }),
1830    )
1831    .await;
1832
1833    let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
1834    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1835
1836    let buffer = project
1837        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
1838        .await
1839        .unwrap();
1840
1841    let fake_server = fake_servers.next().await.unwrap();
1842    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
1843        let params = params.text_document_position_params;
1844        assert_eq!(
1845            params.text_document.uri.to_file_path().unwrap(),
1846            Path::new("/dir/b.rs"),
1847        );
1848        assert_eq!(params.position, lsp::Position::new(0, 22));
1849
1850        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1851            lsp::Location::new(
1852                lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1853                lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1854            ),
1855        )))
1856    });
1857
1858    let mut definitions = project
1859        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
1860        .await
1861        .unwrap();
1862
1863    // Assert no new language server started
1864    cx.foreground().run_until_parked();
1865    assert!(fake_servers.try_next().is_err());
1866
1867    assert_eq!(definitions.len(), 1);
1868    let definition = definitions.pop().unwrap();
1869    cx.update(|cx| {
1870        let target_buffer = definition.target.buffer.read(cx);
1871        assert_eq!(
1872            target_buffer
1873                .file()
1874                .unwrap()
1875                .as_local()
1876                .unwrap()
1877                .abs_path(cx),
1878            Path::new("/dir/a.rs"),
1879        );
1880        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
1881        assert_eq!(
1882            list_worktrees(&project, cx),
1883            [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
1884        );
1885
1886        drop(definition);
1887    });
1888    cx.read(|cx| {
1889        assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
1890    });
1891
1892    fn list_worktrees<'a>(
1893        project: &'a ModelHandle<Project>,
1894        cx: &'a AppContext,
1895    ) -> Vec<(&'a Path, bool)> {
1896        project
1897            .read(cx)
1898            .worktrees(cx)
1899            .map(|worktree| {
1900                let worktree = worktree.read(cx);
1901                (
1902                    worktree.as_local().unwrap().abs_path().as_ref(),
1903                    worktree.is_visible(),
1904                )
1905            })
1906            .collect::<Vec<_>>()
1907    }
1908}
1909
1910#[gpui::test]
1911async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
1912    let mut language = Language::new(
1913        LanguageConfig {
1914            name: "TypeScript".into(),
1915            path_suffixes: vec!["ts".to_string()],
1916            ..Default::default()
1917        },
1918        Some(tree_sitter_typescript::language_typescript()),
1919    );
1920    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1921
1922    let fs = FakeFs::new(cx.background());
1923    fs.insert_tree(
1924        "/dir",
1925        json!({
1926            "a.ts": "",
1927        }),
1928    )
1929    .await;
1930
1931    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1932    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1933    let buffer = project
1934        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
1935        .await
1936        .unwrap();
1937
1938    let fake_server = fake_language_servers.next().await.unwrap();
1939
1940    let text = "let a = b.fqn";
1941    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
1942    let completions = project.update(cx, |project, cx| {
1943        project.completions(&buffer, text.len(), cx)
1944    });
1945
1946    fake_server
1947        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
1948            Ok(Some(lsp::CompletionResponse::Array(vec![
1949                lsp::CompletionItem {
1950                    label: "fullyQualifiedName?".into(),
1951                    insert_text: Some("fullyQualifiedName".into()),
1952                    ..Default::default()
1953                },
1954            ])))
1955        })
1956        .next()
1957        .await;
1958    let completions = completions.await.unwrap();
1959    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
1960    assert_eq!(completions.len(), 1);
1961    assert_eq!(completions[0].new_text, "fullyQualifiedName");
1962    assert_eq!(
1963        completions[0].old_range.to_offset(&snapshot),
1964        text.len() - 3..text.len()
1965    );
1966
1967    let text = "let a = \"atoms/cmp\"";
1968    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
1969    let completions = project.update(cx, |project, cx| {
1970        project.completions(&buffer, text.len() - 1, cx)
1971    });
1972
1973    fake_server
1974        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
1975            Ok(Some(lsp::CompletionResponse::Array(vec![
1976                lsp::CompletionItem {
1977                    label: "component".into(),
1978                    ..Default::default()
1979                },
1980            ])))
1981        })
1982        .next()
1983        .await;
1984    let completions = completions.await.unwrap();
1985    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
1986    assert_eq!(completions.len(), 1);
1987    assert_eq!(completions[0].new_text, "component");
1988    assert_eq!(
1989        completions[0].old_range.to_offset(&snapshot),
1990        text.len() - 4..text.len() - 1
1991    );
1992}
1993
1994#[gpui::test]
1995async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
1996    let mut language = Language::new(
1997        LanguageConfig {
1998            name: "TypeScript".into(),
1999            path_suffixes: vec!["ts".to_string()],
2000            ..Default::default()
2001        },
2002        Some(tree_sitter_typescript::language_typescript()),
2003    );
2004    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2005
2006    let fs = FakeFs::new(cx.background());
2007    fs.insert_tree(
2008        "/dir",
2009        json!({
2010            "a.ts": "",
2011        }),
2012    )
2013    .await;
2014
2015    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2016    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2017    let buffer = project
2018        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2019        .await
2020        .unwrap();
2021
2022    let fake_server = fake_language_servers.next().await.unwrap();
2023
2024    let text = "let a = b.fqn";
2025    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2026    let completions = project.update(cx, |project, cx| {
2027        project.completions(&buffer, text.len(), cx)
2028    });
2029
2030    fake_server
2031        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2032            Ok(Some(lsp::CompletionResponse::Array(vec![
2033                lsp::CompletionItem {
2034                    label: "fullyQualifiedName?".into(),
2035                    insert_text: Some("fully\rQualified\r\nName".into()),
2036                    ..Default::default()
2037                },
2038            ])))
2039        })
2040        .next()
2041        .await;
2042    let completions = completions.await.unwrap();
2043    assert_eq!(completions.len(), 1);
2044    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2045}
2046
2047#[gpui::test(iterations = 10)]
2048async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2049    let mut language = Language::new(
2050        LanguageConfig {
2051            name: "TypeScript".into(),
2052            path_suffixes: vec!["ts".to_string()],
2053            ..Default::default()
2054        },
2055        None,
2056    );
2057    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2058
2059    let fs = FakeFs::new(cx.background());
2060    fs.insert_tree(
2061        "/dir",
2062        json!({
2063            "a.ts": "a",
2064        }),
2065    )
2066    .await;
2067
2068    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2069    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2070    let buffer = project
2071        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2072        .await
2073        .unwrap();
2074
2075    let fake_server = fake_language_servers.next().await.unwrap();
2076
2077    // Language server returns code actions that contain commands, and not edits.
2078    let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2079    fake_server
2080        .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2081            Ok(Some(vec![
2082                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2083                    title: "The code action".into(),
2084                    command: Some(lsp::Command {
2085                        title: "The command".into(),
2086                        command: "_the/command".into(),
2087                        arguments: Some(vec![json!("the-argument")]),
2088                    }),
2089                    ..Default::default()
2090                }),
2091                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2092                    title: "two".into(),
2093                    ..Default::default()
2094                }),
2095            ]))
2096        })
2097        .next()
2098        .await;
2099
2100    let action = actions.await.unwrap()[0].clone();
2101    let apply = project.update(cx, |project, cx| {
2102        project.apply_code_action(buffer.clone(), action, true, cx)
2103    });
2104
2105    // Resolving the code action does not populate its edits. In absence of
2106    // edits, we must execute the given command.
2107    fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2108        |action, _| async move { Ok(action) },
2109    );
2110
2111    // While executing the command, the language server sends the editor
2112    // a `workspaceEdit` request.
2113    fake_server
2114        .handle_request::<lsp::request::ExecuteCommand, _, _>({
2115            let fake = fake_server.clone();
2116            move |params, _| {
2117                assert_eq!(params.command, "_the/command");
2118                let fake = fake.clone();
2119                async move {
2120                    fake.server
2121                        .request::<lsp::request::ApplyWorkspaceEdit>(
2122                            lsp::ApplyWorkspaceEditParams {
2123                                label: None,
2124                                edit: lsp::WorkspaceEdit {
2125                                    changes: Some(
2126                                        [(
2127                                            lsp::Url::from_file_path("/dir/a.ts").unwrap(),
2128                                            vec![lsp::TextEdit {
2129                                                range: lsp::Range::new(
2130                                                    lsp::Position::new(0, 0),
2131                                                    lsp::Position::new(0, 0),
2132                                                ),
2133                                                new_text: "X".into(),
2134                                            }],
2135                                        )]
2136                                        .into_iter()
2137                                        .collect(),
2138                                    ),
2139                                    ..Default::default()
2140                                },
2141                            },
2142                        )
2143                        .await
2144                        .unwrap();
2145                    Ok(Some(json!(null)))
2146                }
2147            }
2148        })
2149        .next()
2150        .await;
2151
2152    // Applying the code action returns a project transaction containing the edits
2153    // sent by the language server in its `workspaceEdit` request.
2154    let transaction = apply.await.unwrap();
2155    assert!(transaction.0.contains_key(&buffer));
2156    buffer.update(cx, |buffer, cx| {
2157        assert_eq!(buffer.text(), "Xa");
2158        buffer.undo(cx);
2159        assert_eq!(buffer.text(), "a");
2160    });
2161}
2162
2163#[gpui::test]
2164async fn test_save_file(cx: &mut gpui::TestAppContext) {
2165    let fs = FakeFs::new(cx.background());
2166    fs.insert_tree(
2167        "/dir",
2168        json!({
2169            "file1": "the old contents",
2170        }),
2171    )
2172    .await;
2173
2174    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2175    let buffer = project
2176        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2177        .await
2178        .unwrap();
2179    buffer.update(cx, |buffer, cx| {
2180        assert_eq!(buffer.text(), "the old contents");
2181        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2182    });
2183
2184    project
2185        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2186        .await
2187        .unwrap();
2188
2189    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2190    assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2191}
2192
2193#[gpui::test]
2194async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2195    let fs = FakeFs::new(cx.background());
2196    fs.insert_tree(
2197        "/dir",
2198        json!({
2199            "file1": "the old contents",
2200        }),
2201    )
2202    .await;
2203
2204    let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2205    let buffer = project
2206        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2207        .await
2208        .unwrap();
2209    buffer.update(cx, |buffer, cx| {
2210        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2211    });
2212
2213    project
2214        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2215        .await
2216        .unwrap();
2217
2218    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2219    assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2220}
2221
2222#[gpui::test]
2223async fn test_save_as(cx: &mut gpui::TestAppContext) {
2224    let fs = FakeFs::new(cx.background());
2225    fs.insert_tree("/dir", json!({})).await;
2226
2227    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2228
2229    let languages = project.read_with(cx, |project, _| project.languages().clone());
2230    languages.register(
2231        "/some/path",
2232        LanguageConfig {
2233            name: "Rust".into(),
2234            path_suffixes: vec!["rs".into()],
2235            ..Default::default()
2236        },
2237        tree_sitter_rust::language(),
2238        None,
2239        |_| Default::default(),
2240    );
2241
2242    let buffer = project.update(cx, |project, cx| {
2243        project.create_buffer("", None, cx).unwrap()
2244    });
2245    buffer.update(cx, |buffer, cx| {
2246        buffer.edit([(0..0, "abc")], None, cx);
2247        assert!(buffer.is_dirty());
2248        assert!(!buffer.has_conflict());
2249        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2250    });
2251    project
2252        .update(cx, |project, cx| {
2253            project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2254        })
2255        .await
2256        .unwrap();
2257    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2258
2259    cx.foreground().run_until_parked();
2260    buffer.read_with(cx, |buffer, cx| {
2261        assert_eq!(
2262            buffer.file().unwrap().full_path(cx),
2263            Path::new("dir/file1.rs")
2264        );
2265        assert!(!buffer.is_dirty());
2266        assert!(!buffer.has_conflict());
2267        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2268    });
2269
2270    let opened_buffer = project
2271        .update(cx, |project, cx| {
2272            project.open_local_buffer("/dir/file1.rs", cx)
2273        })
2274        .await
2275        .unwrap();
2276    assert_eq!(opened_buffer, buffer);
2277}
2278
2279#[gpui::test(retries = 5)]
2280async fn test_rescan_and_remote_updates(
2281    deterministic: Arc<Deterministic>,
2282    cx: &mut gpui::TestAppContext,
2283) {
2284    let dir = temp_tree(json!({
2285        "a": {
2286            "file1": "",
2287            "file2": "",
2288            "file3": "",
2289        },
2290        "b": {
2291            "c": {
2292                "file4": "",
2293                "file5": "",
2294            }
2295        }
2296    }));
2297
2298    let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2299    let rpc = project.read_with(cx, |p, _| p.client.clone());
2300
2301    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
2302        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2303        async move { buffer.await.unwrap() }
2304    };
2305    let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2306        project.read_with(cx, |project, cx| {
2307            let tree = project.worktrees(cx).next().unwrap();
2308            tree.read(cx)
2309                .entry_for_path(path)
2310                .unwrap_or_else(|| panic!("no entry for path {}", path))
2311                .id
2312        })
2313    };
2314
2315    let buffer2 = buffer_for_path("a/file2", cx).await;
2316    let buffer3 = buffer_for_path("a/file3", cx).await;
2317    let buffer4 = buffer_for_path("b/c/file4", cx).await;
2318    let buffer5 = buffer_for_path("b/c/file5", cx).await;
2319
2320    let file2_id = id_for_path("a/file2", cx);
2321    let file3_id = id_for_path("a/file3", cx);
2322    let file4_id = id_for_path("b/c/file4", cx);
2323
2324    // Create a remote copy of this worktree.
2325    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2326    let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2327    let remote = cx.update(|cx| {
2328        Worktree::remote(
2329            1,
2330            1,
2331            proto::WorktreeMetadata {
2332                id: initial_snapshot.id().to_proto(),
2333                root_name: initial_snapshot.root_name().into(),
2334                abs_path: initial_snapshot
2335                    .abs_path()
2336                    .as_os_str()
2337                    .to_string_lossy()
2338                    .into(),
2339                visible: true,
2340            },
2341            rpc.clone(),
2342            cx,
2343        )
2344    });
2345    remote.update(cx, |remote, _| {
2346        let update = initial_snapshot.build_initial_update(1);
2347        remote.as_remote_mut().unwrap().update_from_remote(update);
2348    });
2349    deterministic.run_until_parked();
2350
2351    cx.read(|cx| {
2352        assert!(!buffer2.read(cx).is_dirty());
2353        assert!(!buffer3.read(cx).is_dirty());
2354        assert!(!buffer4.read(cx).is_dirty());
2355        assert!(!buffer5.read(cx).is_dirty());
2356    });
2357
2358    // Rename and delete files and directories.
2359    tree.flush_fs_events(cx).await;
2360    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2361    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2362    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2363    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2364    tree.flush_fs_events(cx).await;
2365
2366    let expected_paths = vec![
2367        "a",
2368        "a/file1",
2369        "a/file2.new",
2370        "b",
2371        "d",
2372        "d/file3",
2373        "d/file4",
2374    ];
2375
2376    cx.read(|app| {
2377        assert_eq!(
2378            tree.read(app)
2379                .paths()
2380                .map(|p| p.to_str().unwrap())
2381                .collect::<Vec<_>>(),
2382            expected_paths
2383        );
2384
2385        assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2386        assert_eq!(id_for_path("d/file3", cx), file3_id);
2387        assert_eq!(id_for_path("d/file4", cx), file4_id);
2388
2389        assert_eq!(
2390            buffer2.read(app).file().unwrap().path().as_ref(),
2391            Path::new("a/file2.new")
2392        );
2393        assert_eq!(
2394            buffer3.read(app).file().unwrap().path().as_ref(),
2395            Path::new("d/file3")
2396        );
2397        assert_eq!(
2398            buffer4.read(app).file().unwrap().path().as_ref(),
2399            Path::new("d/file4")
2400        );
2401        assert_eq!(
2402            buffer5.read(app).file().unwrap().path().as_ref(),
2403            Path::new("b/c/file5")
2404        );
2405
2406        assert!(!buffer2.read(app).file().unwrap().is_deleted());
2407        assert!(!buffer3.read(app).file().unwrap().is_deleted());
2408        assert!(!buffer4.read(app).file().unwrap().is_deleted());
2409        assert!(buffer5.read(app).file().unwrap().is_deleted());
2410    });
2411
2412    // Update the remote worktree. Check that it becomes consistent with the
2413    // local worktree.
2414    remote.update(cx, |remote, cx| {
2415        let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
2416            &initial_snapshot,
2417            1,
2418            1,
2419            true,
2420        );
2421        remote.as_remote_mut().unwrap().update_from_remote(update);
2422    });
2423    deterministic.run_until_parked();
2424    remote.read_with(cx, |remote, _| {
2425        assert_eq!(
2426            remote
2427                .paths()
2428                .map(|p| p.to_str().unwrap())
2429                .collect::<Vec<_>>(),
2430            expected_paths
2431        );
2432    });
2433}
2434
2435#[gpui::test(iterations = 10)]
2436async fn test_buffer_identity_across_renames(
2437    deterministic: Arc<Deterministic>,
2438    cx: &mut gpui::TestAppContext,
2439) {
2440    let fs = FakeFs::new(cx.background());
2441    fs.insert_tree(
2442        "/dir",
2443        json!({
2444            "a": {
2445                "file1": "",
2446            }
2447        }),
2448    )
2449    .await;
2450
2451    let project = Project::test(fs, [Path::new("/dir")], cx).await;
2452    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2453    let tree_id = tree.read_with(cx, |tree, _| tree.id());
2454
2455    let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2456        project.read_with(cx, |project, cx| {
2457            let tree = project.worktrees(cx).next().unwrap();
2458            tree.read(cx)
2459                .entry_for_path(path)
2460                .unwrap_or_else(|| panic!("no entry for path {}", path))
2461                .id
2462        })
2463    };
2464
2465    let dir_id = id_for_path("a", cx);
2466    let file_id = id_for_path("a/file1", cx);
2467    let buffer = project
2468        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2469        .await
2470        .unwrap();
2471    buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2472
2473    project
2474        .update(cx, |project, cx| {
2475            project.rename_entry(dir_id, Path::new("b"), cx)
2476        })
2477        .unwrap()
2478        .await
2479        .unwrap();
2480    deterministic.run_until_parked();
2481    assert_eq!(id_for_path("b", cx), dir_id);
2482    assert_eq!(id_for_path("b/file1", cx), file_id);
2483    buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2484}
2485
2486#[gpui::test]
2487async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
2488    let fs = FakeFs::new(cx.background());
2489    fs.insert_tree(
2490        "/dir",
2491        json!({
2492            "a.txt": "a-contents",
2493            "b.txt": "b-contents",
2494        }),
2495    )
2496    .await;
2497
2498    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2499
2500    // Spawn multiple tasks to open paths, repeating some paths.
2501    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2502        (
2503            p.open_local_buffer("/dir/a.txt", cx),
2504            p.open_local_buffer("/dir/b.txt", cx),
2505            p.open_local_buffer("/dir/a.txt", cx),
2506        )
2507    });
2508
2509    let buffer_a_1 = buffer_a_1.await.unwrap();
2510    let buffer_a_2 = buffer_a_2.await.unwrap();
2511    let buffer_b = buffer_b.await.unwrap();
2512    assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
2513    assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
2514
2515    // There is only one buffer per path.
2516    let buffer_a_id = buffer_a_1.id();
2517    assert_eq!(buffer_a_2.id(), buffer_a_id);
2518
2519    // Open the same path again while it is still open.
2520    drop(buffer_a_1);
2521    let buffer_a_3 = project
2522        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2523        .await
2524        .unwrap();
2525
2526    // There's still only one buffer per path.
2527    assert_eq!(buffer_a_3.id(), buffer_a_id);
2528}
2529
2530#[gpui::test]
2531async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
2532    let fs = FakeFs::new(cx.background());
2533    fs.insert_tree(
2534        "/dir",
2535        json!({
2536            "file1": "abc",
2537            "file2": "def",
2538            "file3": "ghi",
2539        }),
2540    )
2541    .await;
2542
2543    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2544
2545    let buffer1 = project
2546        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2547        .await
2548        .unwrap();
2549    let events = Rc::new(RefCell::new(Vec::new()));
2550
2551    // initially, the buffer isn't dirty.
2552    buffer1.update(cx, |buffer, cx| {
2553        cx.subscribe(&buffer1, {
2554            let events = events.clone();
2555            move |_, _, event, _| match event {
2556                BufferEvent::Operation(_) => {}
2557                _ => events.borrow_mut().push(event.clone()),
2558            }
2559        })
2560        .detach();
2561
2562        assert!(!buffer.is_dirty());
2563        assert!(events.borrow().is_empty());
2564
2565        buffer.edit([(1..2, "")], None, cx);
2566    });
2567
2568    // after the first edit, the buffer is dirty, and emits a dirtied event.
2569    buffer1.update(cx, |buffer, cx| {
2570        assert!(buffer.text() == "ac");
2571        assert!(buffer.is_dirty());
2572        assert_eq!(
2573            *events.borrow(),
2574            &[language::Event::Edited, language::Event::DirtyChanged]
2575        );
2576        events.borrow_mut().clear();
2577        buffer.did_save(
2578            buffer.version(),
2579            buffer.as_rope().fingerprint(),
2580            buffer.file().unwrap().mtime(),
2581            cx,
2582        );
2583    });
2584
2585    // after saving, the buffer is not dirty, and emits a saved event.
2586    buffer1.update(cx, |buffer, cx| {
2587        assert!(!buffer.is_dirty());
2588        assert_eq!(*events.borrow(), &[language::Event::Saved]);
2589        events.borrow_mut().clear();
2590
2591        buffer.edit([(1..1, "B")], None, cx);
2592        buffer.edit([(2..2, "D")], None, cx);
2593    });
2594
2595    // after editing again, the buffer is dirty, and emits another dirty event.
2596    buffer1.update(cx, |buffer, cx| {
2597        assert!(buffer.text() == "aBDc");
2598        assert!(buffer.is_dirty());
2599        assert_eq!(
2600            *events.borrow(),
2601            &[
2602                language::Event::Edited,
2603                language::Event::DirtyChanged,
2604                language::Event::Edited,
2605            ],
2606        );
2607        events.borrow_mut().clear();
2608
2609        // After restoring the buffer to its previously-saved state,
2610        // the buffer is not considered dirty anymore.
2611        buffer.edit([(1..3, "")], None, cx);
2612        assert!(buffer.text() == "ac");
2613        assert!(!buffer.is_dirty());
2614    });
2615
2616    assert_eq!(
2617        *events.borrow(),
2618        &[language::Event::Edited, language::Event::DirtyChanged]
2619    );
2620
2621    // When a file is deleted, the buffer is considered dirty.
2622    let events = Rc::new(RefCell::new(Vec::new()));
2623    let buffer2 = project
2624        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2625        .await
2626        .unwrap();
2627    buffer2.update(cx, |_, cx| {
2628        cx.subscribe(&buffer2, {
2629            let events = events.clone();
2630            move |_, _, event, _| events.borrow_mut().push(event.clone())
2631        })
2632        .detach();
2633    });
2634
2635    fs.remove_file("/dir/file2".as_ref(), Default::default())
2636        .await
2637        .unwrap();
2638    cx.foreground().run_until_parked();
2639    buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
2640    assert_eq!(
2641        *events.borrow(),
2642        &[
2643            language::Event::DirtyChanged,
2644            language::Event::FileHandleChanged
2645        ]
2646    );
2647
2648    // When a file is already dirty when deleted, we don't emit a Dirtied event.
2649    let events = Rc::new(RefCell::new(Vec::new()));
2650    let buffer3 = project
2651        .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
2652        .await
2653        .unwrap();
2654    buffer3.update(cx, |_, cx| {
2655        cx.subscribe(&buffer3, {
2656            let events = events.clone();
2657            move |_, _, event, _| events.borrow_mut().push(event.clone())
2658        })
2659        .detach();
2660    });
2661
2662    buffer3.update(cx, |buffer, cx| {
2663        buffer.edit([(0..0, "x")], None, cx);
2664    });
2665    events.borrow_mut().clear();
2666    fs.remove_file("/dir/file3".as_ref(), Default::default())
2667        .await
2668        .unwrap();
2669    cx.foreground().run_until_parked();
2670    assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
2671    cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
2672}
2673
2674#[gpui::test]
2675async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
2676    let initial_contents = "aaa\nbbbbb\nc\n";
2677    let fs = FakeFs::new(cx.background());
2678    fs.insert_tree(
2679        "/dir",
2680        json!({
2681            "the-file": initial_contents,
2682        }),
2683    )
2684    .await;
2685    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2686    let buffer = project
2687        .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
2688        .await
2689        .unwrap();
2690
2691    let anchors = (0..3)
2692        .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
2693        .collect::<Vec<_>>();
2694
2695    // Change the file on disk, adding two new lines of text, and removing
2696    // one line.
2697    buffer.read_with(cx, |buffer, _| {
2698        assert!(!buffer.is_dirty());
2699        assert!(!buffer.has_conflict());
2700    });
2701    let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
2702    fs.save(
2703        "/dir/the-file".as_ref(),
2704        &new_contents.into(),
2705        LineEnding::Unix,
2706    )
2707    .await
2708    .unwrap();
2709
2710    // Because the buffer was not modified, it is reloaded from disk. Its
2711    // contents are edited according to the diff between the old and new
2712    // file contents.
2713    cx.foreground().run_until_parked();
2714    buffer.update(cx, |buffer, _| {
2715        assert_eq!(buffer.text(), new_contents);
2716        assert!(!buffer.is_dirty());
2717        assert!(!buffer.has_conflict());
2718
2719        let anchor_positions = anchors
2720            .iter()
2721            .map(|anchor| anchor.to_point(&*buffer))
2722            .collect::<Vec<_>>();
2723        assert_eq!(
2724            anchor_positions,
2725            [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
2726        );
2727    });
2728
2729    // Modify the buffer
2730    buffer.update(cx, |buffer, cx| {
2731        buffer.edit([(0..0, " ")], None, cx);
2732        assert!(buffer.is_dirty());
2733        assert!(!buffer.has_conflict());
2734    });
2735
2736    // Change the file on disk again, adding blank lines to the beginning.
2737    fs.save(
2738        "/dir/the-file".as_ref(),
2739        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
2740        LineEnding::Unix,
2741    )
2742    .await
2743    .unwrap();
2744
2745    // Because the buffer is modified, it doesn't reload from disk, but is
2746    // marked as having a conflict.
2747    cx.foreground().run_until_parked();
2748    buffer.read_with(cx, |buffer, _| {
2749        assert!(buffer.has_conflict());
2750    });
2751}
2752
2753#[gpui::test]
2754async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
2755    let fs = FakeFs::new(cx.background());
2756    fs.insert_tree(
2757        "/dir",
2758        json!({
2759            "file1": "a\nb\nc\n",
2760            "file2": "one\r\ntwo\r\nthree\r\n",
2761        }),
2762    )
2763    .await;
2764
2765    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2766    let buffer1 = project
2767        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2768        .await
2769        .unwrap();
2770    let buffer2 = project
2771        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2772        .await
2773        .unwrap();
2774
2775    buffer1.read_with(cx, |buffer, _| {
2776        assert_eq!(buffer.text(), "a\nb\nc\n");
2777        assert_eq!(buffer.line_ending(), LineEnding::Unix);
2778    });
2779    buffer2.read_with(cx, |buffer, _| {
2780        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
2781        assert_eq!(buffer.line_ending(), LineEnding::Windows);
2782    });
2783
2784    // Change a file's line endings on disk from unix to windows. The buffer's
2785    // state updates correctly.
2786    fs.save(
2787        "/dir/file1".as_ref(),
2788        &"aaa\nb\nc\n".into(),
2789        LineEnding::Windows,
2790    )
2791    .await
2792    .unwrap();
2793    cx.foreground().run_until_parked();
2794    buffer1.read_with(cx, |buffer, _| {
2795        assert_eq!(buffer.text(), "aaa\nb\nc\n");
2796        assert_eq!(buffer.line_ending(), LineEnding::Windows);
2797    });
2798
2799    // Save a file with windows line endings. The file is written correctly.
2800    buffer2.update(cx, |buffer, cx| {
2801        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
2802    });
2803    project
2804        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
2805        .await
2806        .unwrap();
2807    assert_eq!(
2808        fs.load("/dir/file2".as_ref()).await.unwrap(),
2809        "one\r\ntwo\r\nthree\r\nfour\r\n",
2810    );
2811}
2812
2813#[gpui::test]
2814async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
2815    cx.foreground().forbid_parking();
2816
2817    let fs = FakeFs::new(cx.background());
2818    fs.insert_tree(
2819        "/the-dir",
2820        json!({
2821            "a.rs": "
2822                fn foo(mut v: Vec<usize>) {
2823                    for x in &v {
2824                        v.push(1);
2825                    }
2826                }
2827            "
2828            .unindent(),
2829        }),
2830    )
2831    .await;
2832
2833    let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
2834    let buffer = project
2835        .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
2836        .await
2837        .unwrap();
2838
2839    let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
2840    let message = lsp::PublishDiagnosticsParams {
2841        uri: buffer_uri.clone(),
2842        diagnostics: vec![
2843            lsp::Diagnostic {
2844                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2845                severity: Some(DiagnosticSeverity::WARNING),
2846                message: "error 1".to_string(),
2847                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2848                    location: lsp::Location {
2849                        uri: buffer_uri.clone(),
2850                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2851                    },
2852                    message: "error 1 hint 1".to_string(),
2853                }]),
2854                ..Default::default()
2855            },
2856            lsp::Diagnostic {
2857                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2858                severity: Some(DiagnosticSeverity::HINT),
2859                message: "error 1 hint 1".to_string(),
2860                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2861                    location: lsp::Location {
2862                        uri: buffer_uri.clone(),
2863                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2864                    },
2865                    message: "original diagnostic".to_string(),
2866                }]),
2867                ..Default::default()
2868            },
2869            lsp::Diagnostic {
2870                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2871                severity: Some(DiagnosticSeverity::ERROR),
2872                message: "error 2".to_string(),
2873                related_information: Some(vec![
2874                    lsp::DiagnosticRelatedInformation {
2875                        location: lsp::Location {
2876                            uri: buffer_uri.clone(),
2877                            range: lsp::Range::new(
2878                                lsp::Position::new(1, 13),
2879                                lsp::Position::new(1, 15),
2880                            ),
2881                        },
2882                        message: "error 2 hint 1".to_string(),
2883                    },
2884                    lsp::DiagnosticRelatedInformation {
2885                        location: lsp::Location {
2886                            uri: buffer_uri.clone(),
2887                            range: lsp::Range::new(
2888                                lsp::Position::new(1, 13),
2889                                lsp::Position::new(1, 15),
2890                            ),
2891                        },
2892                        message: "error 2 hint 2".to_string(),
2893                    },
2894                ]),
2895                ..Default::default()
2896            },
2897            lsp::Diagnostic {
2898                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2899                severity: Some(DiagnosticSeverity::HINT),
2900                message: "error 2 hint 1".to_string(),
2901                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2902                    location: lsp::Location {
2903                        uri: buffer_uri.clone(),
2904                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2905                    },
2906                    message: "original diagnostic".to_string(),
2907                }]),
2908                ..Default::default()
2909            },
2910            lsp::Diagnostic {
2911                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2912                severity: Some(DiagnosticSeverity::HINT),
2913                message: "error 2 hint 2".to_string(),
2914                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2915                    location: lsp::Location {
2916                        uri: buffer_uri,
2917                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2918                    },
2919                    message: "original diagnostic".to_string(),
2920                }]),
2921                ..Default::default()
2922            },
2923        ],
2924        version: None,
2925    };
2926
2927    project
2928        .update(cx, |p, cx| p.update_diagnostics(0, message, &[], cx))
2929        .unwrap();
2930    let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2931
2932    assert_eq!(
2933        buffer
2934            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
2935            .collect::<Vec<_>>(),
2936        &[
2937            DiagnosticEntry {
2938                range: Point::new(1, 8)..Point::new(1, 9),
2939                diagnostic: Diagnostic {
2940                    severity: DiagnosticSeverity::WARNING,
2941                    message: "error 1".to_string(),
2942                    group_id: 1,
2943                    is_primary: true,
2944                    ..Default::default()
2945                }
2946            },
2947            DiagnosticEntry {
2948                range: Point::new(1, 8)..Point::new(1, 9),
2949                diagnostic: Diagnostic {
2950                    severity: DiagnosticSeverity::HINT,
2951                    message: "error 1 hint 1".to_string(),
2952                    group_id: 1,
2953                    is_primary: false,
2954                    ..Default::default()
2955                }
2956            },
2957            DiagnosticEntry {
2958                range: Point::new(1, 13)..Point::new(1, 15),
2959                diagnostic: Diagnostic {
2960                    severity: DiagnosticSeverity::HINT,
2961                    message: "error 2 hint 1".to_string(),
2962                    group_id: 0,
2963                    is_primary: false,
2964                    ..Default::default()
2965                }
2966            },
2967            DiagnosticEntry {
2968                range: Point::new(1, 13)..Point::new(1, 15),
2969                diagnostic: Diagnostic {
2970                    severity: DiagnosticSeverity::HINT,
2971                    message: "error 2 hint 2".to_string(),
2972                    group_id: 0,
2973                    is_primary: false,
2974                    ..Default::default()
2975                }
2976            },
2977            DiagnosticEntry {
2978                range: Point::new(2, 8)..Point::new(2, 17),
2979                diagnostic: Diagnostic {
2980                    severity: DiagnosticSeverity::ERROR,
2981                    message: "error 2".to_string(),
2982                    group_id: 0,
2983                    is_primary: true,
2984                    ..Default::default()
2985                }
2986            }
2987        ]
2988    );
2989
2990    assert_eq!(
2991        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
2992        &[
2993            DiagnosticEntry {
2994                range: Point::new(1, 13)..Point::new(1, 15),
2995                diagnostic: Diagnostic {
2996                    severity: DiagnosticSeverity::HINT,
2997                    message: "error 2 hint 1".to_string(),
2998                    group_id: 0,
2999                    is_primary: false,
3000                    ..Default::default()
3001                }
3002            },
3003            DiagnosticEntry {
3004                range: Point::new(1, 13)..Point::new(1, 15),
3005                diagnostic: Diagnostic {
3006                    severity: DiagnosticSeverity::HINT,
3007                    message: "error 2 hint 2".to_string(),
3008                    group_id: 0,
3009                    is_primary: false,
3010                    ..Default::default()
3011                }
3012            },
3013            DiagnosticEntry {
3014                range: Point::new(2, 8)..Point::new(2, 17),
3015                diagnostic: Diagnostic {
3016                    severity: DiagnosticSeverity::ERROR,
3017                    message: "error 2".to_string(),
3018                    group_id: 0,
3019                    is_primary: true,
3020                    ..Default::default()
3021                }
3022            }
3023        ]
3024    );
3025
3026    assert_eq!(
3027        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3028        &[
3029            DiagnosticEntry {
3030                range: Point::new(1, 8)..Point::new(1, 9),
3031                diagnostic: Diagnostic {
3032                    severity: DiagnosticSeverity::WARNING,
3033                    message: "error 1".to_string(),
3034                    group_id: 1,
3035                    is_primary: true,
3036                    ..Default::default()
3037                }
3038            },
3039            DiagnosticEntry {
3040                range: Point::new(1, 8)..Point::new(1, 9),
3041                diagnostic: Diagnostic {
3042                    severity: DiagnosticSeverity::HINT,
3043                    message: "error 1 hint 1".to_string(),
3044                    group_id: 1,
3045                    is_primary: false,
3046                    ..Default::default()
3047                }
3048            },
3049        ]
3050    );
3051}
3052
3053#[gpui::test]
3054async fn test_rename(cx: &mut gpui::TestAppContext) {
3055    cx.foreground().forbid_parking();
3056
3057    let mut language = Language::new(
3058        LanguageConfig {
3059            name: "Rust".into(),
3060            path_suffixes: vec!["rs".to_string()],
3061            ..Default::default()
3062        },
3063        Some(tree_sitter_rust::language()),
3064    );
3065    let mut fake_servers = language
3066        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3067            capabilities: lsp::ServerCapabilities {
3068                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3069                    prepare_provider: Some(true),
3070                    work_done_progress_options: Default::default(),
3071                })),
3072                ..Default::default()
3073            },
3074            ..Default::default()
3075        }))
3076        .await;
3077
3078    let fs = FakeFs::new(cx.background());
3079    fs.insert_tree(
3080        "/dir",
3081        json!({
3082            "one.rs": "const ONE: usize = 1;",
3083            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3084        }),
3085    )
3086    .await;
3087
3088    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3089    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3090    let buffer = project
3091        .update(cx, |project, cx| {
3092            project.open_local_buffer("/dir/one.rs", cx)
3093        })
3094        .await
3095        .unwrap();
3096
3097    let fake_server = fake_servers.next().await.unwrap();
3098
3099    let response = project.update(cx, |project, cx| {
3100        project.prepare_rename(buffer.clone(), 7, cx)
3101    });
3102    fake_server
3103        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3104            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3105            assert_eq!(params.position, lsp::Position::new(0, 7));
3106            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3107                lsp::Position::new(0, 6),
3108                lsp::Position::new(0, 9),
3109            ))))
3110        })
3111        .next()
3112        .await
3113        .unwrap();
3114    let range = response.await.unwrap().unwrap();
3115    let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
3116    assert_eq!(range, 6..9);
3117
3118    let response = project.update(cx, |project, cx| {
3119        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3120    });
3121    fake_server
3122        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3123            assert_eq!(
3124                params.text_document_position.text_document.uri.as_str(),
3125                "file:///dir/one.rs"
3126            );
3127            assert_eq!(
3128                params.text_document_position.position,
3129                lsp::Position::new(0, 7)
3130            );
3131            assert_eq!(params.new_name, "THREE");
3132            Ok(Some(lsp::WorkspaceEdit {
3133                changes: Some(
3134                    [
3135                        (
3136                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3137                            vec![lsp::TextEdit::new(
3138                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3139                                "THREE".to_string(),
3140                            )],
3141                        ),
3142                        (
3143                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3144                            vec![
3145                                lsp::TextEdit::new(
3146                                    lsp::Range::new(
3147                                        lsp::Position::new(0, 24),
3148                                        lsp::Position::new(0, 27),
3149                                    ),
3150                                    "THREE".to_string(),
3151                                ),
3152                                lsp::TextEdit::new(
3153                                    lsp::Range::new(
3154                                        lsp::Position::new(0, 35),
3155                                        lsp::Position::new(0, 38),
3156                                    ),
3157                                    "THREE".to_string(),
3158                                ),
3159                            ],
3160                        ),
3161                    ]
3162                    .into_iter()
3163                    .collect(),
3164                ),
3165                ..Default::default()
3166            }))
3167        })
3168        .next()
3169        .await
3170        .unwrap();
3171    let mut transaction = response.await.unwrap().0;
3172    assert_eq!(transaction.len(), 2);
3173    assert_eq!(
3174        transaction
3175            .remove_entry(&buffer)
3176            .unwrap()
3177            .0
3178            .read_with(cx, |buffer, _| buffer.text()),
3179        "const THREE: usize = 1;"
3180    );
3181    assert_eq!(
3182        transaction
3183            .into_keys()
3184            .next()
3185            .unwrap()
3186            .read_with(cx, |buffer, _| buffer.text()),
3187        "const TWO: usize = one::THREE + one::THREE;"
3188    );
3189}
3190
3191#[gpui::test]
3192async fn test_search(cx: &mut gpui::TestAppContext) {
3193    let fs = FakeFs::new(cx.background());
3194    fs.insert_tree(
3195        "/dir",
3196        json!({
3197            "one.rs": "const ONE: usize = 1;",
3198            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3199            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3200            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3201        }),
3202    )
3203    .await;
3204    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3205    assert_eq!(
3206        search(&project, SearchQuery::text("TWO", false, true), cx)
3207            .await
3208            .unwrap(),
3209        HashMap::from_iter([
3210            ("two.rs".to_string(), vec![6..9]),
3211            ("three.rs".to_string(), vec![37..40])
3212        ])
3213    );
3214
3215    let buffer_4 = project
3216        .update(cx, |project, cx| {
3217            project.open_local_buffer("/dir/four.rs", cx)
3218        })
3219        .await
3220        .unwrap();
3221    buffer_4.update(cx, |buffer, cx| {
3222        let text = "two::TWO";
3223        buffer.edit([(20..28, text), (31..43, text)], None, cx);
3224    });
3225
3226    assert_eq!(
3227        search(&project, SearchQuery::text("TWO", false, true), cx)
3228            .await
3229            .unwrap(),
3230        HashMap::from_iter([
3231            ("two.rs".to_string(), vec![6..9]),
3232            ("three.rs".to_string(), vec![37..40]),
3233            ("four.rs".to_string(), vec![25..28, 36..39])
3234        ])
3235    );
3236
3237    async fn search(
3238        project: &ModelHandle<Project>,
3239        query: SearchQuery,
3240        cx: &mut gpui::TestAppContext,
3241    ) -> Result<HashMap<String, Vec<Range<usize>>>> {
3242        let results = project
3243            .update(cx, |project, cx| project.search(query, cx))
3244            .await?;
3245
3246        Ok(results
3247            .into_iter()
3248            .map(|(buffer, ranges)| {
3249                buffer.read_with(cx, |buffer, _| {
3250                    let path = buffer.file().unwrap().path().to_string_lossy().to_string();
3251                    let ranges = ranges
3252                        .into_iter()
3253                        .map(|range| range.to_offset(buffer))
3254                        .collect::<Vec<_>>();
3255                    (path, ranges)
3256                })
3257            })
3258            .collect())
3259    }
3260}