project_tests.rs

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