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.read_with(cx, |buffer, _| {
1277        assert_eq!(
1278            buffer
1279                .snapshot()
1280                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1281                .collect::<Vec<_>>(),
1282            &[
1283                DiagnosticEntry {
1284                    range: Point::new(3, 9)..Point::new(3, 11),
1285                    diagnostic: Diagnostic {
1286                        source: Some("disk".into()),
1287                        severity: DiagnosticSeverity::ERROR,
1288                        message: "undefined variable 'BB'".to_string(),
1289                        is_disk_based: true,
1290                        group_id: 1,
1291                        is_primary: true,
1292                        ..Default::default()
1293                    },
1294                },
1295                DiagnosticEntry {
1296                    range: Point::new(4, 9)..Point::new(4, 12),
1297                    diagnostic: Diagnostic {
1298                        source: Some("disk".into()),
1299                        severity: DiagnosticSeverity::ERROR,
1300                        message: "undefined variable 'CCC'".to_string(),
1301                        is_disk_based: true,
1302                        group_id: 2,
1303                        is_primary: true,
1304                        ..Default::default()
1305                    }
1306                }
1307            ]
1308        );
1309        assert_eq!(
1310            chunks_with_diagnostics(buffer, 0..buffer.len()),
1311            [
1312                ("\n\nfn a() { ".to_string(), None),
1313                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1314                (" }\nfn b() { ".to_string(), None),
1315                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1316                (" }\nfn c() { ".to_string(), None),
1317                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1318                (" }\n".to_string(), None),
1319            ]
1320        );
1321        assert_eq!(
1322            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1323            [
1324                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1325                (" }\nfn c() { ".to_string(), None),
1326                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1327            ]
1328        );
1329    });
1330
1331    // Ensure overlapping diagnostics are highlighted correctly.
1332    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1333        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1334        version: Some(open_notification.text_document.version),
1335        diagnostics: vec![
1336            lsp::Diagnostic {
1337                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1338                severity: Some(DiagnosticSeverity::ERROR),
1339                message: "undefined variable 'A'".to_string(),
1340                source: Some("disk".to_string()),
1341                ..Default::default()
1342            },
1343            lsp::Diagnostic {
1344                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1345                severity: Some(DiagnosticSeverity::WARNING),
1346                message: "unreachable statement".to_string(),
1347                source: Some("disk".to_string()),
1348                ..Default::default()
1349            },
1350        ],
1351    });
1352
1353    buffer.next_notification(cx).await;
1354    buffer.read_with(cx, |buffer, _| {
1355        assert_eq!(
1356            buffer
1357                .snapshot()
1358                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1359                .collect::<Vec<_>>(),
1360            &[
1361                DiagnosticEntry {
1362                    range: Point::new(2, 9)..Point::new(2, 12),
1363                    diagnostic: Diagnostic {
1364                        source: Some("disk".into()),
1365                        severity: DiagnosticSeverity::WARNING,
1366                        message: "unreachable statement".to_string(),
1367                        is_disk_based: true,
1368                        group_id: 4,
1369                        is_primary: true,
1370                        ..Default::default()
1371                    }
1372                },
1373                DiagnosticEntry {
1374                    range: Point::new(2, 9)..Point::new(2, 10),
1375                    diagnostic: Diagnostic {
1376                        source: Some("disk".into()),
1377                        severity: DiagnosticSeverity::ERROR,
1378                        message: "undefined variable 'A'".to_string(),
1379                        is_disk_based: true,
1380                        group_id: 3,
1381                        is_primary: true,
1382                        ..Default::default()
1383                    },
1384                }
1385            ]
1386        );
1387        assert_eq!(
1388            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1389            [
1390                ("fn a() { ".to_string(), None),
1391                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1392                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1393                ("\n".to_string(), None),
1394            ]
1395        );
1396        assert_eq!(
1397            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1398            [
1399                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1400                ("\n".to_string(), None),
1401            ]
1402        );
1403    });
1404
1405    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1406    // changes since the last save.
1407    buffer.update(cx, |buffer, cx| {
1408        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
1409        buffer.edit(
1410            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1411            None,
1412            cx,
1413        );
1414        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1415    });
1416    let change_notification_2 = fake_server
1417        .receive_notification::<lsp::notification::DidChangeTextDocument>()
1418        .await;
1419    assert!(
1420        change_notification_2.text_document.version > change_notification_1.text_document.version
1421    );
1422
1423    // Handle out-of-order diagnostics
1424    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1425        uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1426        version: Some(change_notification_2.text_document.version),
1427        diagnostics: vec![
1428            lsp::Diagnostic {
1429                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1430                severity: Some(DiagnosticSeverity::ERROR),
1431                message: "undefined variable 'BB'".to_string(),
1432                source: Some("disk".to_string()),
1433                ..Default::default()
1434            },
1435            lsp::Diagnostic {
1436                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1437                severity: Some(DiagnosticSeverity::WARNING),
1438                message: "undefined variable 'A'".to_string(),
1439                source: Some("disk".to_string()),
1440                ..Default::default()
1441            },
1442        ],
1443    });
1444
1445    buffer.next_notification(cx).await;
1446    buffer.read_with(cx, |buffer, _| {
1447        assert_eq!(
1448            buffer
1449                .snapshot()
1450                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1451                .collect::<Vec<_>>(),
1452            &[
1453                DiagnosticEntry {
1454                    range: Point::new(2, 21)..Point::new(2, 22),
1455                    diagnostic: Diagnostic {
1456                        source: Some("disk".into()),
1457                        severity: DiagnosticSeverity::WARNING,
1458                        message: "undefined variable 'A'".to_string(),
1459                        is_disk_based: true,
1460                        group_id: 6,
1461                        is_primary: true,
1462                        ..Default::default()
1463                    }
1464                },
1465                DiagnosticEntry {
1466                    range: Point::new(3, 9)..Point::new(3, 14),
1467                    diagnostic: Diagnostic {
1468                        source: Some("disk".into()),
1469                        severity: DiagnosticSeverity::ERROR,
1470                        message: "undefined variable 'BB'".to_string(),
1471                        is_disk_based: true,
1472                        group_id: 5,
1473                        is_primary: true,
1474                        ..Default::default()
1475                    },
1476                }
1477            ]
1478        );
1479    });
1480}
1481
1482#[gpui::test]
1483async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1484    init_test(cx);
1485
1486    let text = concat!(
1487        "let one = ;\n", //
1488        "let two = \n",
1489        "let three = 3;\n",
1490    );
1491
1492    let fs = FakeFs::new(cx.background());
1493    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1494
1495    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1496    let buffer = project
1497        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1498        .await
1499        .unwrap();
1500
1501    project.update(cx, |project, cx| {
1502        project
1503            .update_buffer_diagnostics(
1504                &buffer,
1505                LanguageServerId(0),
1506                None,
1507                vec![
1508                    DiagnosticEntry {
1509                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1510                        diagnostic: Diagnostic {
1511                            severity: DiagnosticSeverity::ERROR,
1512                            message: "syntax error 1".to_string(),
1513                            ..Default::default()
1514                        },
1515                    },
1516                    DiagnosticEntry {
1517                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1518                        diagnostic: Diagnostic {
1519                            severity: DiagnosticSeverity::ERROR,
1520                            message: "syntax error 2".to_string(),
1521                            ..Default::default()
1522                        },
1523                    },
1524                ],
1525                cx,
1526            )
1527            .unwrap();
1528    });
1529
1530    // An empty range is extended forward to include the following character.
1531    // At the end of a line, an empty range is extended backward to include
1532    // the preceding character.
1533    buffer.read_with(cx, |buffer, _| {
1534        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1535        assert_eq!(
1536            chunks
1537                .iter()
1538                .map(|(s, d)| (s.as_str(), *d))
1539                .collect::<Vec<_>>(),
1540            &[
1541                ("let one = ", None),
1542                (";", Some(DiagnosticSeverity::ERROR)),
1543                ("\nlet two =", None),
1544                (" ", Some(DiagnosticSeverity::ERROR)),
1545                ("\nlet three = 3;\n", None)
1546            ]
1547        );
1548    });
1549}
1550
1551#[gpui::test]
1552async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
1553    init_test(cx);
1554
1555    let fs = FakeFs::new(cx.background());
1556    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1557        .await;
1558
1559    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1560
1561    project.update(cx, |project, cx| {
1562        project
1563            .update_diagnostic_entries(
1564                LanguageServerId(0),
1565                Path::new("/dir/a.rs").to_owned(),
1566                None,
1567                vec![DiagnosticEntry {
1568                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1569                    diagnostic: Diagnostic {
1570                        severity: DiagnosticSeverity::ERROR,
1571                        is_primary: true,
1572                        message: "syntax error a1".to_string(),
1573                        ..Default::default()
1574                    },
1575                }],
1576                cx,
1577            )
1578            .unwrap();
1579        project
1580            .update_diagnostic_entries(
1581                LanguageServerId(1),
1582                Path::new("/dir/a.rs").to_owned(),
1583                None,
1584                vec![DiagnosticEntry {
1585                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1586                    diagnostic: Diagnostic {
1587                        severity: DiagnosticSeverity::ERROR,
1588                        is_primary: true,
1589                        message: "syntax error b1".to_string(),
1590                        ..Default::default()
1591                    },
1592                }],
1593                cx,
1594            )
1595            .unwrap();
1596
1597        assert_eq!(
1598            project.diagnostic_summary(cx),
1599            DiagnosticSummary {
1600                error_count: 2,
1601                warning_count: 0,
1602            }
1603        );
1604    });
1605}
1606
1607#[gpui::test]
1608async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
1609    init_test(cx);
1610
1611    let mut language = Language::new(
1612        LanguageConfig {
1613            name: "Rust".into(),
1614            path_suffixes: vec!["rs".to_string()],
1615            ..Default::default()
1616        },
1617        Some(tree_sitter_rust::language()),
1618    );
1619    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1620
1621    let text = "
1622        fn a() {
1623            f1();
1624        }
1625        fn b() {
1626            f2();
1627        }
1628        fn c() {
1629            f3();
1630        }
1631    "
1632    .unindent();
1633
1634    let fs = FakeFs::new(cx.background());
1635    fs.insert_tree(
1636        "/dir",
1637        json!({
1638            "a.rs": text.clone(),
1639        }),
1640    )
1641    .await;
1642
1643    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1644    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1645    let buffer = project
1646        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1647        .await
1648        .unwrap();
1649
1650    let mut fake_server = fake_servers.next().await.unwrap();
1651    let lsp_document_version = fake_server
1652        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1653        .await
1654        .text_document
1655        .version;
1656
1657    // Simulate editing the buffer after the language server computes some edits.
1658    buffer.update(cx, |buffer, cx| {
1659        buffer.edit(
1660            [(
1661                Point::new(0, 0)..Point::new(0, 0),
1662                "// above first function\n",
1663            )],
1664            None,
1665            cx,
1666        );
1667        buffer.edit(
1668            [(
1669                Point::new(2, 0)..Point::new(2, 0),
1670                "    // inside first function\n",
1671            )],
1672            None,
1673            cx,
1674        );
1675        buffer.edit(
1676            [(
1677                Point::new(6, 4)..Point::new(6, 4),
1678                "// inside second function ",
1679            )],
1680            None,
1681            cx,
1682        );
1683
1684        assert_eq!(
1685            buffer.text(),
1686            "
1687                // above first function
1688                fn a() {
1689                    // inside first function
1690                    f1();
1691                }
1692                fn b() {
1693                    // inside second function f2();
1694                }
1695                fn c() {
1696                    f3();
1697                }
1698            "
1699            .unindent()
1700        );
1701    });
1702
1703    let edits = project
1704        .update(cx, |project, cx| {
1705            project.edits_from_lsp(
1706                &buffer,
1707                vec![
1708                    // replace body of first function
1709                    lsp::TextEdit {
1710                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
1711                        new_text: "
1712                            fn a() {
1713                                f10();
1714                            }
1715                            "
1716                        .unindent(),
1717                    },
1718                    // edit inside second function
1719                    lsp::TextEdit {
1720                        range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
1721                        new_text: "00".into(),
1722                    },
1723                    // edit inside third function via two distinct edits
1724                    lsp::TextEdit {
1725                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
1726                        new_text: "4000".into(),
1727                    },
1728                    lsp::TextEdit {
1729                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
1730                        new_text: "".into(),
1731                    },
1732                ],
1733                LanguageServerId(0),
1734                Some(lsp_document_version),
1735                cx,
1736            )
1737        })
1738        .await
1739        .unwrap();
1740
1741    buffer.update(cx, |buffer, cx| {
1742        for (range, new_text) in edits {
1743            buffer.edit([(range, new_text)], None, cx);
1744        }
1745        assert_eq!(
1746            buffer.text(),
1747            "
1748                // above first function
1749                fn a() {
1750                    // inside first function
1751                    f10();
1752                }
1753                fn b() {
1754                    // inside second function f200();
1755                }
1756                fn c() {
1757                    f4000();
1758                }
1759                "
1760            .unindent()
1761        );
1762    });
1763}
1764
1765#[gpui::test]
1766async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
1767    init_test(cx);
1768
1769    let text = "
1770        use a::b;
1771        use a::c;
1772
1773        fn f() {
1774            b();
1775            c();
1776        }
1777    "
1778    .unindent();
1779
1780    let fs = FakeFs::new(cx.background());
1781    fs.insert_tree(
1782        "/dir",
1783        json!({
1784            "a.rs": text.clone(),
1785        }),
1786    )
1787    .await;
1788
1789    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1790    let buffer = project
1791        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1792        .await
1793        .unwrap();
1794
1795    // Simulate the language server sending us a small edit in the form of a very large diff.
1796    // Rust-analyzer does this when performing a merge-imports code action.
1797    let edits = project
1798        .update(cx, |project, cx| {
1799            project.edits_from_lsp(
1800                &buffer,
1801                [
1802                    // Replace the first use statement without editing the semicolon.
1803                    lsp::TextEdit {
1804                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
1805                        new_text: "a::{b, c}".into(),
1806                    },
1807                    // Reinsert the remainder of the file between the semicolon and the final
1808                    // newline of the file.
1809                    lsp::TextEdit {
1810                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1811                        new_text: "\n\n".into(),
1812                    },
1813                    lsp::TextEdit {
1814                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1815                        new_text: "
1816                            fn f() {
1817                                b();
1818                                c();
1819                            }"
1820                        .unindent(),
1821                    },
1822                    // Delete everything after the first newline of the file.
1823                    lsp::TextEdit {
1824                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
1825                        new_text: "".into(),
1826                    },
1827                ],
1828                LanguageServerId(0),
1829                None,
1830                cx,
1831            )
1832        })
1833        .await
1834        .unwrap();
1835
1836    buffer.update(cx, |buffer, cx| {
1837        let edits = edits
1838            .into_iter()
1839            .map(|(range, text)| {
1840                (
1841                    range.start.to_point(buffer)..range.end.to_point(buffer),
1842                    text,
1843                )
1844            })
1845            .collect::<Vec<_>>();
1846
1847        assert_eq!(
1848            edits,
1849            [
1850                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1851                (Point::new(1, 0)..Point::new(2, 0), "".into())
1852            ]
1853        );
1854
1855        for (range, new_text) in edits {
1856            buffer.edit([(range, new_text)], None, cx);
1857        }
1858        assert_eq!(
1859            buffer.text(),
1860            "
1861                use a::{b, c};
1862
1863                fn f() {
1864                    b();
1865                    c();
1866                }
1867            "
1868            .unindent()
1869        );
1870    });
1871}
1872
1873#[gpui::test]
1874async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
1875    init_test(cx);
1876
1877    let text = "
1878        use a::b;
1879        use a::c;
1880
1881        fn f() {
1882            b();
1883            c();
1884        }
1885    "
1886    .unindent();
1887
1888    let fs = FakeFs::new(cx.background());
1889    fs.insert_tree(
1890        "/dir",
1891        json!({
1892            "a.rs": text.clone(),
1893        }),
1894    )
1895    .await;
1896
1897    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1898    let buffer = project
1899        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1900        .await
1901        .unwrap();
1902
1903    // Simulate the language server sending us edits in a non-ordered fashion,
1904    // with ranges sometimes being inverted or pointing to invalid locations.
1905    let edits = project
1906        .update(cx, |project, cx| {
1907            project.edits_from_lsp(
1908                &buffer,
1909                [
1910                    lsp::TextEdit {
1911                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1912                        new_text: "\n\n".into(),
1913                    },
1914                    lsp::TextEdit {
1915                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
1916                        new_text: "a::{b, c}".into(),
1917                    },
1918                    lsp::TextEdit {
1919                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
1920                        new_text: "".into(),
1921                    },
1922                    lsp::TextEdit {
1923                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1924                        new_text: "
1925                            fn f() {
1926                                b();
1927                                c();
1928                            }"
1929                        .unindent(),
1930                    },
1931                ],
1932                LanguageServerId(0),
1933                None,
1934                cx,
1935            )
1936        })
1937        .await
1938        .unwrap();
1939
1940    buffer.update(cx, |buffer, cx| {
1941        let edits = edits
1942            .into_iter()
1943            .map(|(range, text)| {
1944                (
1945                    range.start.to_point(buffer)..range.end.to_point(buffer),
1946                    text,
1947                )
1948            })
1949            .collect::<Vec<_>>();
1950
1951        assert_eq!(
1952            edits,
1953            [
1954                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1955                (Point::new(1, 0)..Point::new(2, 0), "".into())
1956            ]
1957        );
1958
1959        for (range, new_text) in edits {
1960            buffer.edit([(range, new_text)], None, cx);
1961        }
1962        assert_eq!(
1963            buffer.text(),
1964            "
1965                use a::{b, c};
1966
1967                fn f() {
1968                    b();
1969                    c();
1970                }
1971            "
1972            .unindent()
1973        );
1974    });
1975}
1976
1977fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
1978    buffer: &Buffer,
1979    range: Range<T>,
1980) -> Vec<(String, Option<DiagnosticSeverity>)> {
1981    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1982    for chunk in buffer.snapshot().chunks(range, true) {
1983        if chunks.last().map_or(false, |prev_chunk| {
1984            prev_chunk.1 == chunk.diagnostic_severity
1985        }) {
1986            chunks.last_mut().unwrap().0.push_str(chunk.text);
1987        } else {
1988            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
1989        }
1990    }
1991    chunks
1992}
1993
1994#[gpui::test(iterations = 10)]
1995async fn test_definition(cx: &mut gpui::TestAppContext) {
1996    init_test(cx);
1997
1998    let mut language = Language::new(
1999        LanguageConfig {
2000            name: "Rust".into(),
2001            path_suffixes: vec!["rs".to_string()],
2002            ..Default::default()
2003        },
2004        Some(tree_sitter_rust::language()),
2005    );
2006    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
2007
2008    let fs = FakeFs::new(cx.background());
2009    fs.insert_tree(
2010        "/dir",
2011        json!({
2012            "a.rs": "const fn a() { A }",
2013            "b.rs": "const y: i32 = crate::a()",
2014        }),
2015    )
2016    .await;
2017
2018    let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2019    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2020
2021    let buffer = project
2022        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2023        .await
2024        .unwrap();
2025
2026    let fake_server = fake_servers.next().await.unwrap();
2027    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2028        let params = params.text_document_position_params;
2029        assert_eq!(
2030            params.text_document.uri.to_file_path().unwrap(),
2031            Path::new("/dir/b.rs"),
2032        );
2033        assert_eq!(params.position, lsp::Position::new(0, 22));
2034
2035        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2036            lsp::Location::new(
2037                lsp::Url::from_file_path("/dir/a.rs").unwrap(),
2038                lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2039            ),
2040        )))
2041    });
2042
2043    let mut definitions = project
2044        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2045        .await
2046        .unwrap();
2047
2048    // Assert no new language server started
2049    cx.foreground().run_until_parked();
2050    assert!(fake_servers.try_next().is_err());
2051
2052    assert_eq!(definitions.len(), 1);
2053    let definition = definitions.pop().unwrap();
2054    cx.update(|cx| {
2055        let target_buffer = definition.target.buffer.read(cx);
2056        assert_eq!(
2057            target_buffer
2058                .file()
2059                .unwrap()
2060                .as_local()
2061                .unwrap()
2062                .abs_path(cx),
2063            Path::new("/dir/a.rs"),
2064        );
2065        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2066        assert_eq!(
2067            list_worktrees(&project, cx),
2068            [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
2069        );
2070
2071        drop(definition);
2072    });
2073    cx.read(|cx| {
2074        assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2075    });
2076
2077    fn list_worktrees<'a>(
2078        project: &'a ModelHandle<Project>,
2079        cx: &'a AppContext,
2080    ) -> Vec<(&'a Path, bool)> {
2081        project
2082            .read(cx)
2083            .worktrees(cx)
2084            .map(|worktree| {
2085                let worktree = worktree.read(cx);
2086                (
2087                    worktree.as_local().unwrap().abs_path().as_ref(),
2088                    worktree.is_visible(),
2089                )
2090            })
2091            .collect::<Vec<_>>()
2092    }
2093}
2094
2095#[gpui::test]
2096async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
2097    init_test(cx);
2098
2099    let mut language = Language::new(
2100        LanguageConfig {
2101            name: "TypeScript".into(),
2102            path_suffixes: vec!["ts".to_string()],
2103            ..Default::default()
2104        },
2105        Some(tree_sitter_typescript::language_typescript()),
2106    );
2107    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2108
2109    let fs = FakeFs::new(cx.background());
2110    fs.insert_tree(
2111        "/dir",
2112        json!({
2113            "a.ts": "",
2114        }),
2115    )
2116    .await;
2117
2118    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2119    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2120    let buffer = project
2121        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2122        .await
2123        .unwrap();
2124
2125    let fake_server = fake_language_servers.next().await.unwrap();
2126
2127    let text = "let a = b.fqn";
2128    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2129    let completions = project.update(cx, |project, cx| {
2130        project.completions(&buffer, text.len(), cx)
2131    });
2132
2133    fake_server
2134        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2135            Ok(Some(lsp::CompletionResponse::Array(vec![
2136                lsp::CompletionItem {
2137                    label: "fullyQualifiedName?".into(),
2138                    insert_text: Some("fullyQualifiedName".into()),
2139                    ..Default::default()
2140                },
2141            ])))
2142        })
2143        .next()
2144        .await;
2145    let completions = completions.await.unwrap();
2146    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2147    assert_eq!(completions.len(), 1);
2148    assert_eq!(completions[0].new_text, "fullyQualifiedName");
2149    assert_eq!(
2150        completions[0].old_range.to_offset(&snapshot),
2151        text.len() - 3..text.len()
2152    );
2153
2154    let text = "let a = \"atoms/cmp\"";
2155    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2156    let completions = project.update(cx, |project, cx| {
2157        project.completions(&buffer, text.len() - 1, cx)
2158    });
2159
2160    fake_server
2161        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2162            Ok(Some(lsp::CompletionResponse::Array(vec![
2163                lsp::CompletionItem {
2164                    label: "component".into(),
2165                    ..Default::default()
2166                },
2167            ])))
2168        })
2169        .next()
2170        .await;
2171    let completions = completions.await.unwrap();
2172    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2173    assert_eq!(completions.len(), 1);
2174    assert_eq!(completions[0].new_text, "component");
2175    assert_eq!(
2176        completions[0].old_range.to_offset(&snapshot),
2177        text.len() - 4..text.len() - 1
2178    );
2179}
2180
2181#[gpui::test]
2182async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
2183    init_test(cx);
2184
2185    let mut language = Language::new(
2186        LanguageConfig {
2187            name: "TypeScript".into(),
2188            path_suffixes: vec!["ts".to_string()],
2189            ..Default::default()
2190        },
2191        Some(tree_sitter_typescript::language_typescript()),
2192    );
2193    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2194
2195    let fs = FakeFs::new(cx.background());
2196    fs.insert_tree(
2197        "/dir",
2198        json!({
2199            "a.ts": "",
2200        }),
2201    )
2202    .await;
2203
2204    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2205    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2206    let buffer = project
2207        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2208        .await
2209        .unwrap();
2210
2211    let fake_server = fake_language_servers.next().await.unwrap();
2212
2213    let text = "let a = b.fqn";
2214    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2215    let completions = project.update(cx, |project, cx| {
2216        project.completions(&buffer, text.len(), cx)
2217    });
2218
2219    fake_server
2220        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2221            Ok(Some(lsp::CompletionResponse::Array(vec![
2222                lsp::CompletionItem {
2223                    label: "fullyQualifiedName?".into(),
2224                    insert_text: Some("fully\rQualified\r\nName".into()),
2225                    ..Default::default()
2226                },
2227            ])))
2228        })
2229        .next()
2230        .await;
2231    let completions = completions.await.unwrap();
2232    assert_eq!(completions.len(), 1);
2233    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2234}
2235
2236#[gpui::test(iterations = 10)]
2237async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2238    init_test(cx);
2239
2240    let mut language = Language::new(
2241        LanguageConfig {
2242            name: "TypeScript".into(),
2243            path_suffixes: vec!["ts".to_string()],
2244            ..Default::default()
2245        },
2246        None,
2247    );
2248    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2249
2250    let fs = FakeFs::new(cx.background());
2251    fs.insert_tree(
2252        "/dir",
2253        json!({
2254            "a.ts": "a",
2255        }),
2256    )
2257    .await;
2258
2259    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2260    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2261    let buffer = project
2262        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2263        .await
2264        .unwrap();
2265
2266    let fake_server = fake_language_servers.next().await.unwrap();
2267
2268    // Language server returns code actions that contain commands, and not edits.
2269    let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2270    fake_server
2271        .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2272            Ok(Some(vec![
2273                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2274                    title: "The code action".into(),
2275                    command: Some(lsp::Command {
2276                        title: "The command".into(),
2277                        command: "_the/command".into(),
2278                        arguments: Some(vec![json!("the-argument")]),
2279                    }),
2280                    ..Default::default()
2281                }),
2282                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2283                    title: "two".into(),
2284                    ..Default::default()
2285                }),
2286            ]))
2287        })
2288        .next()
2289        .await;
2290
2291    let action = actions.await.unwrap()[0].clone();
2292    let apply = project.update(cx, |project, cx| {
2293        project.apply_code_action(buffer.clone(), action, true, cx)
2294    });
2295
2296    // Resolving the code action does not populate its edits. In absence of
2297    // edits, we must execute the given command.
2298    fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2299        |action, _| async move { Ok(action) },
2300    );
2301
2302    // While executing the command, the language server sends the editor
2303    // a `workspaceEdit` request.
2304    fake_server
2305        .handle_request::<lsp::request::ExecuteCommand, _, _>({
2306            let fake = fake_server.clone();
2307            move |params, _| {
2308                assert_eq!(params.command, "_the/command");
2309                let fake = fake.clone();
2310                async move {
2311                    fake.server
2312                        .request::<lsp::request::ApplyWorkspaceEdit>(
2313                            lsp::ApplyWorkspaceEditParams {
2314                                label: None,
2315                                edit: lsp::WorkspaceEdit {
2316                                    changes: Some(
2317                                        [(
2318                                            lsp::Url::from_file_path("/dir/a.ts").unwrap(),
2319                                            vec![lsp::TextEdit {
2320                                                range: lsp::Range::new(
2321                                                    lsp::Position::new(0, 0),
2322                                                    lsp::Position::new(0, 0),
2323                                                ),
2324                                                new_text: "X".into(),
2325                                            }],
2326                                        )]
2327                                        .into_iter()
2328                                        .collect(),
2329                                    ),
2330                                    ..Default::default()
2331                                },
2332                            },
2333                        )
2334                        .await
2335                        .unwrap();
2336                    Ok(Some(json!(null)))
2337                }
2338            }
2339        })
2340        .next()
2341        .await;
2342
2343    // Applying the code action returns a project transaction containing the edits
2344    // sent by the language server in its `workspaceEdit` request.
2345    let transaction = apply.await.unwrap();
2346    assert!(transaction.0.contains_key(&buffer));
2347    buffer.update(cx, |buffer, cx| {
2348        assert_eq!(buffer.text(), "Xa");
2349        buffer.undo(cx);
2350        assert_eq!(buffer.text(), "a");
2351    });
2352}
2353
2354#[gpui::test(iterations = 10)]
2355async fn test_save_file(cx: &mut gpui::TestAppContext) {
2356    init_test(cx);
2357
2358    let fs = FakeFs::new(cx.background());
2359    fs.insert_tree(
2360        "/dir",
2361        json!({
2362            "file1": "the old contents",
2363        }),
2364    )
2365    .await;
2366
2367    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2368    let buffer = project
2369        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2370        .await
2371        .unwrap();
2372    buffer.update(cx, |buffer, cx| {
2373        assert_eq!(buffer.text(), "the old contents");
2374        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2375    });
2376
2377    project
2378        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2379        .await
2380        .unwrap();
2381
2382    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2383    assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2384}
2385
2386#[gpui::test]
2387async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2388    init_test(cx);
2389
2390    let fs = FakeFs::new(cx.background());
2391    fs.insert_tree(
2392        "/dir",
2393        json!({
2394            "file1": "the old contents",
2395        }),
2396    )
2397    .await;
2398
2399    let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2400    let buffer = project
2401        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2402        .await
2403        .unwrap();
2404    buffer.update(cx, |buffer, cx| {
2405        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2406    });
2407
2408    project
2409        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2410        .await
2411        .unwrap();
2412
2413    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2414    assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2415}
2416
2417#[gpui::test]
2418async fn test_save_as(cx: &mut gpui::TestAppContext) {
2419    init_test(cx);
2420
2421    let fs = FakeFs::new(cx.background());
2422    fs.insert_tree("/dir", json!({})).await;
2423
2424    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2425
2426    let languages = project.read_with(cx, |project, _| project.languages().clone());
2427    languages.register(
2428        "/some/path",
2429        LanguageConfig {
2430            name: "Rust".into(),
2431            path_suffixes: vec!["rs".into()],
2432            ..Default::default()
2433        },
2434        tree_sitter_rust::language(),
2435        vec![],
2436        |_| Default::default(),
2437    );
2438
2439    let buffer = project.update(cx, |project, cx| {
2440        project.create_buffer("", None, cx).unwrap()
2441    });
2442    buffer.update(cx, |buffer, cx| {
2443        buffer.edit([(0..0, "abc")], None, cx);
2444        assert!(buffer.is_dirty());
2445        assert!(!buffer.has_conflict());
2446        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2447    });
2448    project
2449        .update(cx, |project, cx| {
2450            project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2451        })
2452        .await
2453        .unwrap();
2454    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2455
2456    cx.foreground().run_until_parked();
2457    buffer.read_with(cx, |buffer, cx| {
2458        assert_eq!(
2459            buffer.file().unwrap().full_path(cx),
2460            Path::new("dir/file1.rs")
2461        );
2462        assert!(!buffer.is_dirty());
2463        assert!(!buffer.has_conflict());
2464        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2465    });
2466
2467    let opened_buffer = project
2468        .update(cx, |project, cx| {
2469            project.open_local_buffer("/dir/file1.rs", cx)
2470        })
2471        .await
2472        .unwrap();
2473    assert_eq!(opened_buffer, buffer);
2474}
2475
2476#[gpui::test(retries = 5)]
2477async fn test_rescan_and_remote_updates(
2478    deterministic: Arc<Deterministic>,
2479    cx: &mut gpui::TestAppContext,
2480) {
2481    init_test(cx);
2482    cx.foreground().allow_parking();
2483
2484    let dir = temp_tree(json!({
2485        "a": {
2486            "file1": "",
2487            "file2": "",
2488            "file3": "",
2489        },
2490        "b": {
2491            "c": {
2492                "file4": "",
2493                "file5": "",
2494            }
2495        }
2496    }));
2497
2498    let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2499    let rpc = project.read_with(cx, |p, _| p.client.clone());
2500
2501    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
2502        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2503        async move { buffer.await.unwrap() }
2504    };
2505    let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2506        project.read_with(cx, |project, cx| {
2507            let tree = project.worktrees(cx).next().unwrap();
2508            tree.read(cx)
2509                .entry_for_path(path)
2510                .unwrap_or_else(|| panic!("no entry for path {}", path))
2511                .id
2512        })
2513    };
2514
2515    let buffer2 = buffer_for_path("a/file2", cx).await;
2516    let buffer3 = buffer_for_path("a/file3", cx).await;
2517    let buffer4 = buffer_for_path("b/c/file4", cx).await;
2518    let buffer5 = buffer_for_path("b/c/file5", cx).await;
2519
2520    let file2_id = id_for_path("a/file2", cx);
2521    let file3_id = id_for_path("a/file3", cx);
2522    let file4_id = id_for_path("b/c/file4", cx);
2523
2524    // Create a remote copy of this worktree.
2525    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2526    let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2527    let remote = cx.update(|cx| {
2528        Worktree::remote(
2529            1,
2530            1,
2531            proto::WorktreeMetadata {
2532                id: initial_snapshot.id().to_proto(),
2533                root_name: initial_snapshot.root_name().into(),
2534                abs_path: initial_snapshot
2535                    .abs_path()
2536                    .as_os_str()
2537                    .to_string_lossy()
2538                    .into(),
2539                visible: true,
2540            },
2541            rpc.clone(),
2542            cx,
2543        )
2544    });
2545    remote.update(cx, |remote, _| {
2546        let update = initial_snapshot.build_initial_update(1);
2547        remote.as_remote_mut().unwrap().update_from_remote(update);
2548    });
2549    deterministic.run_until_parked();
2550
2551    cx.read(|cx| {
2552        assert!(!buffer2.read(cx).is_dirty());
2553        assert!(!buffer3.read(cx).is_dirty());
2554        assert!(!buffer4.read(cx).is_dirty());
2555        assert!(!buffer5.read(cx).is_dirty());
2556    });
2557
2558    // Rename and delete files and directories.
2559    tree.flush_fs_events(cx).await;
2560    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2561    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2562    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2563    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2564    tree.flush_fs_events(cx).await;
2565
2566    let expected_paths = vec![
2567        "a",
2568        "a/file1",
2569        "a/file2.new",
2570        "b",
2571        "d",
2572        "d/file3",
2573        "d/file4",
2574    ];
2575
2576    cx.read(|app| {
2577        assert_eq!(
2578            tree.read(app)
2579                .paths()
2580                .map(|p| p.to_str().unwrap())
2581                .collect::<Vec<_>>(),
2582            expected_paths
2583        );
2584
2585        assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2586        assert_eq!(id_for_path("d/file3", cx), file3_id);
2587        assert_eq!(id_for_path("d/file4", cx), file4_id);
2588
2589        assert_eq!(
2590            buffer2.read(app).file().unwrap().path().as_ref(),
2591            Path::new("a/file2.new")
2592        );
2593        assert_eq!(
2594            buffer3.read(app).file().unwrap().path().as_ref(),
2595            Path::new("d/file3")
2596        );
2597        assert_eq!(
2598            buffer4.read(app).file().unwrap().path().as_ref(),
2599            Path::new("d/file4")
2600        );
2601        assert_eq!(
2602            buffer5.read(app).file().unwrap().path().as_ref(),
2603            Path::new("b/c/file5")
2604        );
2605
2606        assert!(!buffer2.read(app).file().unwrap().is_deleted());
2607        assert!(!buffer3.read(app).file().unwrap().is_deleted());
2608        assert!(!buffer4.read(app).file().unwrap().is_deleted());
2609        assert!(buffer5.read(app).file().unwrap().is_deleted());
2610    });
2611
2612    // Update the remote worktree. Check that it becomes consistent with the
2613    // local worktree.
2614    remote.update(cx, |remote, cx| {
2615        let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
2616            &initial_snapshot,
2617            1,
2618            1,
2619            true,
2620        );
2621        remote.as_remote_mut().unwrap().update_from_remote(update);
2622    });
2623    deterministic.run_until_parked();
2624    remote.read_with(cx, |remote, _| {
2625        assert_eq!(
2626            remote
2627                .paths()
2628                .map(|p| p.to_str().unwrap())
2629                .collect::<Vec<_>>(),
2630            expected_paths
2631        );
2632    });
2633}
2634
2635#[gpui::test(iterations = 10)]
2636async fn test_buffer_identity_across_renames(
2637    deterministic: Arc<Deterministic>,
2638    cx: &mut gpui::TestAppContext,
2639) {
2640    init_test(cx);
2641
2642    let fs = FakeFs::new(cx.background());
2643    fs.insert_tree(
2644        "/dir",
2645        json!({
2646            "a": {
2647                "file1": "",
2648            }
2649        }),
2650    )
2651    .await;
2652
2653    let project = Project::test(fs, [Path::new("/dir")], cx).await;
2654    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2655    let tree_id = tree.read_with(cx, |tree, _| tree.id());
2656
2657    let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2658        project.read_with(cx, |project, cx| {
2659            let tree = project.worktrees(cx).next().unwrap();
2660            tree.read(cx)
2661                .entry_for_path(path)
2662                .unwrap_or_else(|| panic!("no entry for path {}", path))
2663                .id
2664        })
2665    };
2666
2667    let dir_id = id_for_path("a", cx);
2668    let file_id = id_for_path("a/file1", cx);
2669    let buffer = project
2670        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2671        .await
2672        .unwrap();
2673    buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2674
2675    project
2676        .update(cx, |project, cx| {
2677            project.rename_entry(dir_id, Path::new("b"), cx)
2678        })
2679        .unwrap()
2680        .await
2681        .unwrap();
2682    deterministic.run_until_parked();
2683    assert_eq!(id_for_path("b", cx), dir_id);
2684    assert_eq!(id_for_path("b/file1", cx), file_id);
2685    buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2686}
2687
2688#[gpui::test]
2689async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
2690    init_test(cx);
2691
2692    let fs = FakeFs::new(cx.background());
2693    fs.insert_tree(
2694        "/dir",
2695        json!({
2696            "a.txt": "a-contents",
2697            "b.txt": "b-contents",
2698        }),
2699    )
2700    .await;
2701
2702    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2703
2704    // Spawn multiple tasks to open paths, repeating some paths.
2705    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2706        (
2707            p.open_local_buffer("/dir/a.txt", cx),
2708            p.open_local_buffer("/dir/b.txt", cx),
2709            p.open_local_buffer("/dir/a.txt", cx),
2710        )
2711    });
2712
2713    let buffer_a_1 = buffer_a_1.await.unwrap();
2714    let buffer_a_2 = buffer_a_2.await.unwrap();
2715    let buffer_b = buffer_b.await.unwrap();
2716    assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
2717    assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
2718
2719    // There is only one buffer per path.
2720    let buffer_a_id = buffer_a_1.id();
2721    assert_eq!(buffer_a_2.id(), buffer_a_id);
2722
2723    // Open the same path again while it is still open.
2724    drop(buffer_a_1);
2725    let buffer_a_3 = project
2726        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2727        .await
2728        .unwrap();
2729
2730    // There's still only one buffer per path.
2731    assert_eq!(buffer_a_3.id(), buffer_a_id);
2732}
2733
2734#[gpui::test]
2735async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
2736    init_test(cx);
2737
2738    let fs = FakeFs::new(cx.background());
2739    fs.insert_tree(
2740        "/dir",
2741        json!({
2742            "file1": "abc",
2743            "file2": "def",
2744            "file3": "ghi",
2745        }),
2746    )
2747    .await;
2748
2749    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2750
2751    let buffer1 = project
2752        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2753        .await
2754        .unwrap();
2755    let events = Rc::new(RefCell::new(Vec::new()));
2756
2757    // initially, the buffer isn't dirty.
2758    buffer1.update(cx, |buffer, cx| {
2759        cx.subscribe(&buffer1, {
2760            let events = events.clone();
2761            move |_, _, event, _| match event {
2762                BufferEvent::Operation(_) => {}
2763                _ => events.borrow_mut().push(event.clone()),
2764            }
2765        })
2766        .detach();
2767
2768        assert!(!buffer.is_dirty());
2769        assert!(events.borrow().is_empty());
2770
2771        buffer.edit([(1..2, "")], None, cx);
2772    });
2773
2774    // after the first edit, the buffer is dirty, and emits a dirtied event.
2775    buffer1.update(cx, |buffer, cx| {
2776        assert!(buffer.text() == "ac");
2777        assert!(buffer.is_dirty());
2778        assert_eq!(
2779            *events.borrow(),
2780            &[language::Event::Edited, language::Event::DirtyChanged]
2781        );
2782        events.borrow_mut().clear();
2783        buffer.did_save(
2784            buffer.version(),
2785            buffer.as_rope().fingerprint(),
2786            buffer.file().unwrap().mtime(),
2787            cx,
2788        );
2789    });
2790
2791    // after saving, the buffer is not dirty, and emits a saved event.
2792    buffer1.update(cx, |buffer, cx| {
2793        assert!(!buffer.is_dirty());
2794        assert_eq!(*events.borrow(), &[language::Event::Saved]);
2795        events.borrow_mut().clear();
2796
2797        buffer.edit([(1..1, "B")], None, cx);
2798        buffer.edit([(2..2, "D")], None, cx);
2799    });
2800
2801    // after editing again, the buffer is dirty, and emits another dirty event.
2802    buffer1.update(cx, |buffer, cx| {
2803        assert!(buffer.text() == "aBDc");
2804        assert!(buffer.is_dirty());
2805        assert_eq!(
2806            *events.borrow(),
2807            &[
2808                language::Event::Edited,
2809                language::Event::DirtyChanged,
2810                language::Event::Edited,
2811            ],
2812        );
2813        events.borrow_mut().clear();
2814
2815        // After restoring the buffer to its previously-saved state,
2816        // the buffer is not considered dirty anymore.
2817        buffer.edit([(1..3, "")], None, cx);
2818        assert!(buffer.text() == "ac");
2819        assert!(!buffer.is_dirty());
2820    });
2821
2822    assert_eq!(
2823        *events.borrow(),
2824        &[language::Event::Edited, language::Event::DirtyChanged]
2825    );
2826
2827    // When a file is deleted, the buffer is considered dirty.
2828    let events = Rc::new(RefCell::new(Vec::new()));
2829    let buffer2 = project
2830        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2831        .await
2832        .unwrap();
2833    buffer2.update(cx, |_, cx| {
2834        cx.subscribe(&buffer2, {
2835            let events = events.clone();
2836            move |_, _, event, _| events.borrow_mut().push(event.clone())
2837        })
2838        .detach();
2839    });
2840
2841    fs.remove_file("/dir/file2".as_ref(), Default::default())
2842        .await
2843        .unwrap();
2844    cx.foreground().run_until_parked();
2845    buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
2846    assert_eq!(
2847        *events.borrow(),
2848        &[
2849            language::Event::DirtyChanged,
2850            language::Event::FileHandleChanged
2851        ]
2852    );
2853
2854    // When a file is already dirty when deleted, we don't emit a Dirtied event.
2855    let events = Rc::new(RefCell::new(Vec::new()));
2856    let buffer3 = project
2857        .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
2858        .await
2859        .unwrap();
2860    buffer3.update(cx, |_, cx| {
2861        cx.subscribe(&buffer3, {
2862            let events = events.clone();
2863            move |_, _, event, _| events.borrow_mut().push(event.clone())
2864        })
2865        .detach();
2866    });
2867
2868    buffer3.update(cx, |buffer, cx| {
2869        buffer.edit([(0..0, "x")], None, cx);
2870    });
2871    events.borrow_mut().clear();
2872    fs.remove_file("/dir/file3".as_ref(), Default::default())
2873        .await
2874        .unwrap();
2875    cx.foreground().run_until_parked();
2876    assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
2877    cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
2878}
2879
2880#[gpui::test]
2881async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
2882    init_test(cx);
2883
2884    let initial_contents = "aaa\nbbbbb\nc\n";
2885    let fs = FakeFs::new(cx.background());
2886    fs.insert_tree(
2887        "/dir",
2888        json!({
2889            "the-file": initial_contents,
2890        }),
2891    )
2892    .await;
2893    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2894    let buffer = project
2895        .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
2896        .await
2897        .unwrap();
2898
2899    let anchors = (0..3)
2900        .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
2901        .collect::<Vec<_>>();
2902
2903    // Change the file on disk, adding two new lines of text, and removing
2904    // one line.
2905    buffer.read_with(cx, |buffer, _| {
2906        assert!(!buffer.is_dirty());
2907        assert!(!buffer.has_conflict());
2908    });
2909    let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
2910    fs.save(
2911        "/dir/the-file".as_ref(),
2912        &new_contents.into(),
2913        LineEnding::Unix,
2914    )
2915    .await
2916    .unwrap();
2917
2918    // Because the buffer was not modified, it is reloaded from disk. Its
2919    // contents are edited according to the diff between the old and new
2920    // file contents.
2921    cx.foreground().run_until_parked();
2922    buffer.update(cx, |buffer, _| {
2923        assert_eq!(buffer.text(), new_contents);
2924        assert!(!buffer.is_dirty());
2925        assert!(!buffer.has_conflict());
2926
2927        let anchor_positions = anchors
2928            .iter()
2929            .map(|anchor| anchor.to_point(&*buffer))
2930            .collect::<Vec<_>>();
2931        assert_eq!(
2932            anchor_positions,
2933            [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
2934        );
2935    });
2936
2937    // Modify the buffer
2938    buffer.update(cx, |buffer, cx| {
2939        buffer.edit([(0..0, " ")], None, cx);
2940        assert!(buffer.is_dirty());
2941        assert!(!buffer.has_conflict());
2942    });
2943
2944    // Change the file on disk again, adding blank lines to the beginning.
2945    fs.save(
2946        "/dir/the-file".as_ref(),
2947        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
2948        LineEnding::Unix,
2949    )
2950    .await
2951    .unwrap();
2952
2953    // Because the buffer is modified, it doesn't reload from disk, but is
2954    // marked as having a conflict.
2955    cx.foreground().run_until_parked();
2956    buffer.read_with(cx, |buffer, _| {
2957        assert!(buffer.has_conflict());
2958    });
2959}
2960
2961#[gpui::test]
2962async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
2963    init_test(cx);
2964
2965    let fs = FakeFs::new(cx.background());
2966    fs.insert_tree(
2967        "/dir",
2968        json!({
2969            "file1": "a\nb\nc\n",
2970            "file2": "one\r\ntwo\r\nthree\r\n",
2971        }),
2972    )
2973    .await;
2974
2975    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2976    let buffer1 = project
2977        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2978        .await
2979        .unwrap();
2980    let buffer2 = project
2981        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2982        .await
2983        .unwrap();
2984
2985    buffer1.read_with(cx, |buffer, _| {
2986        assert_eq!(buffer.text(), "a\nb\nc\n");
2987        assert_eq!(buffer.line_ending(), LineEnding::Unix);
2988    });
2989    buffer2.read_with(cx, |buffer, _| {
2990        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
2991        assert_eq!(buffer.line_ending(), LineEnding::Windows);
2992    });
2993
2994    // Change a file's line endings on disk from unix to windows. The buffer's
2995    // state updates correctly.
2996    fs.save(
2997        "/dir/file1".as_ref(),
2998        &"aaa\nb\nc\n".into(),
2999        LineEnding::Windows,
3000    )
3001    .await
3002    .unwrap();
3003    cx.foreground().run_until_parked();
3004    buffer1.read_with(cx, |buffer, _| {
3005        assert_eq!(buffer.text(), "aaa\nb\nc\n");
3006        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3007    });
3008
3009    // Save a file with windows line endings. The file is written correctly.
3010    buffer2.update(cx, |buffer, cx| {
3011        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3012    });
3013    project
3014        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3015        .await
3016        .unwrap();
3017    assert_eq!(
3018        fs.load("/dir/file2".as_ref()).await.unwrap(),
3019        "one\r\ntwo\r\nthree\r\nfour\r\n",
3020    );
3021}
3022
3023#[gpui::test]
3024async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
3025    init_test(cx);
3026
3027    let fs = FakeFs::new(cx.background());
3028    fs.insert_tree(
3029        "/the-dir",
3030        json!({
3031            "a.rs": "
3032                fn foo(mut v: Vec<usize>) {
3033                    for x in &v {
3034                        v.push(1);
3035                    }
3036                }
3037            "
3038            .unindent(),
3039        }),
3040    )
3041    .await;
3042
3043    let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3044    let buffer = project
3045        .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3046        .await
3047        .unwrap();
3048
3049    let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
3050    let message = lsp::PublishDiagnosticsParams {
3051        uri: buffer_uri.clone(),
3052        diagnostics: vec![
3053            lsp::Diagnostic {
3054                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3055                severity: Some(DiagnosticSeverity::WARNING),
3056                message: "error 1".to_string(),
3057                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3058                    location: lsp::Location {
3059                        uri: buffer_uri.clone(),
3060                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3061                    },
3062                    message: "error 1 hint 1".to_string(),
3063                }]),
3064                ..Default::default()
3065            },
3066            lsp::Diagnostic {
3067                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3068                severity: Some(DiagnosticSeverity::HINT),
3069                message: "error 1 hint 1".to_string(),
3070                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3071                    location: lsp::Location {
3072                        uri: buffer_uri.clone(),
3073                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3074                    },
3075                    message: "original diagnostic".to_string(),
3076                }]),
3077                ..Default::default()
3078            },
3079            lsp::Diagnostic {
3080                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3081                severity: Some(DiagnosticSeverity::ERROR),
3082                message: "error 2".to_string(),
3083                related_information: Some(vec![
3084                    lsp::DiagnosticRelatedInformation {
3085                        location: lsp::Location {
3086                            uri: buffer_uri.clone(),
3087                            range: lsp::Range::new(
3088                                lsp::Position::new(1, 13),
3089                                lsp::Position::new(1, 15),
3090                            ),
3091                        },
3092                        message: "error 2 hint 1".to_string(),
3093                    },
3094                    lsp::DiagnosticRelatedInformation {
3095                        location: lsp::Location {
3096                            uri: buffer_uri.clone(),
3097                            range: lsp::Range::new(
3098                                lsp::Position::new(1, 13),
3099                                lsp::Position::new(1, 15),
3100                            ),
3101                        },
3102                        message: "error 2 hint 2".to_string(),
3103                    },
3104                ]),
3105                ..Default::default()
3106            },
3107            lsp::Diagnostic {
3108                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3109                severity: Some(DiagnosticSeverity::HINT),
3110                message: "error 2 hint 1".to_string(),
3111                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3112                    location: lsp::Location {
3113                        uri: buffer_uri.clone(),
3114                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3115                    },
3116                    message: "original diagnostic".to_string(),
3117                }]),
3118                ..Default::default()
3119            },
3120            lsp::Diagnostic {
3121                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3122                severity: Some(DiagnosticSeverity::HINT),
3123                message: "error 2 hint 2".to_string(),
3124                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3125                    location: lsp::Location {
3126                        uri: buffer_uri,
3127                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3128                    },
3129                    message: "original diagnostic".to_string(),
3130                }]),
3131                ..Default::default()
3132            },
3133        ],
3134        version: None,
3135    };
3136
3137    project
3138        .update(cx, |p, cx| {
3139            p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3140        })
3141        .unwrap();
3142    let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
3143
3144    assert_eq!(
3145        buffer
3146            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3147            .collect::<Vec<_>>(),
3148        &[
3149            DiagnosticEntry {
3150                range: Point::new(1, 8)..Point::new(1, 9),
3151                diagnostic: Diagnostic {
3152                    severity: DiagnosticSeverity::WARNING,
3153                    message: "error 1".to_string(),
3154                    group_id: 1,
3155                    is_primary: true,
3156                    ..Default::default()
3157                }
3158            },
3159            DiagnosticEntry {
3160                range: Point::new(1, 8)..Point::new(1, 9),
3161                diagnostic: Diagnostic {
3162                    severity: DiagnosticSeverity::HINT,
3163                    message: "error 1 hint 1".to_string(),
3164                    group_id: 1,
3165                    is_primary: false,
3166                    ..Default::default()
3167                }
3168            },
3169            DiagnosticEntry {
3170                range: Point::new(1, 13)..Point::new(1, 15),
3171                diagnostic: Diagnostic {
3172                    severity: DiagnosticSeverity::HINT,
3173                    message: "error 2 hint 1".to_string(),
3174                    group_id: 0,
3175                    is_primary: false,
3176                    ..Default::default()
3177                }
3178            },
3179            DiagnosticEntry {
3180                range: Point::new(1, 13)..Point::new(1, 15),
3181                diagnostic: Diagnostic {
3182                    severity: DiagnosticSeverity::HINT,
3183                    message: "error 2 hint 2".to_string(),
3184                    group_id: 0,
3185                    is_primary: false,
3186                    ..Default::default()
3187                }
3188            },
3189            DiagnosticEntry {
3190                range: Point::new(2, 8)..Point::new(2, 17),
3191                diagnostic: Diagnostic {
3192                    severity: DiagnosticSeverity::ERROR,
3193                    message: "error 2".to_string(),
3194                    group_id: 0,
3195                    is_primary: true,
3196                    ..Default::default()
3197                }
3198            }
3199        ]
3200    );
3201
3202    assert_eq!(
3203        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3204        &[
3205            DiagnosticEntry {
3206                range: Point::new(1, 13)..Point::new(1, 15),
3207                diagnostic: Diagnostic {
3208                    severity: DiagnosticSeverity::HINT,
3209                    message: "error 2 hint 1".to_string(),
3210                    group_id: 0,
3211                    is_primary: false,
3212                    ..Default::default()
3213                }
3214            },
3215            DiagnosticEntry {
3216                range: Point::new(1, 13)..Point::new(1, 15),
3217                diagnostic: Diagnostic {
3218                    severity: DiagnosticSeverity::HINT,
3219                    message: "error 2 hint 2".to_string(),
3220                    group_id: 0,
3221                    is_primary: false,
3222                    ..Default::default()
3223                }
3224            },
3225            DiagnosticEntry {
3226                range: Point::new(2, 8)..Point::new(2, 17),
3227                diagnostic: Diagnostic {
3228                    severity: DiagnosticSeverity::ERROR,
3229                    message: "error 2".to_string(),
3230                    group_id: 0,
3231                    is_primary: true,
3232                    ..Default::default()
3233                }
3234            }
3235        ]
3236    );
3237
3238    assert_eq!(
3239        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3240        &[
3241            DiagnosticEntry {
3242                range: Point::new(1, 8)..Point::new(1, 9),
3243                diagnostic: Diagnostic {
3244                    severity: DiagnosticSeverity::WARNING,
3245                    message: "error 1".to_string(),
3246                    group_id: 1,
3247                    is_primary: true,
3248                    ..Default::default()
3249                }
3250            },
3251            DiagnosticEntry {
3252                range: Point::new(1, 8)..Point::new(1, 9),
3253                diagnostic: Diagnostic {
3254                    severity: DiagnosticSeverity::HINT,
3255                    message: "error 1 hint 1".to_string(),
3256                    group_id: 1,
3257                    is_primary: false,
3258                    ..Default::default()
3259                }
3260            },
3261        ]
3262    );
3263}
3264
3265#[gpui::test]
3266async fn test_rename(cx: &mut gpui::TestAppContext) {
3267    init_test(cx);
3268
3269    let mut language = Language::new(
3270        LanguageConfig {
3271            name: "Rust".into(),
3272            path_suffixes: vec!["rs".to_string()],
3273            ..Default::default()
3274        },
3275        Some(tree_sitter_rust::language()),
3276    );
3277    let mut fake_servers = language
3278        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3279            capabilities: lsp::ServerCapabilities {
3280                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3281                    prepare_provider: Some(true),
3282                    work_done_progress_options: Default::default(),
3283                })),
3284                ..Default::default()
3285            },
3286            ..Default::default()
3287        }))
3288        .await;
3289
3290    let fs = FakeFs::new(cx.background());
3291    fs.insert_tree(
3292        "/dir",
3293        json!({
3294            "one.rs": "const ONE: usize = 1;",
3295            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3296        }),
3297    )
3298    .await;
3299
3300    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3301    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3302    let buffer = project
3303        .update(cx, |project, cx| {
3304            project.open_local_buffer("/dir/one.rs", cx)
3305        })
3306        .await
3307        .unwrap();
3308
3309    let fake_server = fake_servers.next().await.unwrap();
3310
3311    let response = project.update(cx, |project, cx| {
3312        project.prepare_rename(buffer.clone(), 7, cx)
3313    });
3314    fake_server
3315        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3316            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3317            assert_eq!(params.position, lsp::Position::new(0, 7));
3318            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3319                lsp::Position::new(0, 6),
3320                lsp::Position::new(0, 9),
3321            ))))
3322        })
3323        .next()
3324        .await
3325        .unwrap();
3326    let range = response.await.unwrap().unwrap();
3327    let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
3328    assert_eq!(range, 6..9);
3329
3330    let response = project.update(cx, |project, cx| {
3331        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3332    });
3333    fake_server
3334        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3335            assert_eq!(
3336                params.text_document_position.text_document.uri.as_str(),
3337                "file:///dir/one.rs"
3338            );
3339            assert_eq!(
3340                params.text_document_position.position,
3341                lsp::Position::new(0, 7)
3342            );
3343            assert_eq!(params.new_name, "THREE");
3344            Ok(Some(lsp::WorkspaceEdit {
3345                changes: Some(
3346                    [
3347                        (
3348                            lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3349                            vec![lsp::TextEdit::new(
3350                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3351                                "THREE".to_string(),
3352                            )],
3353                        ),
3354                        (
3355                            lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3356                            vec![
3357                                lsp::TextEdit::new(
3358                                    lsp::Range::new(
3359                                        lsp::Position::new(0, 24),
3360                                        lsp::Position::new(0, 27),
3361                                    ),
3362                                    "THREE".to_string(),
3363                                ),
3364                                lsp::TextEdit::new(
3365                                    lsp::Range::new(
3366                                        lsp::Position::new(0, 35),
3367                                        lsp::Position::new(0, 38),
3368                                    ),
3369                                    "THREE".to_string(),
3370                                ),
3371                            ],
3372                        ),
3373                    ]
3374                    .into_iter()
3375                    .collect(),
3376                ),
3377                ..Default::default()
3378            }))
3379        })
3380        .next()
3381        .await
3382        .unwrap();
3383    let mut transaction = response.await.unwrap().0;
3384    assert_eq!(transaction.len(), 2);
3385    assert_eq!(
3386        transaction
3387            .remove_entry(&buffer)
3388            .unwrap()
3389            .0
3390            .read_with(cx, |buffer, _| buffer.text()),
3391        "const THREE: usize = 1;"
3392    );
3393    assert_eq!(
3394        transaction
3395            .into_keys()
3396            .next()
3397            .unwrap()
3398            .read_with(cx, |buffer, _| buffer.text()),
3399        "const TWO: usize = one::THREE + one::THREE;"
3400    );
3401}
3402
3403#[gpui::test]
3404async fn test_search(cx: &mut gpui::TestAppContext) {
3405    init_test(cx);
3406
3407    let fs = FakeFs::new(cx.background());
3408    fs.insert_tree(
3409        "/dir",
3410        json!({
3411            "one.rs": "const ONE: usize = 1;",
3412            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3413            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3414            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3415        }),
3416    )
3417    .await;
3418    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3419    assert_eq!(
3420        search(
3421            &project,
3422            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
3423            cx
3424        )
3425        .await
3426        .unwrap(),
3427        HashMap::from_iter([
3428            ("two.rs".to_string(), vec![6..9]),
3429            ("three.rs".to_string(), vec![37..40])
3430        ])
3431    );
3432
3433    let buffer_4 = project
3434        .update(cx, |project, cx| {
3435            project.open_local_buffer("/dir/four.rs", cx)
3436        })
3437        .await
3438        .unwrap();
3439    buffer_4.update(cx, |buffer, cx| {
3440        let text = "two::TWO";
3441        buffer.edit([(20..28, text), (31..43, text)], None, cx);
3442    });
3443
3444    assert_eq!(
3445        search(
3446            &project,
3447            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
3448            cx
3449        )
3450        .await
3451        .unwrap(),
3452        HashMap::from_iter([
3453            ("two.rs".to_string(), vec![6..9]),
3454            ("three.rs".to_string(), vec![37..40]),
3455            ("four.rs".to_string(), vec![25..28, 36..39])
3456        ])
3457    );
3458}
3459
3460#[gpui::test]
3461async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
3462    init_test(cx);
3463
3464    let search_query = "file";
3465
3466    let fs = FakeFs::new(cx.background());
3467    fs.insert_tree(
3468        "/dir",
3469        json!({
3470            "one.rs": r#"// Rust file one"#,
3471            "one.ts": r#"// TypeScript file one"#,
3472            "two.rs": r#"// Rust file two"#,
3473            "two.ts": r#"// TypeScript file two"#,
3474        }),
3475    )
3476    .await;
3477    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3478
3479    assert!(
3480        search(
3481            &project,
3482            SearchQuery::text(
3483                search_query,
3484                false,
3485                true,
3486                vec![Glob::new("*.odd").unwrap().compile_matcher()],
3487                Vec::new()
3488            ),
3489            cx
3490        )
3491        .await
3492        .unwrap()
3493        .is_empty(),
3494        "If no inclusions match, no files should be returned"
3495    );
3496
3497    assert_eq!(
3498        search(
3499            &project,
3500            SearchQuery::text(
3501                search_query,
3502                false,
3503                true,
3504                vec![Glob::new("*.rs").unwrap().compile_matcher()],
3505                Vec::new()
3506            ),
3507            cx
3508        )
3509        .await
3510        .unwrap(),
3511        HashMap::from_iter([
3512            ("one.rs".to_string(), vec![8..12]),
3513            ("two.rs".to_string(), vec![8..12]),
3514        ]),
3515        "Rust only search should give only Rust files"
3516    );
3517
3518    assert_eq!(
3519        search(
3520            &project,
3521            SearchQuery::text(
3522                search_query,
3523                false,
3524                true,
3525                vec![
3526                    Glob::new("*.ts").unwrap().compile_matcher(),
3527                    Glob::new("*.odd").unwrap().compile_matcher(),
3528                ],
3529                Vec::new()
3530            ),
3531            cx
3532        )
3533        .await
3534        .unwrap(),
3535        HashMap::from_iter([
3536            ("one.ts".to_string(), vec![14..18]),
3537            ("two.ts".to_string(), vec![14..18]),
3538        ]),
3539        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
3540    );
3541
3542    assert_eq!(
3543        search(
3544            &project,
3545            SearchQuery::text(
3546                search_query,
3547                false,
3548                true,
3549                vec![
3550                    Glob::new("*.rs").unwrap().compile_matcher(),
3551                    Glob::new("*.ts").unwrap().compile_matcher(),
3552                    Glob::new("*.odd").unwrap().compile_matcher(),
3553                ],
3554                Vec::new()
3555            ),
3556            cx
3557        )
3558        .await
3559        .unwrap(),
3560        HashMap::from_iter([
3561            ("one.rs".to_string(), vec![8..12]),
3562            ("one.ts".to_string(), vec![14..18]),
3563            ("two.rs".to_string(), vec![8..12]),
3564            ("two.ts".to_string(), vec![14..18]),
3565        ]),
3566        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
3567    );
3568}
3569
3570#[gpui::test]
3571async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
3572    init_test(cx);
3573
3574    let search_query = "file";
3575
3576    let fs = FakeFs::new(cx.background());
3577    fs.insert_tree(
3578        "/dir",
3579        json!({
3580            "one.rs": r#"// Rust file one"#,
3581            "one.ts": r#"// TypeScript file one"#,
3582            "two.rs": r#"// Rust file two"#,
3583            "two.ts": r#"// TypeScript file two"#,
3584        }),
3585    )
3586    .await;
3587    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3588
3589    assert_eq!(
3590        search(
3591            &project,
3592            SearchQuery::text(
3593                search_query,
3594                false,
3595                true,
3596                Vec::new(),
3597                vec![Glob::new("*.odd").unwrap().compile_matcher()],
3598            ),
3599            cx
3600        )
3601        .await
3602        .unwrap(),
3603        HashMap::from_iter([
3604            ("one.rs".to_string(), vec![8..12]),
3605            ("one.ts".to_string(), vec![14..18]),
3606            ("two.rs".to_string(), vec![8..12]),
3607            ("two.ts".to_string(), vec![14..18]),
3608        ]),
3609        "If no exclusions match, all files should be returned"
3610    );
3611
3612    assert_eq!(
3613        search(
3614            &project,
3615            SearchQuery::text(
3616                search_query,
3617                false,
3618                true,
3619                Vec::new(),
3620                vec![Glob::new("*.rs").unwrap().compile_matcher()],
3621            ),
3622            cx
3623        )
3624        .await
3625        .unwrap(),
3626        HashMap::from_iter([
3627            ("one.ts".to_string(), vec![14..18]),
3628            ("two.ts".to_string(), vec![14..18]),
3629        ]),
3630        "Rust exclusion search should give only TypeScript files"
3631    );
3632
3633    assert_eq!(
3634        search(
3635            &project,
3636            SearchQuery::text(
3637                search_query,
3638                false,
3639                true,
3640                Vec::new(),
3641                vec![
3642                    Glob::new("*.ts").unwrap().compile_matcher(),
3643                    Glob::new("*.odd").unwrap().compile_matcher(),
3644                ],
3645            ),
3646            cx
3647        )
3648        .await
3649        .unwrap(),
3650        HashMap::from_iter([
3651            ("one.rs".to_string(), vec![8..12]),
3652            ("two.rs".to_string(), vec![8..12]),
3653        ]),
3654        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
3655    );
3656
3657    assert!(
3658        search(
3659            &project,
3660            SearchQuery::text(
3661                search_query,
3662                false,
3663                true,
3664                Vec::new(),
3665                vec![
3666                    Glob::new("*.rs").unwrap().compile_matcher(),
3667                    Glob::new("*.ts").unwrap().compile_matcher(),
3668                    Glob::new("*.odd").unwrap().compile_matcher(),
3669                ],
3670            ),
3671            cx
3672        )
3673        .await
3674        .unwrap().is_empty(),
3675        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
3676    );
3677}
3678
3679#[gpui::test]
3680async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
3681    init_test(cx);
3682
3683    let search_query = "file";
3684
3685    let fs = FakeFs::new(cx.background());
3686    fs.insert_tree(
3687        "/dir",
3688        json!({
3689            "one.rs": r#"// Rust file one"#,
3690            "one.ts": r#"// TypeScript file one"#,
3691            "two.rs": r#"// Rust file two"#,
3692            "two.ts": r#"// TypeScript file two"#,
3693        }),
3694    )
3695    .await;
3696    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3697
3698    assert!(
3699        search(
3700            &project,
3701            SearchQuery::text(
3702                search_query,
3703                false,
3704                true,
3705                vec![Glob::new("*.odd").unwrap().compile_matcher()],
3706                vec![Glob::new("*.odd").unwrap().compile_matcher()],
3707            ),
3708            cx
3709        )
3710        .await
3711        .unwrap()
3712        .is_empty(),
3713        "If both no exclusions and inclusions match, exclusions should win and return nothing"
3714    );
3715
3716    assert!(
3717        search(
3718            &project,
3719            SearchQuery::text(
3720                search_query,
3721                false,
3722                true,
3723                vec![Glob::new("*.ts").unwrap().compile_matcher()],
3724                vec![Glob::new("*.ts").unwrap().compile_matcher()],
3725            ),
3726            cx
3727        )
3728        .await
3729        .unwrap()
3730        .is_empty(),
3731        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
3732    );
3733
3734    assert!(
3735        search(
3736            &project,
3737            SearchQuery::text(
3738                search_query,
3739                false,
3740                true,
3741                vec![
3742                    Glob::new("*.ts").unwrap().compile_matcher(),
3743                    Glob::new("*.odd").unwrap().compile_matcher()
3744                ],
3745                vec![
3746                    Glob::new("*.ts").unwrap().compile_matcher(),
3747                    Glob::new("*.odd").unwrap().compile_matcher()
3748                ],
3749            ),
3750            cx
3751        )
3752        .await
3753        .unwrap()
3754        .is_empty(),
3755        "Non-matching inclusions and exclusions should not change that."
3756    );
3757
3758    assert_eq!(
3759        search(
3760            &project,
3761            SearchQuery::text(
3762                search_query,
3763                false,
3764                true,
3765                vec![
3766                    Glob::new("*.ts").unwrap().compile_matcher(),
3767                    Glob::new("*.odd").unwrap().compile_matcher()
3768                ],
3769                vec![
3770                    Glob::new("*.rs").unwrap().compile_matcher(),
3771                    Glob::new("*.odd").unwrap().compile_matcher()
3772                ],
3773            ),
3774            cx
3775        )
3776        .await
3777        .unwrap(),
3778        HashMap::from_iter([
3779            ("one.ts".to_string(), vec![14..18]),
3780            ("two.ts".to_string(), vec![14..18]),
3781        ]),
3782        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
3783    );
3784}
3785
3786async fn search(
3787    project: &ModelHandle<Project>,
3788    query: SearchQuery,
3789    cx: &mut gpui::TestAppContext,
3790) -> Result<HashMap<String, Vec<Range<usize>>>> {
3791    let results = project
3792        .update(cx, |project, cx| project.search(query, cx))
3793        .await?;
3794
3795    Ok(results
3796        .into_iter()
3797        .map(|(buffer, ranges)| {
3798            buffer.read_with(cx, |buffer, _| {
3799                let path = buffer.file().unwrap().path().to_string_lossy().to_string();
3800                let ranges = ranges
3801                    .into_iter()
3802                    .map(|range| range.to_offset(buffer))
3803                    .collect::<Vec<_>>();
3804                (path, ranges)
3805            })
3806        })
3807        .collect())
3808}
3809
3810fn init_test(cx: &mut gpui::TestAppContext) {
3811    cx.foreground().forbid_parking();
3812
3813    cx.update(|cx| {
3814        cx.set_global(SettingsStore::test(cx));
3815        language::init(cx);
3816        Project::init_settings(cx);
3817    });
3818}