project_tests.rs

   1use crate::{Event, *};
   2use fs::FakeFs;
   3use futures::{future, StreamExt};
   4use gpui::{AppContext, SemanticVersion, UpdateGlobal};
   5use language::{
   6    language_settings::{AllLanguageSettings, LanguageSettingsContent},
   7    tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
   8    LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint,
   9};
  10use parking_lot::Mutex;
  11use pretty_assertions::assert_eq;
  12use serde_json::json;
  13#[cfg(not(windows))]
  14use std::os;
  15use std::task::Poll;
  16use task::{ResolvedTask, TaskContext, TaskTemplate, TaskTemplates};
  17use unindent::Unindent as _;
  18use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
  19
  20#[gpui::test]
  21async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
  22    cx.executor().allow_parking();
  23
  24    let (tx, mut rx) = futures::channel::mpsc::unbounded();
  25    let _thread = std::thread::spawn(move || {
  26        std::fs::metadata("/tmp").unwrap();
  27        std::thread::sleep(Duration::from_millis(1000));
  28        tx.unbounded_send(1).unwrap();
  29    });
  30    rx.next().await.unwrap();
  31}
  32
  33#[gpui::test]
  34async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
  35    cx.executor().allow_parking();
  36
  37    let io_task = smol::unblock(move || {
  38        println!("sleeping on thread {:?}", std::thread::current().id());
  39        std::thread::sleep(Duration::from_millis(10));
  40        1
  41    });
  42
  43    let task = cx.foreground_executor().spawn(async move {
  44        io_task.await;
  45    });
  46
  47    task.await;
  48}
  49
  50#[cfg(not(windows))]
  51#[gpui::test]
  52async fn test_symlinks(cx: &mut gpui::TestAppContext) {
  53    init_test(cx);
  54    cx.executor().allow_parking();
  55
  56    let dir = temp_tree(json!({
  57        "root": {
  58            "apple": "",
  59            "banana": {
  60                "carrot": {
  61                    "date": "",
  62                    "endive": "",
  63                }
  64            },
  65            "fennel": {
  66                "grape": "",
  67            }
  68        }
  69    }));
  70
  71    let root_link_path = dir.path().join("root_link");
  72    os::unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
  73    os::unix::fs::symlink(
  74        &dir.path().join("root/fennel"),
  75        &dir.path().join("root/finnochio"),
  76    )
  77    .unwrap();
  78
  79    let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
  80
  81    project.update(cx, |project, cx| {
  82        let tree = project.worktrees().next().unwrap().read(cx);
  83        assert_eq!(tree.file_count(), 5);
  84        assert_eq!(
  85            tree.inode_for_path("fennel/grape"),
  86            tree.inode_for_path("finnochio/grape")
  87        );
  88    });
  89}
  90
  91#[gpui::test]
  92async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
  93    init_test(cx);
  94
  95    let fs = FakeFs::new(cx.executor());
  96    fs.insert_tree(
  97        "/the-root",
  98        json!({
  99            ".zed": {
 100                "settings.json": r#"{ "tab_size": 8 }"#,
 101                "tasks.json": r#"[{
 102                    "label": "cargo check",
 103                    "command": "cargo",
 104                    "args": ["check", "--all"]
 105                },]"#,
 106            },
 107            "a": {
 108                "a.rs": "fn a() {\n    A\n}"
 109            },
 110            "b": {
 111                ".zed": {
 112                    "settings.json": r#"{ "tab_size": 2 }"#,
 113                    "tasks.json": r#"[{
 114                        "label": "cargo check",
 115                        "command": "cargo",
 116                        "args": ["check"]
 117                    },]"#,
 118                },
 119                "b.rs": "fn b() {\n  B\n}"
 120            }
 121        }),
 122    )
 123    .await;
 124
 125    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 126    let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
 127    let task_context = TaskContext::default();
 128
 129    cx.executor().run_until_parked();
 130    let worktree_id = cx.update(|cx| {
 131        project.update(cx, |project, cx| {
 132            project.worktrees().next().unwrap().read(cx).id()
 133        })
 134    });
 135    let global_task_source_kind = TaskSourceKind::Worktree {
 136        id: worktree_id,
 137        abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
 138        id_base: "local_tasks_for_worktree".into(),
 139    };
 140
 141    let all_tasks = cx
 142        .update(|cx| {
 143            let tree = worktree.read(cx);
 144
 145            let settings_a = language_settings(
 146                None,
 147                Some(
 148                    &(File::for_entry(
 149                        tree.entry_for_path("a/a.rs").unwrap().clone(),
 150                        worktree.clone(),
 151                    ) as _),
 152                ),
 153                cx,
 154            );
 155            let settings_b = language_settings(
 156                None,
 157                Some(
 158                    &(File::for_entry(
 159                        tree.entry_for_path("b/b.rs").unwrap().clone(),
 160                        worktree.clone(),
 161                    ) as _),
 162                ),
 163                cx,
 164            );
 165
 166            assert_eq!(settings_a.tab_size.get(), 8);
 167            assert_eq!(settings_b.tab_size.get(), 2);
 168
 169            get_all_tasks(&project, Some(worktree_id), &task_context, cx)
 170        })
 171        .await
 172        .into_iter()
 173        .map(|(source_kind, task)| {
 174            let resolved = task.resolved.unwrap();
 175            (
 176                source_kind,
 177                task.resolved_label,
 178                resolved.args,
 179                resolved.env,
 180            )
 181        })
 182        .collect::<Vec<_>>();
 183    assert_eq!(
 184        all_tasks,
 185        vec![
 186            (
 187                global_task_source_kind.clone(),
 188                "cargo check".to_string(),
 189                vec!["check".to_string(), "--all".to_string()],
 190                HashMap::default(),
 191            ),
 192            (
 193                TaskSourceKind::Worktree {
 194                    id: worktree_id,
 195                    abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
 196                    id_base: "local_tasks_for_worktree".into(),
 197                },
 198                "cargo check".to_string(),
 199                vec!["check".to_string()],
 200                HashMap::default(),
 201            ),
 202        ]
 203    );
 204
 205    let (_, resolved_task) = cx
 206        .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
 207        .await
 208        .into_iter()
 209        .find(|(source_kind, _)| source_kind == &global_task_source_kind)
 210        .expect("should have one global task");
 211    project.update(cx, |project, cx| {
 212        project.task_inventory().update(cx, |inventory, _| {
 213            inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
 214        });
 215    });
 216
 217    let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
 218        label: "cargo check".to_string(),
 219        command: "cargo".to_string(),
 220        args: vec![
 221            "check".to_string(),
 222            "--all".to_string(),
 223            "--all-targets".to_string(),
 224        ],
 225        env: HashMap::from_iter(Some((
 226            "RUSTFLAGS".to_string(),
 227            "-Zunstable-options".to_string(),
 228        ))),
 229        ..TaskTemplate::default()
 230    }]))
 231    .unwrap();
 232    let (tx, rx) = futures::channel::mpsc::unbounded();
 233    cx.update(|cx| {
 234        project.update(cx, |project, cx| {
 235            project.task_inventory().update(cx, |inventory, cx| {
 236                inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
 237                inventory.add_source(
 238                    global_task_source_kind.clone(),
 239                    |tx, cx| StaticSource::new(TrackedFile::new(rx, tx, cx)),
 240                    cx,
 241                );
 242            });
 243        })
 244    });
 245    tx.unbounded_send(tasks).unwrap();
 246
 247    cx.run_until_parked();
 248    let all_tasks = cx
 249        .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
 250        .await
 251        .into_iter()
 252        .map(|(source_kind, task)| {
 253            let resolved = task.resolved.unwrap();
 254            (
 255                source_kind,
 256                task.resolved_label,
 257                resolved.args,
 258                resolved.env,
 259            )
 260        })
 261        .collect::<Vec<_>>();
 262    assert_eq!(
 263        all_tasks,
 264        vec![
 265            (
 266                TaskSourceKind::Worktree {
 267                    id: worktree_id,
 268                    abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
 269                    id_base: "local_tasks_for_worktree".into(),
 270                },
 271                "cargo check".to_string(),
 272                vec![
 273                    "check".to_string(),
 274                    "--all".to_string(),
 275                    "--all-targets".to_string()
 276                ],
 277                HashMap::from_iter(Some((
 278                    "RUSTFLAGS".to_string(),
 279                    "-Zunstable-options".to_string()
 280                ))),
 281            ),
 282            (
 283                TaskSourceKind::Worktree {
 284                    id: worktree_id,
 285                    abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
 286                    id_base: "local_tasks_for_worktree".into(),
 287                },
 288                "cargo check".to_string(),
 289                vec!["check".to_string()],
 290                HashMap::default(),
 291            ),
 292        ]
 293    );
 294}
 295
 296#[gpui::test]
 297async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
 298    init_test(cx);
 299
 300    let fs = FakeFs::new(cx.executor());
 301    fs.insert_tree(
 302        "/the-root",
 303        json!({
 304            "test.rs": "const A: i32 = 1;",
 305            "test2.rs": "",
 306            "Cargo.toml": "a = 1",
 307            "package.json": "{\"a\": 1}",
 308        }),
 309    )
 310    .await;
 311
 312    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 313    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 314
 315    let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
 316        "Rust",
 317        FakeLspAdapter {
 318            name: "the-rust-language-server",
 319            capabilities: lsp::ServerCapabilities {
 320                completion_provider: Some(lsp::CompletionOptions {
 321                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
 322                    ..Default::default()
 323                }),
 324                ..Default::default()
 325            },
 326            ..Default::default()
 327        },
 328    );
 329    let mut fake_json_servers = language_registry.register_fake_lsp_adapter(
 330        "JSON",
 331        FakeLspAdapter {
 332            name: "the-json-language-server",
 333            capabilities: lsp::ServerCapabilities {
 334                completion_provider: Some(lsp::CompletionOptions {
 335                    trigger_characters: Some(vec![":".to_string()]),
 336                    ..Default::default()
 337                }),
 338                ..Default::default()
 339            },
 340            ..Default::default()
 341        },
 342    );
 343
 344    // Open a buffer without an associated language server.
 345    let toml_buffer = project
 346        .update(cx, |project, cx| {
 347            project.open_local_buffer("/the-root/Cargo.toml", cx)
 348        })
 349        .await
 350        .unwrap();
 351
 352    // Open a buffer with an associated language server before the language for it has been loaded.
 353    let rust_buffer = project
 354        .update(cx, |project, cx| {
 355            project.open_local_buffer("/the-root/test.rs", cx)
 356        })
 357        .await
 358        .unwrap();
 359    rust_buffer.update(cx, |buffer, _| {
 360        assert_eq!(buffer.language().map(|l| l.name()), None);
 361    });
 362
 363    // Now we add the languages to the project, and ensure they get assigned to all
 364    // the relevant open buffers.
 365    language_registry.add(json_lang());
 366    language_registry.add(rust_lang());
 367    cx.executor().run_until_parked();
 368    rust_buffer.update(cx, |buffer, _| {
 369        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
 370    });
 371
 372    // A server is started up, and it is notified about Rust files.
 373    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 374    assert_eq!(
 375        fake_rust_server
 376            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 377            .await
 378            .text_document,
 379        lsp::TextDocumentItem {
 380            uri: lsp::Uri::from_file_path("/the-root/test.rs")
 381                .unwrap()
 382                .into(),
 383            version: 0,
 384            text: "const A: i32 = 1;".to_string(),
 385            language_id: "rust".to_string(),
 386        }
 387    );
 388
 389    // The buffer is configured based on the language server's capabilities.
 390    rust_buffer.update(cx, |buffer, _| {
 391        assert_eq!(
 392            buffer.completion_triggers(),
 393            &[".".to_string(), "::".to_string()]
 394        );
 395    });
 396    toml_buffer.update(cx, |buffer, _| {
 397        assert!(buffer.completion_triggers().is_empty());
 398    });
 399
 400    // Edit a buffer. The changes are reported to the language server.
 401    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
 402    assert_eq!(
 403        fake_rust_server
 404            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 405            .await
 406            .text_document,
 407        lsp::VersionedTextDocumentIdentifier::new(
 408            lsp::Uri::from_file_path("/the-root/test.rs")
 409                .unwrap()
 410                .into(),
 411            1
 412        )
 413    );
 414
 415    // Open a third buffer with a different associated language server.
 416    let json_buffer = project
 417        .update(cx, |project, cx| {
 418            project.open_local_buffer("/the-root/package.json", cx)
 419        })
 420        .await
 421        .unwrap();
 422
 423    // A json language server is started up and is only notified about the json buffer.
 424    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 425    assert_eq!(
 426        fake_json_server
 427            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 428            .await
 429            .text_document,
 430        lsp::TextDocumentItem {
 431            uri: lsp::Uri::from_file_path("/the-root/package.json")
 432                .unwrap()
 433                .into(),
 434            version: 0,
 435            text: "{\"a\": 1}".to_string(),
 436            language_id: "json".to_string(),
 437        }
 438    );
 439
 440    // This buffer is configured based on the second language server's
 441    // capabilities.
 442    json_buffer.update(cx, |buffer, _| {
 443        assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
 444    });
 445
 446    // When opening another buffer whose language server is already running,
 447    // it is also configured based on the existing language server's capabilities.
 448    let rust_buffer2 = project
 449        .update(cx, |project, cx| {
 450            project.open_local_buffer("/the-root/test2.rs", cx)
 451        })
 452        .await
 453        .unwrap();
 454    rust_buffer2.update(cx, |buffer, _| {
 455        assert_eq!(
 456            buffer.completion_triggers(),
 457            &[".".to_string(), "::".to_string()]
 458        );
 459    });
 460
 461    // Changes are reported only to servers matching the buffer's language.
 462    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
 463    rust_buffer2.update(cx, |buffer, cx| {
 464        buffer.edit([(0..0, "let x = 1;")], None, cx)
 465    });
 466    assert_eq!(
 467        fake_rust_server
 468            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 469            .await
 470            .text_document,
 471        lsp::VersionedTextDocumentIdentifier::new(
 472            lsp::Uri::from_file_path("/the-root/test2.rs")
 473                .unwrap()
 474                .into(),
 475            1
 476        )
 477    );
 478
 479    // Save notifications are reported to all servers.
 480    project
 481        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
 482        .await
 483        .unwrap();
 484    assert_eq!(
 485        fake_rust_server
 486            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 487            .await
 488            .text_document,
 489        lsp::TextDocumentIdentifier::new(
 490            lsp::Uri::from_file_path("/the-root/Cargo.toml")
 491                .unwrap()
 492                .into()
 493        )
 494    );
 495    assert_eq!(
 496        fake_json_server
 497            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 498            .await
 499            .text_document,
 500        lsp::TextDocumentIdentifier::new(
 501            lsp::Uri::from_file_path("/the-root/Cargo.toml")
 502                .unwrap()
 503                .into()
 504        )
 505    );
 506
 507    // Renames are reported only to servers matching the buffer's language.
 508    fs.rename(
 509        Path::new("/the-root/test2.rs"),
 510        Path::new("/the-root/test3.rs"),
 511        Default::default(),
 512    )
 513    .await
 514    .unwrap();
 515    assert_eq!(
 516        fake_rust_server
 517            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 518            .await
 519            .text_document,
 520        lsp::TextDocumentIdentifier::new(
 521            lsp::Uri::from_file_path("/the-root/test2.rs")
 522                .unwrap()
 523                .into()
 524        ),
 525    );
 526    assert_eq!(
 527        fake_rust_server
 528            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 529            .await
 530            .text_document,
 531        lsp::TextDocumentItem {
 532            uri: lsp::Uri::from_file_path("/the-root/test3.rs")
 533                .unwrap()
 534                .into(),
 535            version: 0,
 536            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 537            language_id: "rust".to_string(),
 538        },
 539    );
 540
 541    rust_buffer2.update(cx, |buffer, cx| {
 542        buffer.update_diagnostics(
 543            LanguageServerId(0),
 544            DiagnosticSet::from_sorted_entries(
 545                vec![DiagnosticEntry {
 546                    diagnostic: Default::default(),
 547                    range: Anchor::MIN..Anchor::MAX,
 548                }],
 549                &buffer.snapshot(),
 550            ),
 551            cx,
 552        );
 553        assert_eq!(
 554            buffer
 555                .snapshot()
 556                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 557                .count(),
 558            1
 559        );
 560    });
 561
 562    // When the rename changes the extension of the file, the buffer gets closed on the old
 563    // language server and gets opened on the new one.
 564    fs.rename(
 565        Path::new("/the-root/test3.rs"),
 566        Path::new("/the-root/test3.json"),
 567        Default::default(),
 568    )
 569    .await
 570    .unwrap();
 571    assert_eq!(
 572        fake_rust_server
 573            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 574            .await
 575            .text_document,
 576        lsp::TextDocumentIdentifier::new(
 577            lsp::Uri::from_file_path("/the-root/test3.rs")
 578                .unwrap()
 579                .into(),
 580        ),
 581    );
 582    assert_eq!(
 583        fake_json_server
 584            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 585            .await
 586            .text_document,
 587        lsp::TextDocumentItem {
 588            uri: lsp::Uri::from_file_path("/the-root/test3.json")
 589                .unwrap()
 590                .into(),
 591            version: 0,
 592            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 593            language_id: "json".to_string(),
 594        },
 595    );
 596
 597    // We clear the diagnostics, since the language has changed.
 598    rust_buffer2.update(cx, |buffer, _| {
 599        assert_eq!(
 600            buffer
 601                .snapshot()
 602                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 603                .count(),
 604            0
 605        );
 606    });
 607
 608    // The renamed file's version resets after changing language server.
 609    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
 610    assert_eq!(
 611        fake_json_server
 612            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 613            .await
 614            .text_document,
 615        lsp::VersionedTextDocumentIdentifier::new(
 616            lsp::Uri::from_file_path("/the-root/test3.json")
 617                .unwrap()
 618                .into(),
 619            1
 620        )
 621    );
 622
 623    // Restart language servers
 624    project.update(cx, |project, cx| {
 625        project.restart_language_servers_for_buffers(
 626            vec![rust_buffer.clone(), json_buffer.clone()],
 627            cx,
 628        );
 629    });
 630
 631    let mut rust_shutdown_requests = fake_rust_server
 632        .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 633    let mut json_shutdown_requests = fake_json_server
 634        .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 635    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
 636
 637    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 638    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 639
 640    // Ensure rust document is reopened in new rust language server
 641    assert_eq!(
 642        fake_rust_server
 643            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 644            .await
 645            .text_document,
 646        lsp::TextDocumentItem {
 647            uri: lsp::Uri::from_file_path("/the-root/test.rs")
 648                .unwrap()
 649                .into(),
 650            version: 0,
 651            text: rust_buffer.update(cx, |buffer, _| buffer.text()),
 652            language_id: "rust".to_string(),
 653        }
 654    );
 655
 656    // Ensure json documents are reopened in new json language server
 657    assert_set_eq!(
 658        [
 659            fake_json_server
 660                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 661                .await
 662                .text_document,
 663            fake_json_server
 664                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 665                .await
 666                .text_document,
 667        ],
 668        [
 669            lsp::TextDocumentItem {
 670                uri: lsp::Uri::from_file_path("/the-root/package.json")
 671                    .unwrap()
 672                    .into(),
 673                version: 0,
 674                text: json_buffer.update(cx, |buffer, _| buffer.text()),
 675                language_id: "json".to_string(),
 676            },
 677            lsp::TextDocumentItem {
 678                uri: lsp::Uri::from_file_path("/the-root/test3.json")
 679                    .unwrap()
 680                    .into(),
 681                version: 0,
 682                text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 683                language_id: "json".to_string(),
 684            }
 685        ]
 686    );
 687
 688    // Close notifications are reported only to servers matching the buffer's language.
 689    cx.update(|_| drop(json_buffer));
 690    let close_message = lsp::DidCloseTextDocumentParams {
 691        text_document: lsp::TextDocumentIdentifier::new(
 692            lsp::Uri::from_file_path("/the-root/package.json")
 693                .unwrap()
 694                .into(),
 695        ),
 696    };
 697    assert_eq!(
 698        fake_json_server
 699            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 700            .await,
 701        close_message,
 702    );
 703}
 704
 705#[gpui::test]
 706async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
 707    init_test(cx);
 708
 709    let fs = FakeFs::new(cx.executor());
 710    fs.insert_tree(
 711        "/the-root",
 712        json!({
 713            ".gitignore": "target\n",
 714            "src": {
 715                "a.rs": "",
 716                "b.rs": "",
 717            },
 718            "target": {
 719                "x": {
 720                    "out": {
 721                        "x.rs": ""
 722                    }
 723                },
 724                "y": {
 725                    "out": {
 726                        "y.rs": "",
 727                    }
 728                },
 729                "z": {
 730                    "out": {
 731                        "z.rs": ""
 732                    }
 733                }
 734            }
 735        }),
 736    )
 737    .await;
 738
 739    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
 740    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 741    language_registry.add(rust_lang());
 742    let mut fake_servers = language_registry.register_fake_lsp_adapter(
 743        "Rust",
 744        FakeLspAdapter {
 745            name: "the-language-server",
 746            ..Default::default()
 747        },
 748    );
 749
 750    cx.executor().run_until_parked();
 751
 752    // Start the language server by opening a buffer with a compatible file extension.
 753    let _buffer = project
 754        .update(cx, |project, cx| {
 755            project.open_local_buffer("/the-root/src/a.rs", cx)
 756        })
 757        .await
 758        .unwrap();
 759
 760    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
 761    project.update(cx, |project, cx| {
 762        let worktree = project.worktrees().next().unwrap();
 763        assert_eq!(
 764            worktree
 765                .read(cx)
 766                .snapshot()
 767                .entries(true, 0)
 768                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 769                .collect::<Vec<_>>(),
 770            &[
 771                (Path::new(""), false),
 772                (Path::new(".gitignore"), false),
 773                (Path::new("src"), false),
 774                (Path::new("src/a.rs"), false),
 775                (Path::new("src/b.rs"), false),
 776                (Path::new("target"), true),
 777            ]
 778        );
 779    });
 780
 781    let prev_read_dir_count = fs.read_dir_call_count();
 782
 783    // Keep track of the FS events reported to the language server.
 784    let fake_server = fake_servers.next().await.unwrap();
 785    let file_changes = Arc::new(Mutex::new(Vec::new()));
 786    fake_server
 787        .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
 788            registrations: vec![lsp::Registration {
 789                id: Default::default(),
 790                method: "workspace/didChangeWatchedFiles".to_string(),
 791                register_options: serde_json::to_value(
 792                    lsp::DidChangeWatchedFilesRegistrationOptions {
 793                        watchers: vec![
 794                            lsp::FileSystemWatcher {
 795                                glob_pattern: lsp::GlobPattern::String(
 796                                    "/the-root/Cargo.toml".to_string(),
 797                                ),
 798                                kind: None,
 799                            },
 800                            lsp::FileSystemWatcher {
 801                                glob_pattern: lsp::GlobPattern::String(
 802                                    "/the-root/src/*.{rs,c}".to_string(),
 803                                ),
 804                                kind: None,
 805                            },
 806                            lsp::FileSystemWatcher {
 807                                glob_pattern: lsp::GlobPattern::String(
 808                                    "/the-root/target/y/**/*.rs".to_string(),
 809                                ),
 810                                kind: None,
 811                            },
 812                        ],
 813                    },
 814                )
 815                .ok(),
 816            }],
 817        })
 818        .await
 819        .unwrap();
 820    fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
 821        let file_changes = file_changes.clone();
 822        move |params, _| {
 823            let mut file_changes = file_changes.lock();
 824            file_changes.extend(params.changes);
 825            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
 826        }
 827    });
 828
 829    cx.executor().run_until_parked();
 830    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
 831    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
 832
 833    // Now the language server has asked us to watch an ignored directory path,
 834    // so we recursively load it.
 835    project.update(cx, |project, cx| {
 836        let worktree = project.worktrees().next().unwrap();
 837        assert_eq!(
 838            worktree
 839                .read(cx)
 840                .snapshot()
 841                .entries(true, 0)
 842                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 843                .collect::<Vec<_>>(),
 844            &[
 845                (Path::new(""), false),
 846                (Path::new(".gitignore"), false),
 847                (Path::new("src"), false),
 848                (Path::new("src/a.rs"), false),
 849                (Path::new("src/b.rs"), false),
 850                (Path::new("target"), true),
 851                (Path::new("target/x"), true),
 852                (Path::new("target/y"), true),
 853                (Path::new("target/y/out"), true),
 854                (Path::new("target/y/out/y.rs"), true),
 855                (Path::new("target/z"), true),
 856            ]
 857        );
 858    });
 859
 860    // Perform some file system mutations, two of which match the watched patterns,
 861    // and one of which does not.
 862    fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
 863        .await
 864        .unwrap();
 865    fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
 866        .await
 867        .unwrap();
 868    fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
 869        .await
 870        .unwrap();
 871    fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
 872        .await
 873        .unwrap();
 874    fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
 875        .await
 876        .unwrap();
 877
 878    // The language server receives events for the FS mutations that match its watch patterns.
 879    cx.executor().run_until_parked();
 880    assert_eq!(
 881        &*file_changes.lock(),
 882        &[
 883            lsp::FileEvent {
 884                uri: lsp::Uri::from_file_path("/the-root/src/b.rs")
 885                    .unwrap()
 886                    .into(),
 887                typ: lsp::FileChangeType::DELETED,
 888            },
 889            lsp::FileEvent {
 890                uri: lsp::Uri::from_file_path("/the-root/src/c.rs")
 891                    .unwrap()
 892                    .into(),
 893                typ: lsp::FileChangeType::CREATED,
 894            },
 895            lsp::FileEvent {
 896                uri: lsp::Uri::from_file_path("/the-root/target/y/out/y2.rs")
 897                    .unwrap()
 898                    .into(),
 899                typ: lsp::FileChangeType::CREATED,
 900            },
 901        ]
 902    );
 903}
 904
 905#[gpui::test]
 906async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 907    init_test(cx);
 908
 909    let fs = FakeFs::new(cx.executor());
 910    fs.insert_tree(
 911        "/dir",
 912        json!({
 913            "a.rs": "let a = 1;",
 914            "b.rs": "let b = 2;"
 915        }),
 916    )
 917    .await;
 918
 919    let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
 920
 921    let buffer_a = project
 922        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 923        .await
 924        .unwrap();
 925    let buffer_b = project
 926        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
 927        .await
 928        .unwrap();
 929
 930    project.update(cx, |project, cx| {
 931        project
 932            .update_diagnostics(
 933                LanguageServerId(0),
 934                lsp::PublishDiagnosticsParams {
 935                    uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
 936                    version: None,
 937                    diagnostics: vec![lsp::Diagnostic {
 938                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 939                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 940                        message: "error 1".to_string(),
 941                        ..Default::default()
 942                    }],
 943                },
 944                &[],
 945                cx,
 946            )
 947            .unwrap();
 948        project
 949            .update_diagnostics(
 950                LanguageServerId(0),
 951                lsp::PublishDiagnosticsParams {
 952                    uri: Uri::from_file_path("/dir/b.rs").unwrap().into(),
 953                    version: None,
 954                    diagnostics: vec![lsp::Diagnostic {
 955                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 956                        severity: Some(lsp::DiagnosticSeverity::WARNING),
 957                        message: "error 2".to_string(),
 958                        ..Default::default()
 959                    }],
 960                },
 961                &[],
 962                cx,
 963            )
 964            .unwrap();
 965    });
 966
 967    buffer_a.update(cx, |buffer, _| {
 968        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 969        assert_eq!(
 970            chunks
 971                .iter()
 972                .map(|(s, d)| (s.as_str(), *d))
 973                .collect::<Vec<_>>(),
 974            &[
 975                ("let ", None),
 976                ("a", Some(DiagnosticSeverity::ERROR)),
 977                (" = 1;", None),
 978            ]
 979        );
 980    });
 981    buffer_b.update(cx, |buffer, _| {
 982        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 983        assert_eq!(
 984            chunks
 985                .iter()
 986                .map(|(s, d)| (s.as_str(), *d))
 987                .collect::<Vec<_>>(),
 988            &[
 989                ("let ", None),
 990                ("b", Some(DiagnosticSeverity::WARNING)),
 991                (" = 2;", None),
 992            ]
 993        );
 994    });
 995}
 996
 997#[gpui::test]
 998async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
 999    init_test(cx);
1000
1001    let fs = FakeFs::new(cx.executor());
1002    fs.insert_tree(
1003        "/root",
1004        json!({
1005            "dir": {
1006                ".git": {
1007                    "HEAD": "ref: refs/heads/main",
1008                },
1009                ".gitignore": "b.rs",
1010                "a.rs": "let a = 1;",
1011                "b.rs": "let b = 2;",
1012            },
1013            "other.rs": "let b = c;"
1014        }),
1015    )
1016    .await;
1017
1018    let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
1019    let (worktree, _) = project
1020        .update(cx, |project, cx| {
1021            project.find_or_create_local_worktree("/root/dir", true, cx)
1022        })
1023        .await
1024        .unwrap();
1025    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1026
1027    let (worktree, _) = project
1028        .update(cx, |project, cx| {
1029            project.find_or_create_local_worktree("/root/other.rs", false, cx)
1030        })
1031        .await
1032        .unwrap();
1033    let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
1034
1035    let server_id = LanguageServerId(0);
1036    project.update(cx, |project, cx| {
1037        project
1038            .update_diagnostics(
1039                server_id,
1040                lsp::PublishDiagnosticsParams {
1041                    uri: Uri::from_file_path("/root/dir/b.rs").unwrap().into(),
1042                    version: None,
1043                    diagnostics: vec![lsp::Diagnostic {
1044                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1045                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1046                        message: "unused variable 'b'".to_string(),
1047                        ..Default::default()
1048                    }],
1049                },
1050                &[],
1051                cx,
1052            )
1053            .unwrap();
1054        project
1055            .update_diagnostics(
1056                server_id,
1057                lsp::PublishDiagnosticsParams {
1058                    uri: Uri::from_file_path("/root/other.rs").unwrap().into(),
1059                    version: None,
1060                    diagnostics: vec![lsp::Diagnostic {
1061                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
1062                        severity: Some(lsp::DiagnosticSeverity::ERROR),
1063                        message: "unknown variable 'c'".to_string(),
1064                        ..Default::default()
1065                    }],
1066                },
1067                &[],
1068                cx,
1069            )
1070            .unwrap();
1071    });
1072
1073    let main_ignored_buffer = project
1074        .update(cx, |project, cx| {
1075            project.open_buffer((main_worktree_id, "b.rs"), cx)
1076        })
1077        .await
1078        .unwrap();
1079    main_ignored_buffer.update(cx, |buffer, _| {
1080        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1081        assert_eq!(
1082            chunks
1083                .iter()
1084                .map(|(s, d)| (s.as_str(), *d))
1085                .collect::<Vec<_>>(),
1086            &[
1087                ("let ", None),
1088                ("b", Some(DiagnosticSeverity::ERROR)),
1089                (" = 2;", None),
1090            ],
1091            "Gigitnored buffers should still get in-buffer diagnostics",
1092        );
1093    });
1094    let other_buffer = project
1095        .update(cx, |project, cx| {
1096            project.open_buffer((other_worktree_id, ""), cx)
1097        })
1098        .await
1099        .unwrap();
1100    other_buffer.update(cx, |buffer, _| {
1101        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1102        assert_eq!(
1103            chunks
1104                .iter()
1105                .map(|(s, d)| (s.as_str(), *d))
1106                .collect::<Vec<_>>(),
1107            &[
1108                ("let b = ", None),
1109                ("c", Some(DiagnosticSeverity::ERROR)),
1110                (";", None),
1111            ],
1112            "Buffers from hidden projects should still get in-buffer diagnostics"
1113        );
1114    });
1115
1116    project.update(cx, |project, cx| {
1117        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
1118        assert_eq!(
1119            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
1120            vec![(
1121                ProjectPath {
1122                    worktree_id: main_worktree_id,
1123                    path: Arc::from(Path::new("b.rs")),
1124                },
1125                server_id,
1126                DiagnosticSummary {
1127                    error_count: 1,
1128                    warning_count: 0,
1129                }
1130            )]
1131        );
1132        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
1133        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
1134    });
1135}
1136
1137#[gpui::test]
1138async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
1139    init_test(cx);
1140
1141    let progress_token = "the-progress-token";
1142
1143    let fs = FakeFs::new(cx.executor());
1144    fs.insert_tree(
1145        "/dir",
1146        json!({
1147            "a.rs": "fn a() { A }",
1148            "b.rs": "const y: i32 = 1",
1149        }),
1150    )
1151    .await;
1152
1153    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1154    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1155
1156    language_registry.add(rust_lang());
1157    let mut fake_servers = language_registry.register_fake_lsp_adapter(
1158        "Rust",
1159        FakeLspAdapter {
1160            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1161            disk_based_diagnostics_sources: vec!["disk".into()],
1162            ..Default::default()
1163        },
1164    );
1165
1166    let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
1167
1168    // Cause worktree to start the fake language server
1169    let _buffer = project
1170        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
1171        .await
1172        .unwrap();
1173
1174    let mut events = cx.events(&project);
1175
1176    let fake_server = fake_servers.next().await.unwrap();
1177    assert_eq!(
1178        events.next().await.unwrap(),
1179        Event::LanguageServerAdded(LanguageServerId(0)),
1180    );
1181
1182    fake_server
1183        .start_progress(format!("{}/0", progress_token))
1184        .await;
1185    assert_eq!(
1186        events.next().await.unwrap(),
1187        Event::DiskBasedDiagnosticsStarted {
1188            language_server_id: LanguageServerId(0),
1189        }
1190    );
1191
1192    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1193        uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1194        version: None,
1195        diagnostics: vec![lsp::Diagnostic {
1196            range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1197            severity: Some(lsp::DiagnosticSeverity::ERROR),
1198            message: "undefined variable 'A'".to_string(),
1199            ..Default::default()
1200        }],
1201    });
1202    assert_eq!(
1203        events.next().await.unwrap(),
1204        Event::DiagnosticsUpdated {
1205            language_server_id: LanguageServerId(0),
1206            path: (worktree_id, Path::new("a.rs")).into()
1207        }
1208    );
1209
1210    fake_server.end_progress(format!("{}/0", progress_token));
1211    assert_eq!(
1212        events.next().await.unwrap(),
1213        Event::DiskBasedDiagnosticsFinished {
1214            language_server_id: LanguageServerId(0)
1215        }
1216    );
1217
1218    let buffer = project
1219        .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
1220        .await
1221        .unwrap();
1222
1223    buffer.update(cx, |buffer, _| {
1224        let snapshot = buffer.snapshot();
1225        let diagnostics = snapshot
1226            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1227            .collect::<Vec<_>>();
1228        assert_eq!(
1229            diagnostics,
1230            &[DiagnosticEntry {
1231                range: Point::new(0, 9)..Point::new(0, 10),
1232                diagnostic: Diagnostic {
1233                    severity: lsp::DiagnosticSeverity::ERROR,
1234                    message: "undefined variable 'A'".to_string(),
1235                    group_id: 0,
1236                    is_primary: true,
1237                    ..Default::default()
1238                }
1239            }]
1240        )
1241    });
1242
1243    // Ensure publishing empty diagnostics twice only results in one update event.
1244    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1245        uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1246        version: None,
1247        diagnostics: Default::default(),
1248    });
1249    assert_eq!(
1250        events.next().await.unwrap(),
1251        Event::DiagnosticsUpdated {
1252            language_server_id: LanguageServerId(0),
1253            path: (worktree_id, Path::new("a.rs")).into()
1254        }
1255    );
1256
1257    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1258        uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1259        version: None,
1260        diagnostics: Default::default(),
1261    });
1262    cx.executor().run_until_parked();
1263    assert_eq!(futures::poll!(events.next()), Poll::Pending);
1264}
1265
1266#[gpui::test]
1267async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
1268    init_test(cx);
1269
1270    let progress_token = "the-progress-token";
1271
1272    let fs = FakeFs::new(cx.executor());
1273    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1274
1275    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1276
1277    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1278    language_registry.add(rust_lang());
1279    let mut fake_servers = language_registry.register_fake_lsp_adapter(
1280        "Rust",
1281        FakeLspAdapter {
1282            name: "the-language-server",
1283            disk_based_diagnostics_sources: vec!["disk".into()],
1284            disk_based_diagnostics_progress_token: Some(progress_token.into()),
1285            ..Default::default()
1286        },
1287    );
1288
1289    let buffer = project
1290        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1291        .await
1292        .unwrap();
1293
1294    // Simulate diagnostics starting to update.
1295    let fake_server = fake_servers.next().await.unwrap();
1296    fake_server.start_progress(progress_token).await;
1297
1298    // Restart the server before the diagnostics finish updating.
1299    project.update(cx, |project, cx| {
1300        project.restart_language_servers_for_buffers([buffer], cx);
1301    });
1302    let mut events = cx.events(&project);
1303
1304    // Simulate the newly started server sending more diagnostics.
1305    let fake_server = fake_servers.next().await.unwrap();
1306    assert_eq!(
1307        events.next().await.unwrap(),
1308        Event::LanguageServerAdded(LanguageServerId(1))
1309    );
1310    fake_server.start_progress(progress_token).await;
1311    assert_eq!(
1312        events.next().await.unwrap(),
1313        Event::DiskBasedDiagnosticsStarted {
1314            language_server_id: LanguageServerId(1)
1315        }
1316    );
1317    project.update(cx, |project, _| {
1318        assert_eq!(
1319            project
1320                .language_servers_running_disk_based_diagnostics()
1321                .collect::<Vec<_>>(),
1322            [LanguageServerId(1)]
1323        );
1324    });
1325
1326    // All diagnostics are considered done, despite the old server's diagnostic
1327    // task never completing.
1328    fake_server.end_progress(progress_token);
1329    assert_eq!(
1330        events.next().await.unwrap(),
1331        Event::DiskBasedDiagnosticsFinished {
1332            language_server_id: LanguageServerId(1)
1333        }
1334    );
1335    project.update(cx, |project, _| {
1336        assert_eq!(
1337            project
1338                .language_servers_running_disk_based_diagnostics()
1339                .collect::<Vec<_>>(),
1340            [] as [language::LanguageServerId; 0]
1341        );
1342    });
1343}
1344
1345#[gpui::test]
1346async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
1347    init_test(cx);
1348
1349    let fs = FakeFs::new(cx.executor());
1350    fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
1351
1352    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1353
1354    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1355    language_registry.add(rust_lang());
1356    let mut fake_servers =
1357        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
1358
1359    let buffer = project
1360        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1361        .await
1362        .unwrap();
1363
1364    // Publish diagnostics
1365    let fake_server = fake_servers.next().await.unwrap();
1366    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1367        uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1368        version: None,
1369        diagnostics: vec![lsp::Diagnostic {
1370            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1371            severity: Some(lsp::DiagnosticSeverity::ERROR),
1372            message: "the message".to_string(),
1373            ..Default::default()
1374        }],
1375    });
1376
1377    cx.executor().run_until_parked();
1378    buffer.update(cx, |buffer, _| {
1379        assert_eq!(
1380            buffer
1381                .snapshot()
1382                .diagnostics_in_range::<_, usize>(0..1, false)
1383                .map(|entry| entry.diagnostic.message.clone())
1384                .collect::<Vec<_>>(),
1385            ["the message".to_string()]
1386        );
1387    });
1388    project.update(cx, |project, cx| {
1389        assert_eq!(
1390            project.diagnostic_summary(false, cx),
1391            DiagnosticSummary {
1392                error_count: 1,
1393                warning_count: 0,
1394            }
1395        );
1396    });
1397
1398    project.update(cx, |project, cx| {
1399        project.restart_language_servers_for_buffers([buffer.clone()], cx);
1400    });
1401
1402    // The diagnostics are cleared.
1403    cx.executor().run_until_parked();
1404    buffer.update(cx, |buffer, _| {
1405        assert_eq!(
1406            buffer
1407                .snapshot()
1408                .diagnostics_in_range::<_, usize>(0..1, false)
1409                .map(|entry| entry.diagnostic.message.clone())
1410                .collect::<Vec<_>>(),
1411            Vec::<String>::new(),
1412        );
1413    });
1414    project.update(cx, |project, cx| {
1415        assert_eq!(
1416            project.diagnostic_summary(false, cx),
1417            DiagnosticSummary {
1418                error_count: 0,
1419                warning_count: 0,
1420            }
1421        );
1422    });
1423}
1424
1425#[gpui::test]
1426async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
1427    init_test(cx);
1428
1429    let fs = FakeFs::new(cx.executor());
1430    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1431
1432    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1433    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1434
1435    language_registry.add(rust_lang());
1436    let mut fake_servers =
1437        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
1438
1439    let buffer = project
1440        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1441        .await
1442        .unwrap();
1443
1444    // Before restarting the server, report diagnostics with an unknown buffer version.
1445    let fake_server = fake_servers.next().await.unwrap();
1446    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1447        uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1448        version: Some(10000),
1449        diagnostics: Vec::new(),
1450    });
1451    cx.executor().run_until_parked();
1452
1453    project.update(cx, |project, cx| {
1454        project.restart_language_servers_for_buffers([buffer.clone()], cx);
1455    });
1456    let mut fake_server = fake_servers.next().await.unwrap();
1457    let notification = fake_server
1458        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1459        .await
1460        .text_document;
1461    assert_eq!(notification.version, 0);
1462}
1463
1464#[gpui::test]
1465async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
1466    init_test(cx);
1467
1468    let fs = FakeFs::new(cx.executor());
1469    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
1470        .await;
1471
1472    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1473    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1474
1475    let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
1476        "Rust",
1477        FakeLspAdapter {
1478            name: "rust-lsp",
1479            ..Default::default()
1480        },
1481    );
1482    let mut fake_js_servers = language_registry.register_fake_lsp_adapter(
1483        "JavaScript",
1484        FakeLspAdapter {
1485            name: "js-lsp",
1486            ..Default::default()
1487        },
1488    );
1489    language_registry.add(rust_lang());
1490    language_registry.add(js_lang());
1491
1492    let _rs_buffer = project
1493        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1494        .await
1495        .unwrap();
1496    let _js_buffer = project
1497        .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1498        .await
1499        .unwrap();
1500
1501    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1502    assert_eq!(
1503        fake_rust_server_1
1504            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1505            .await
1506            .text_document
1507            .uri
1508            .as_str(),
1509        "file:///dir/a.rs"
1510    );
1511
1512    let mut fake_js_server = fake_js_servers.next().await.unwrap();
1513    assert_eq!(
1514        fake_js_server
1515            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1516            .await
1517            .text_document
1518            .uri
1519            .as_str(),
1520        "file:///dir/b.js"
1521    );
1522
1523    // Disable Rust language server, ensuring only that server gets stopped.
1524    cx.update(|cx| {
1525        SettingsStore::update_global(cx, |settings, cx| {
1526            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1527                settings.languages.insert(
1528                    Arc::from("Rust"),
1529                    LanguageSettingsContent {
1530                        enable_language_server: Some(false),
1531                        ..Default::default()
1532                    },
1533                );
1534            });
1535        })
1536    });
1537    fake_rust_server_1
1538        .receive_notification::<lsp::notification::Exit>()
1539        .await;
1540
1541    // Enable Rust and disable JavaScript language servers, ensuring that the
1542    // former gets started again and that the latter stops.
1543    cx.update(|cx| {
1544        SettingsStore::update_global(cx, |settings, cx| {
1545            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1546                settings.languages.insert(
1547                    Arc::from("Rust"),
1548                    LanguageSettingsContent {
1549                        enable_language_server: Some(true),
1550                        ..Default::default()
1551                    },
1552                );
1553                settings.languages.insert(
1554                    Arc::from("JavaScript"),
1555                    LanguageSettingsContent {
1556                        enable_language_server: Some(false),
1557                        ..Default::default()
1558                    },
1559                );
1560            });
1561        })
1562    });
1563    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1564    assert_eq!(
1565        fake_rust_server_2
1566            .receive_notification::<lsp::notification::DidOpenTextDocument>()
1567            .await
1568            .text_document
1569            .uri
1570            .as_str(),
1571        "file:///dir/a.rs"
1572    );
1573    fake_js_server
1574        .receive_notification::<lsp::notification::Exit>()
1575        .await;
1576}
1577
1578#[gpui::test(iterations = 3)]
1579async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
1580    init_test(cx);
1581
1582    let text = "
1583        fn a() { A }
1584        fn b() { BB }
1585        fn c() { CCC }
1586    "
1587    .unindent();
1588
1589    let fs = FakeFs::new(cx.executor());
1590    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1591
1592    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1593    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1594
1595    language_registry.add(rust_lang());
1596    let mut fake_servers = language_registry.register_fake_lsp_adapter(
1597        "Rust",
1598        FakeLspAdapter {
1599            disk_based_diagnostics_sources: vec!["disk".into()],
1600            ..Default::default()
1601        },
1602    );
1603
1604    let buffer = project
1605        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1606        .await
1607        .unwrap();
1608
1609    let mut fake_server = fake_servers.next().await.unwrap();
1610    let open_notification = fake_server
1611        .receive_notification::<lsp::notification::DidOpenTextDocument>()
1612        .await;
1613
1614    // Edit the buffer, moving the content down
1615    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1616    let change_notification_1 = fake_server
1617        .receive_notification::<lsp::notification::DidChangeTextDocument>()
1618        .await;
1619    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1620
1621    // Report some diagnostics for the initial version of the buffer
1622    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1623        uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1624        version: Some(open_notification.text_document.version),
1625        diagnostics: vec![
1626            lsp::Diagnostic {
1627                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1628                severity: Some(DiagnosticSeverity::ERROR),
1629                message: "undefined variable 'A'".to_string(),
1630                source: Some("disk".to_string()),
1631                ..Default::default()
1632            },
1633            lsp::Diagnostic {
1634                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1635                severity: Some(DiagnosticSeverity::ERROR),
1636                message: "undefined variable 'BB'".to_string(),
1637                source: Some("disk".to_string()),
1638                ..Default::default()
1639            },
1640            lsp::Diagnostic {
1641                range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
1642                severity: Some(DiagnosticSeverity::ERROR),
1643                source: Some("disk".to_string()),
1644                message: "undefined variable 'CCC'".to_string(),
1645                ..Default::default()
1646            },
1647        ],
1648    });
1649
1650    // The diagnostics have moved down since they were created.
1651    cx.executor().run_until_parked();
1652    buffer.update(cx, |buffer, _| {
1653        assert_eq!(
1654            buffer
1655                .snapshot()
1656                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1657                .collect::<Vec<_>>(),
1658            &[
1659                DiagnosticEntry {
1660                    range: Point::new(3, 9)..Point::new(3, 11),
1661                    diagnostic: Diagnostic {
1662                        source: Some("disk".into()),
1663                        severity: DiagnosticSeverity::ERROR,
1664                        message: "undefined variable 'BB'".to_string(),
1665                        is_disk_based: true,
1666                        group_id: 1,
1667                        is_primary: true,
1668                        ..Default::default()
1669                    },
1670                },
1671                DiagnosticEntry {
1672                    range: Point::new(4, 9)..Point::new(4, 12),
1673                    diagnostic: Diagnostic {
1674                        source: Some("disk".into()),
1675                        severity: DiagnosticSeverity::ERROR,
1676                        message: "undefined variable 'CCC'".to_string(),
1677                        is_disk_based: true,
1678                        group_id: 2,
1679                        is_primary: true,
1680                        ..Default::default()
1681                    }
1682                }
1683            ]
1684        );
1685        assert_eq!(
1686            chunks_with_diagnostics(buffer, 0..buffer.len()),
1687            [
1688                ("\n\nfn a() { ".to_string(), None),
1689                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1690                (" }\nfn b() { ".to_string(), None),
1691                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1692                (" }\nfn c() { ".to_string(), None),
1693                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1694                (" }\n".to_string(), None),
1695            ]
1696        );
1697        assert_eq!(
1698            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1699            [
1700                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1701                (" }\nfn c() { ".to_string(), None),
1702                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1703            ]
1704        );
1705    });
1706
1707    // Ensure overlapping diagnostics are highlighted correctly.
1708    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1709        uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1710        version: Some(open_notification.text_document.version),
1711        diagnostics: vec![
1712            lsp::Diagnostic {
1713                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1714                severity: Some(DiagnosticSeverity::ERROR),
1715                message: "undefined variable 'A'".to_string(),
1716                source: Some("disk".to_string()),
1717                ..Default::default()
1718            },
1719            lsp::Diagnostic {
1720                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1721                severity: Some(DiagnosticSeverity::WARNING),
1722                message: "unreachable statement".to_string(),
1723                source: Some("disk".to_string()),
1724                ..Default::default()
1725            },
1726        ],
1727    });
1728
1729    cx.executor().run_until_parked();
1730    buffer.update(cx, |buffer, _| {
1731        assert_eq!(
1732            buffer
1733                .snapshot()
1734                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1735                .collect::<Vec<_>>(),
1736            &[
1737                DiagnosticEntry {
1738                    range: Point::new(2, 9)..Point::new(2, 12),
1739                    diagnostic: Diagnostic {
1740                        source: Some("disk".into()),
1741                        severity: DiagnosticSeverity::WARNING,
1742                        message: "unreachable statement".to_string(),
1743                        is_disk_based: true,
1744                        group_id: 4,
1745                        is_primary: true,
1746                        ..Default::default()
1747                    }
1748                },
1749                DiagnosticEntry {
1750                    range: Point::new(2, 9)..Point::new(2, 10),
1751                    diagnostic: Diagnostic {
1752                        source: Some("disk".into()),
1753                        severity: DiagnosticSeverity::ERROR,
1754                        message: "undefined variable 'A'".to_string(),
1755                        is_disk_based: true,
1756                        group_id: 3,
1757                        is_primary: true,
1758                        ..Default::default()
1759                    },
1760                }
1761            ]
1762        );
1763        assert_eq!(
1764            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1765            [
1766                ("fn a() { ".to_string(), None),
1767                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1768                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1769                ("\n".to_string(), None),
1770            ]
1771        );
1772        assert_eq!(
1773            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1774            [
1775                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1776                ("\n".to_string(), None),
1777            ]
1778        );
1779    });
1780
1781    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1782    // changes since the last save.
1783    buffer.update(cx, |buffer, cx| {
1784        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
1785        buffer.edit(
1786            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1787            None,
1788            cx,
1789        );
1790        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1791    });
1792    let change_notification_2 = fake_server
1793        .receive_notification::<lsp::notification::DidChangeTextDocument>()
1794        .await;
1795    assert!(
1796        change_notification_2.text_document.version > change_notification_1.text_document.version
1797    );
1798
1799    // Handle out-of-order diagnostics
1800    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1801        uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1802        version: Some(change_notification_2.text_document.version),
1803        diagnostics: vec![
1804            lsp::Diagnostic {
1805                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1806                severity: Some(DiagnosticSeverity::ERROR),
1807                message: "undefined variable 'BB'".to_string(),
1808                source: Some("disk".to_string()),
1809                ..Default::default()
1810            },
1811            lsp::Diagnostic {
1812                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1813                severity: Some(DiagnosticSeverity::WARNING),
1814                message: "undefined variable 'A'".to_string(),
1815                source: Some("disk".to_string()),
1816                ..Default::default()
1817            },
1818        ],
1819    });
1820
1821    cx.executor().run_until_parked();
1822    buffer.update(cx, |buffer, _| {
1823        assert_eq!(
1824            buffer
1825                .snapshot()
1826                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1827                .collect::<Vec<_>>(),
1828            &[
1829                DiagnosticEntry {
1830                    range: Point::new(2, 21)..Point::new(2, 22),
1831                    diagnostic: Diagnostic {
1832                        source: Some("disk".into()),
1833                        severity: DiagnosticSeverity::WARNING,
1834                        message: "undefined variable 'A'".to_string(),
1835                        is_disk_based: true,
1836                        group_id: 6,
1837                        is_primary: true,
1838                        ..Default::default()
1839                    }
1840                },
1841                DiagnosticEntry {
1842                    range: Point::new(3, 9)..Point::new(3, 14),
1843                    diagnostic: Diagnostic {
1844                        source: Some("disk".into()),
1845                        severity: DiagnosticSeverity::ERROR,
1846                        message: "undefined variable 'BB'".to_string(),
1847                        is_disk_based: true,
1848                        group_id: 5,
1849                        is_primary: true,
1850                        ..Default::default()
1851                    },
1852                }
1853            ]
1854        );
1855    });
1856}
1857
1858#[gpui::test]
1859async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1860    init_test(cx);
1861
1862    let text = concat!(
1863        "let one = ;\n", //
1864        "let two = \n",
1865        "let three = 3;\n",
1866    );
1867
1868    let fs = FakeFs::new(cx.executor());
1869    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1870
1871    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1872    let buffer = project
1873        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1874        .await
1875        .unwrap();
1876
1877    project.update(cx, |project, cx| {
1878        project
1879            .update_buffer_diagnostics(
1880                &buffer,
1881                LanguageServerId(0),
1882                None,
1883                vec![
1884                    DiagnosticEntry {
1885                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1886                        diagnostic: Diagnostic {
1887                            severity: DiagnosticSeverity::ERROR,
1888                            message: "syntax error 1".to_string(),
1889                            ..Default::default()
1890                        },
1891                    },
1892                    DiagnosticEntry {
1893                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1894                        diagnostic: Diagnostic {
1895                            severity: DiagnosticSeverity::ERROR,
1896                            message: "syntax error 2".to_string(),
1897                            ..Default::default()
1898                        },
1899                    },
1900                ],
1901                cx,
1902            )
1903            .unwrap();
1904    });
1905
1906    // An empty range is extended forward to include the following character.
1907    // At the end of a line, an empty range is extended backward to include
1908    // the preceding character.
1909    buffer.update(cx, |buffer, _| {
1910        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1911        assert_eq!(
1912            chunks
1913                .iter()
1914                .map(|(s, d)| (s.as_str(), *d))
1915                .collect::<Vec<_>>(),
1916            &[
1917                ("let one = ", None),
1918                (";", Some(DiagnosticSeverity::ERROR)),
1919                ("\nlet two =", None),
1920                (" ", Some(DiagnosticSeverity::ERROR)),
1921                ("\nlet three = 3;\n", None)
1922            ]
1923        );
1924    });
1925}
1926
1927#[gpui::test]
1928async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
1929    init_test(cx);
1930
1931    let fs = FakeFs::new(cx.executor());
1932    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1933        .await;
1934
1935    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1936
1937    project.update(cx, |project, cx| {
1938        project
1939            .update_diagnostic_entries(
1940                LanguageServerId(0),
1941                Path::new("/dir/a.rs").to_owned(),
1942                None,
1943                vec![DiagnosticEntry {
1944                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1945                    diagnostic: Diagnostic {
1946                        severity: DiagnosticSeverity::ERROR,
1947                        is_primary: true,
1948                        message: "syntax error a1".to_string(),
1949                        ..Default::default()
1950                    },
1951                }],
1952                cx,
1953            )
1954            .unwrap();
1955        project
1956            .update_diagnostic_entries(
1957                LanguageServerId(1),
1958                Path::new("/dir/a.rs").to_owned(),
1959                None,
1960                vec![DiagnosticEntry {
1961                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1962                    diagnostic: Diagnostic {
1963                        severity: DiagnosticSeverity::ERROR,
1964                        is_primary: true,
1965                        message: "syntax error b1".to_string(),
1966                        ..Default::default()
1967                    },
1968                }],
1969                cx,
1970            )
1971            .unwrap();
1972
1973        assert_eq!(
1974            project.diagnostic_summary(false, cx),
1975            DiagnosticSummary {
1976                error_count: 2,
1977                warning_count: 0,
1978            }
1979        );
1980    });
1981}
1982
1983#[gpui::test]
1984async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
1985    init_test(cx);
1986
1987    let text = "
1988        fn a() {
1989            f1();
1990        }
1991        fn b() {
1992            f2();
1993        }
1994        fn c() {
1995            f3();
1996        }
1997    "
1998    .unindent();
1999
2000    let fs = FakeFs::new(cx.executor());
2001    fs.insert_tree(
2002        "/dir",
2003        json!({
2004            "a.rs": text.clone(),
2005        }),
2006    )
2007    .await;
2008
2009    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2010
2011    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2012    language_registry.add(rust_lang());
2013    let mut fake_servers =
2014        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
2015
2016    let buffer = project
2017        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2018        .await
2019        .unwrap();
2020
2021    let mut fake_server = fake_servers.next().await.unwrap();
2022    let lsp_document_version = fake_server
2023        .receive_notification::<lsp::notification::DidOpenTextDocument>()
2024        .await
2025        .text_document
2026        .version;
2027
2028    // Simulate editing the buffer after the language server computes some edits.
2029    buffer.update(cx, |buffer, cx| {
2030        buffer.edit(
2031            [(
2032                Point::new(0, 0)..Point::new(0, 0),
2033                "// above first function\n",
2034            )],
2035            None,
2036            cx,
2037        );
2038        buffer.edit(
2039            [(
2040                Point::new(2, 0)..Point::new(2, 0),
2041                "    // inside first function\n",
2042            )],
2043            None,
2044            cx,
2045        );
2046        buffer.edit(
2047            [(
2048                Point::new(6, 4)..Point::new(6, 4),
2049                "// inside second function ",
2050            )],
2051            None,
2052            cx,
2053        );
2054
2055        assert_eq!(
2056            buffer.text(),
2057            "
2058                // above first function
2059                fn a() {
2060                    // inside first function
2061                    f1();
2062                }
2063                fn b() {
2064                    // inside second function f2();
2065                }
2066                fn c() {
2067                    f3();
2068                }
2069            "
2070            .unindent()
2071        );
2072    });
2073
2074    let edits = project
2075        .update(cx, |project, cx| {
2076            project.edits_from_lsp(
2077                &buffer,
2078                vec![
2079                    // replace body of first function
2080                    lsp::TextEdit {
2081                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
2082                        new_text: "
2083                            fn a() {
2084                                f10();
2085                            }
2086                            "
2087                        .unindent(),
2088                    },
2089                    // edit inside second function
2090                    lsp::TextEdit {
2091                        range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
2092                        new_text: "00".into(),
2093                    },
2094                    // edit inside third function via two distinct edits
2095                    lsp::TextEdit {
2096                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
2097                        new_text: "4000".into(),
2098                    },
2099                    lsp::TextEdit {
2100                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
2101                        new_text: "".into(),
2102                    },
2103                ],
2104                LanguageServerId(0),
2105                Some(lsp_document_version),
2106                cx,
2107            )
2108        })
2109        .await
2110        .unwrap();
2111
2112    buffer.update(cx, |buffer, cx| {
2113        for (range, new_text) in edits {
2114            buffer.edit([(range, new_text)], None, cx);
2115        }
2116        assert_eq!(
2117            buffer.text(),
2118            "
2119                // above first function
2120                fn a() {
2121                    // inside first function
2122                    f10();
2123                }
2124                fn b() {
2125                    // inside second function f200();
2126                }
2127                fn c() {
2128                    f4000();
2129                }
2130                "
2131            .unindent()
2132        );
2133    });
2134}
2135
2136#[gpui::test]
2137async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
2138    init_test(cx);
2139
2140    let text = "
2141        use a::b;
2142        use a::c;
2143
2144        fn f() {
2145            b();
2146            c();
2147        }
2148    "
2149    .unindent();
2150
2151    let fs = FakeFs::new(cx.executor());
2152    fs.insert_tree(
2153        "/dir",
2154        json!({
2155            "a.rs": text.clone(),
2156        }),
2157    )
2158    .await;
2159
2160    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2161    let buffer = project
2162        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2163        .await
2164        .unwrap();
2165
2166    // Simulate the language server sending us a small edit in the form of a very large diff.
2167    // Rust-analyzer does this when performing a merge-imports code action.
2168    let edits = project
2169        .update(cx, |project, cx| {
2170            project.edits_from_lsp(
2171                &buffer,
2172                [
2173                    // Replace the first use statement without editing the semicolon.
2174                    lsp::TextEdit {
2175                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
2176                        new_text: "a::{b, c}".into(),
2177                    },
2178                    // Reinsert the remainder of the file between the semicolon and the final
2179                    // newline of the file.
2180                    lsp::TextEdit {
2181                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2182                        new_text: "\n\n".into(),
2183                    },
2184                    lsp::TextEdit {
2185                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2186                        new_text: "
2187                            fn f() {
2188                                b();
2189                                c();
2190                            }"
2191                        .unindent(),
2192                    },
2193                    // Delete everything after the first newline of the file.
2194                    lsp::TextEdit {
2195                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
2196                        new_text: "".into(),
2197                    },
2198                ],
2199                LanguageServerId(0),
2200                None,
2201                cx,
2202            )
2203        })
2204        .await
2205        .unwrap();
2206
2207    buffer.update(cx, |buffer, cx| {
2208        let edits = edits
2209            .into_iter()
2210            .map(|(range, text)| {
2211                (
2212                    range.start.to_point(buffer)..range.end.to_point(buffer),
2213                    text,
2214                )
2215            })
2216            .collect::<Vec<_>>();
2217
2218        assert_eq!(
2219            edits,
2220            [
2221                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2222                (Point::new(1, 0)..Point::new(2, 0), "".into())
2223            ]
2224        );
2225
2226        for (range, new_text) in edits {
2227            buffer.edit([(range, new_text)], None, cx);
2228        }
2229        assert_eq!(
2230            buffer.text(),
2231            "
2232                use a::{b, c};
2233
2234                fn f() {
2235                    b();
2236                    c();
2237                }
2238            "
2239            .unindent()
2240        );
2241    });
2242}
2243
2244#[gpui::test]
2245async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
2246    init_test(cx);
2247
2248    let text = "
2249        use a::b;
2250        use a::c;
2251
2252        fn f() {
2253            b();
2254            c();
2255        }
2256    "
2257    .unindent();
2258
2259    let fs = FakeFs::new(cx.executor());
2260    fs.insert_tree(
2261        "/dir",
2262        json!({
2263            "a.rs": text.clone(),
2264        }),
2265    )
2266    .await;
2267
2268    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2269    let buffer = project
2270        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2271        .await
2272        .unwrap();
2273
2274    // Simulate the language server sending us edits in a non-ordered fashion,
2275    // with ranges sometimes being inverted or pointing to invalid locations.
2276    let edits = project
2277        .update(cx, |project, cx| {
2278            project.edits_from_lsp(
2279                &buffer,
2280                [
2281                    lsp::TextEdit {
2282                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2283                        new_text: "\n\n".into(),
2284                    },
2285                    lsp::TextEdit {
2286                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
2287                        new_text: "a::{b, c}".into(),
2288                    },
2289                    lsp::TextEdit {
2290                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
2291                        new_text: "".into(),
2292                    },
2293                    lsp::TextEdit {
2294                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2295                        new_text: "
2296                            fn f() {
2297                                b();
2298                                c();
2299                            }"
2300                        .unindent(),
2301                    },
2302                ],
2303                LanguageServerId(0),
2304                None,
2305                cx,
2306            )
2307        })
2308        .await
2309        .unwrap();
2310
2311    buffer.update(cx, |buffer, cx| {
2312        let edits = edits
2313            .into_iter()
2314            .map(|(range, text)| {
2315                (
2316                    range.start.to_point(buffer)..range.end.to_point(buffer),
2317                    text,
2318                )
2319            })
2320            .collect::<Vec<_>>();
2321
2322        assert_eq!(
2323            edits,
2324            [
2325                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2326                (Point::new(1, 0)..Point::new(2, 0), "".into())
2327            ]
2328        );
2329
2330        for (range, new_text) in edits {
2331            buffer.edit([(range, new_text)], None, cx);
2332        }
2333        assert_eq!(
2334            buffer.text(),
2335            "
2336                use a::{b, c};
2337
2338                fn f() {
2339                    b();
2340                    c();
2341                }
2342            "
2343            .unindent()
2344        );
2345    });
2346}
2347
2348fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2349    buffer: &Buffer,
2350    range: Range<T>,
2351) -> Vec<(String, Option<DiagnosticSeverity>)> {
2352    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2353    for chunk in buffer.snapshot().chunks(range, true) {
2354        if chunks.last().map_or(false, |prev_chunk| {
2355            prev_chunk.1 == chunk.diagnostic_severity
2356        }) {
2357            chunks.last_mut().unwrap().0.push_str(chunk.text);
2358        } else {
2359            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2360        }
2361    }
2362    chunks
2363}
2364
2365#[gpui::test(iterations = 10)]
2366async fn test_definition(cx: &mut gpui::TestAppContext) {
2367    init_test(cx);
2368
2369    let fs = FakeFs::new(cx.executor());
2370    fs.insert_tree(
2371        "/dir",
2372        json!({
2373            "a.rs": "const fn a() { A }",
2374            "b.rs": "const y: i32 = crate::a()",
2375        }),
2376    )
2377    .await;
2378
2379    let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2380
2381    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2382    language_registry.add(rust_lang());
2383    let mut fake_servers =
2384        language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
2385
2386    let buffer = project
2387        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2388        .await
2389        .unwrap();
2390
2391    let fake_server = fake_servers.next().await.unwrap();
2392    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2393        let params = params.text_document_position_params;
2394        assert_eq!(
2395            Uri::from(params.text_document.uri).to_file_path().unwrap(),
2396            Path::new("/dir/b.rs"),
2397        );
2398        assert_eq!(params.position, lsp::Position::new(0, 22));
2399
2400        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2401            lsp::Location::new(
2402                lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
2403                lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2404            ),
2405        )))
2406    });
2407
2408    let mut definitions = project
2409        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2410        .await
2411        .unwrap();
2412
2413    // Assert no new language server started
2414    cx.executor().run_until_parked();
2415    assert!(fake_servers.try_next().is_err());
2416
2417    assert_eq!(definitions.len(), 1);
2418    let definition = definitions.pop().unwrap();
2419    cx.update(|cx| {
2420        let target_buffer = definition.target.buffer.read(cx);
2421        assert_eq!(
2422            target_buffer
2423                .file()
2424                .unwrap()
2425                .as_local()
2426                .unwrap()
2427                .abs_path(cx),
2428            Path::new("/dir/a.rs"),
2429        );
2430        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2431        assert_eq!(
2432            list_worktrees(&project, cx),
2433            [("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
2434        );
2435
2436        drop(definition);
2437    });
2438    cx.update(|cx| {
2439        assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2440    });
2441
2442    fn list_worktrees<'a>(
2443        project: &'a Model<Project>,
2444        cx: &'a AppContext,
2445    ) -> Vec<(&'a Path, bool)> {
2446        project
2447            .read(cx)
2448            .worktrees()
2449            .map(|worktree| {
2450                let worktree = worktree.read(cx);
2451                (
2452                    worktree.as_local().unwrap().abs_path().as_ref(),
2453                    worktree.is_visible(),
2454                )
2455            })
2456            .collect::<Vec<_>>()
2457    }
2458}
2459
2460#[gpui::test]
2461async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
2462    init_test(cx);
2463
2464    let fs = FakeFs::new(cx.executor());
2465    fs.insert_tree(
2466        "/dir",
2467        json!({
2468            "a.ts": "",
2469        }),
2470    )
2471    .await;
2472
2473    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2474
2475    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2476    language_registry.add(typescript_lang());
2477    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2478        "TypeScript",
2479        FakeLspAdapter {
2480            capabilities: lsp::ServerCapabilities {
2481                completion_provider: Some(lsp::CompletionOptions {
2482                    trigger_characters: Some(vec![":".to_string()]),
2483                    ..Default::default()
2484                }),
2485                ..Default::default()
2486            },
2487            ..Default::default()
2488        },
2489    );
2490
2491    let buffer = project
2492        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2493        .await
2494        .unwrap();
2495
2496    let fake_server = fake_language_servers.next().await.unwrap();
2497
2498    let text = "let a = b.fqn";
2499    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2500    let completions = project.update(cx, |project, cx| {
2501        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
2502    });
2503
2504    fake_server
2505        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2506            Ok(Some(lsp::CompletionResponse::Array(vec![
2507                lsp::CompletionItem {
2508                    label: "fullyQualifiedName?".into(),
2509                    insert_text: Some("fullyQualifiedName".into()),
2510                    ..Default::default()
2511                },
2512            ])))
2513        })
2514        .next()
2515        .await;
2516    let completions = completions.await.unwrap();
2517    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2518    assert_eq!(completions.len(), 1);
2519    assert_eq!(completions[0].new_text, "fullyQualifiedName");
2520    assert_eq!(
2521        completions[0].old_range.to_offset(&snapshot),
2522        text.len() - 3..text.len()
2523    );
2524
2525    let text = "let a = \"atoms/cmp\"";
2526    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2527    let completions = project.update(cx, |project, cx| {
2528        project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
2529    });
2530
2531    fake_server
2532        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2533            Ok(Some(lsp::CompletionResponse::Array(vec![
2534                lsp::CompletionItem {
2535                    label: "component".into(),
2536                    ..Default::default()
2537                },
2538            ])))
2539        })
2540        .next()
2541        .await;
2542    let completions = completions.await.unwrap();
2543    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2544    assert_eq!(completions.len(), 1);
2545    assert_eq!(completions[0].new_text, "component");
2546    assert_eq!(
2547        completions[0].old_range.to_offset(&snapshot),
2548        text.len() - 4..text.len() - 1
2549    );
2550}
2551
2552#[gpui::test]
2553async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
2554    init_test(cx);
2555
2556    let fs = FakeFs::new(cx.executor());
2557    fs.insert_tree(
2558        "/dir",
2559        json!({
2560            "a.ts": "",
2561        }),
2562    )
2563    .await;
2564
2565    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2566
2567    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2568    language_registry.add(typescript_lang());
2569    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2570        "TypeScript",
2571        FakeLspAdapter {
2572            capabilities: lsp::ServerCapabilities {
2573                completion_provider: Some(lsp::CompletionOptions {
2574                    trigger_characters: Some(vec![":".to_string()]),
2575                    ..Default::default()
2576                }),
2577                ..Default::default()
2578            },
2579            ..Default::default()
2580        },
2581    );
2582
2583    let buffer = project
2584        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2585        .await
2586        .unwrap();
2587
2588    let fake_server = fake_language_servers.next().await.unwrap();
2589
2590    let text = "let a = b.fqn";
2591    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2592    let completions = project.update(cx, |project, cx| {
2593        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
2594    });
2595
2596    fake_server
2597        .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2598            Ok(Some(lsp::CompletionResponse::Array(vec![
2599                lsp::CompletionItem {
2600                    label: "fullyQualifiedName?".into(),
2601                    insert_text: Some("fully\rQualified\r\nName".into()),
2602                    ..Default::default()
2603                },
2604            ])))
2605        })
2606        .next()
2607        .await;
2608    let completions = completions.await.unwrap();
2609    assert_eq!(completions.len(), 1);
2610    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2611}
2612
2613#[gpui::test(iterations = 10)]
2614async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2615    init_test(cx);
2616
2617    let fs = FakeFs::new(cx.executor());
2618    fs.insert_tree(
2619        "/dir",
2620        json!({
2621            "a.ts": "a",
2622        }),
2623    )
2624    .await;
2625
2626    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2627
2628    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2629    language_registry.add(typescript_lang());
2630    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2631        "TypeScript",
2632        FakeLspAdapter {
2633            capabilities: lsp::ServerCapabilities {
2634                code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
2635                    lsp::CodeActionOptions {
2636                        resolve_provider: Some(true),
2637                        ..lsp::CodeActionOptions::default()
2638                    },
2639                )),
2640                ..lsp::ServerCapabilities::default()
2641            },
2642            ..FakeLspAdapter::default()
2643        },
2644    );
2645
2646    let buffer = project
2647        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2648        .await
2649        .unwrap();
2650
2651    let fake_server = fake_language_servers.next().await.unwrap();
2652
2653    // Language server returns code actions that contain commands, and not edits.
2654    let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2655    fake_server
2656        .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2657            Ok(Some(vec![
2658                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2659                    title: "The code action".into(),
2660                    data: Some(serde_json::json!({
2661                        "command": "_the/command",
2662                    })),
2663                    ..lsp::CodeAction::default()
2664                }),
2665                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2666                    title: "two".into(),
2667                    ..lsp::CodeAction::default()
2668                }),
2669            ]))
2670        })
2671        .next()
2672        .await;
2673
2674    let action = actions.await[0].clone();
2675    let apply = project.update(cx, |project, cx| {
2676        project.apply_code_action(buffer.clone(), action, true, cx)
2677    });
2678
2679    // Resolving the code action does not populate its edits. In absence of
2680    // edits, we must execute the given command.
2681    fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2682        |mut action, _| async move {
2683            if action.data.is_some() {
2684                action.command = Some(lsp::Command {
2685                    title: "The command".into(),
2686                    command: "_the/command".into(),
2687                    arguments: Some(vec![json!("the-argument")]),
2688                });
2689            }
2690            Ok(action)
2691        },
2692    );
2693
2694    // While executing the command, the language server sends the editor
2695    // a `workspaceEdit` request.
2696    fake_server
2697        .handle_request::<lsp::request::ExecuteCommand, _, _>({
2698            let fake = fake_server.clone();
2699            move |params, _| {
2700                assert_eq!(params.command, "_the/command");
2701                let fake = fake.clone();
2702                async move {
2703                    fake.server
2704                        .request::<lsp::request::ApplyWorkspaceEdit>(
2705                            lsp::ApplyWorkspaceEditParams {
2706                                label: None,
2707                                edit: lsp::WorkspaceEdit {
2708                                    changes: Some(
2709                                        [(
2710                                            lsp::Uri::from_file_path("/dir/a.ts").unwrap().into(),
2711                                            vec![lsp::TextEdit {
2712                                                range: lsp::Range::new(
2713                                                    lsp::Position::new(0, 0),
2714                                                    lsp::Position::new(0, 0),
2715                                                ),
2716                                                new_text: "X".into(),
2717                                            }],
2718                                        )]
2719                                        .into_iter()
2720                                        .collect(),
2721                                    ),
2722                                    ..Default::default()
2723                                },
2724                            },
2725                        )
2726                        .await
2727                        .unwrap();
2728                    Ok(Some(json!(null)))
2729                }
2730            }
2731        })
2732        .next()
2733        .await;
2734
2735    // Applying the code action returns a project transaction containing the edits
2736    // sent by the language server in its `workspaceEdit` request.
2737    let transaction = apply.await.unwrap();
2738    assert!(transaction.0.contains_key(&buffer));
2739    buffer.update(cx, |buffer, cx| {
2740        assert_eq!(buffer.text(), "Xa");
2741        buffer.undo(cx);
2742        assert_eq!(buffer.text(), "a");
2743    });
2744}
2745
2746#[gpui::test(iterations = 10)]
2747async fn test_save_file(cx: &mut gpui::TestAppContext) {
2748    init_test(cx);
2749
2750    let fs = FakeFs::new(cx.executor());
2751    fs.insert_tree(
2752        "/dir",
2753        json!({
2754            "file1": "the old contents",
2755        }),
2756    )
2757    .await;
2758
2759    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2760    let buffer = project
2761        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2762        .await
2763        .unwrap();
2764    buffer.update(cx, |buffer, cx| {
2765        assert_eq!(buffer.text(), "the old contents");
2766        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2767    });
2768
2769    project
2770        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2771        .await
2772        .unwrap();
2773
2774    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2775    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2776}
2777
2778#[gpui::test(iterations = 30)]
2779async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
2780    init_test(cx);
2781
2782    let fs = FakeFs::new(cx.executor().clone());
2783    fs.insert_tree(
2784        "/dir",
2785        json!({
2786            "file1": "the original contents",
2787        }),
2788    )
2789    .await;
2790
2791    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2792    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
2793    let buffer = project
2794        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2795        .await
2796        .unwrap();
2797
2798    // Simulate buffer diffs being slow, so that they don't complete before
2799    // the next file change occurs.
2800    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
2801
2802    // Change the buffer's file on disk, and then wait for the file change
2803    // to be detected by the worktree, so that the buffer starts reloading.
2804    fs.save(
2805        "/dir/file1".as_ref(),
2806        &"the first contents".into(),
2807        Default::default(),
2808    )
2809    .await
2810    .unwrap();
2811    worktree.next_event(cx).await;
2812
2813    // Change the buffer's file again. Depending on the random seed, the
2814    // previous file change may still be in progress.
2815    fs.save(
2816        "/dir/file1".as_ref(),
2817        &"the second contents".into(),
2818        Default::default(),
2819    )
2820    .await
2821    .unwrap();
2822    worktree.next_event(cx).await;
2823
2824    cx.executor().run_until_parked();
2825    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2826    buffer.read_with(cx, |buffer, _| {
2827        assert_eq!(buffer.text(), on_disk_text);
2828        assert!(!buffer.is_dirty(), "buffer should not be dirty");
2829        assert!(!buffer.has_conflict(), "buffer should not be dirty");
2830    });
2831}
2832
2833#[gpui::test(iterations = 30)]
2834async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
2835    init_test(cx);
2836
2837    let fs = FakeFs::new(cx.executor().clone());
2838    fs.insert_tree(
2839        "/dir",
2840        json!({
2841            "file1": "the original contents",
2842        }),
2843    )
2844    .await;
2845
2846    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2847    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
2848    let buffer = project
2849        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2850        .await
2851        .unwrap();
2852
2853    // Simulate buffer diffs being slow, so that they don't complete before
2854    // the next file change occurs.
2855    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
2856
2857    // Change the buffer's file on disk, and then wait for the file change
2858    // to be detected by the worktree, so that the buffer starts reloading.
2859    fs.save(
2860        "/dir/file1".as_ref(),
2861        &"the first contents".into(),
2862        Default::default(),
2863    )
2864    .await
2865    .unwrap();
2866    worktree.next_event(cx).await;
2867
2868    cx.executor()
2869        .spawn(cx.executor().simulate_random_delay())
2870        .await;
2871
2872    // Perform a noop edit, causing the buffer's version to increase.
2873    buffer.update(cx, |buffer, cx| {
2874        buffer.edit([(0..0, " ")], None, cx);
2875        buffer.undo(cx);
2876    });
2877
2878    cx.executor().run_until_parked();
2879    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2880    buffer.read_with(cx, |buffer, _| {
2881        let buffer_text = buffer.text();
2882        if buffer_text == on_disk_text {
2883            assert!(
2884                !buffer.is_dirty() && !buffer.has_conflict(),
2885                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
2886            );
2887        }
2888        // If the file change occurred while the buffer was processing the first
2889        // change, the buffer will be in a conflicting state.
2890        else {
2891            assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
2892            assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
2893        }
2894    });
2895}
2896
2897#[gpui::test]
2898async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2899    init_test(cx);
2900
2901    let fs = FakeFs::new(cx.executor());
2902    fs.insert_tree(
2903        "/dir",
2904        json!({
2905            "file1": "the old contents",
2906        }),
2907    )
2908    .await;
2909
2910    let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2911    let buffer = project
2912        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2913        .await
2914        .unwrap();
2915    buffer.update(cx, |buffer, cx| {
2916        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2917    });
2918
2919    project
2920        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2921        .await
2922        .unwrap();
2923
2924    let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2925    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2926}
2927
2928#[gpui::test]
2929async fn test_save_as(cx: &mut gpui::TestAppContext) {
2930    init_test(cx);
2931
2932    let fs = FakeFs::new(cx.executor());
2933    fs.insert_tree("/dir", json!({})).await;
2934
2935    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2936
2937    let languages = project.update(cx, |project, _| project.languages().clone());
2938    languages.add(rust_lang());
2939
2940    let buffer = project.update(cx, |project, cx| project.create_local_buffer("", None, cx));
2941    buffer.update(cx, |buffer, cx| {
2942        buffer.edit([(0..0, "abc")], None, cx);
2943        assert!(buffer.is_dirty());
2944        assert!(!buffer.has_conflict());
2945        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2946    });
2947    project
2948        .update(cx, |project, cx| {
2949            let worktree_id = project.worktrees().next().unwrap().read(cx).id();
2950            let path = ProjectPath {
2951                worktree_id,
2952                path: Arc::from(Path::new("file1.rs")),
2953            };
2954            project.save_buffer_as(buffer.clone(), path, cx)
2955        })
2956        .await
2957        .unwrap();
2958    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2959
2960    cx.executor().run_until_parked();
2961    buffer.update(cx, |buffer, cx| {
2962        assert_eq!(
2963            buffer.file().unwrap().full_path(cx),
2964            Path::new("dir/file1.rs")
2965        );
2966        assert!(!buffer.is_dirty());
2967        assert!(!buffer.has_conflict());
2968        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2969    });
2970
2971    let opened_buffer = project
2972        .update(cx, |project, cx| {
2973            project.open_local_buffer("/dir/file1.rs", cx)
2974        })
2975        .await
2976        .unwrap();
2977    assert_eq!(opened_buffer, buffer);
2978}
2979
2980#[gpui::test(retries = 5)]
2981async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
2982    use worktree::WorktreeModelHandle as _;
2983
2984    init_test(cx);
2985    cx.executor().allow_parking();
2986
2987    let dir = temp_tree(json!({
2988        "a": {
2989            "file1": "",
2990            "file2": "",
2991            "file3": "",
2992        },
2993        "b": {
2994            "c": {
2995                "file4": "",
2996                "file5": "",
2997            }
2998        }
2999    }));
3000
3001    let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
3002
3003    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3004        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
3005        async move { buffer.await.unwrap() }
3006    };
3007    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3008        project.update(cx, |project, cx| {
3009            let tree = project.worktrees().next().unwrap();
3010            tree.read(cx)
3011                .entry_for_path(path)
3012                .unwrap_or_else(|| panic!("no entry for path {}", path))
3013                .id
3014        })
3015    };
3016
3017    let buffer2 = buffer_for_path("a/file2", cx).await;
3018    let buffer3 = buffer_for_path("a/file3", cx).await;
3019    let buffer4 = buffer_for_path("b/c/file4", cx).await;
3020    let buffer5 = buffer_for_path("b/c/file5", cx).await;
3021
3022    let file2_id = id_for_path("a/file2", cx);
3023    let file3_id = id_for_path("a/file3", cx);
3024    let file4_id = id_for_path("b/c/file4", cx);
3025
3026    // Create a remote copy of this worktree.
3027    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3028    let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
3029
3030    let updates = Arc::new(Mutex::new(Vec::new()));
3031    tree.update(cx, |tree, cx| {
3032        let updates = updates.clone();
3033        tree.observe_updates(0, cx, move |update| {
3034            updates.lock().push(update);
3035            async { true }
3036        });
3037    });
3038
3039    let remote = cx.update(|cx| {
3040        Worktree::remote(
3041            0,
3042            1,
3043            metadata,
3044            Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
3045            cx,
3046        )
3047    });
3048
3049    cx.executor().run_until_parked();
3050
3051    cx.update(|cx| {
3052        assert!(!buffer2.read(cx).is_dirty());
3053        assert!(!buffer3.read(cx).is_dirty());
3054        assert!(!buffer4.read(cx).is_dirty());
3055        assert!(!buffer5.read(cx).is_dirty());
3056    });
3057
3058    // Rename and delete files and directories.
3059    tree.flush_fs_events(cx).await;
3060    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
3061    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
3062    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
3063    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
3064    tree.flush_fs_events(cx).await;
3065
3066    let expected_paths = vec![
3067        "a",
3068        "a/file1",
3069        "a/file2.new",
3070        "b",
3071        "d",
3072        "d/file3",
3073        "d/file4",
3074    ];
3075
3076    cx.update(|app| {
3077        assert_eq!(
3078            tree.read(app)
3079                .paths()
3080                .map(|p| p.to_str().unwrap())
3081                .collect::<Vec<_>>(),
3082            expected_paths
3083        );
3084    });
3085
3086    assert_eq!(id_for_path("a/file2.new", cx), file2_id);
3087    assert_eq!(id_for_path("d/file3", cx), file3_id);
3088    assert_eq!(id_for_path("d/file4", cx), file4_id);
3089
3090    cx.update(|cx| {
3091        assert_eq!(
3092            buffer2.read(cx).file().unwrap().path().as_ref(),
3093            Path::new("a/file2.new")
3094        );
3095        assert_eq!(
3096            buffer3.read(cx).file().unwrap().path().as_ref(),
3097            Path::new("d/file3")
3098        );
3099        assert_eq!(
3100            buffer4.read(cx).file().unwrap().path().as_ref(),
3101            Path::new("d/file4")
3102        );
3103        assert_eq!(
3104            buffer5.read(cx).file().unwrap().path().as_ref(),
3105            Path::new("b/c/file5")
3106        );
3107
3108        assert!(!buffer2.read(cx).file().unwrap().is_deleted());
3109        assert!(!buffer3.read(cx).file().unwrap().is_deleted());
3110        assert!(!buffer4.read(cx).file().unwrap().is_deleted());
3111        assert!(buffer5.read(cx).file().unwrap().is_deleted());
3112    });
3113
3114    // Update the remote worktree. Check that it becomes consistent with the
3115    // local worktree.
3116    cx.executor().run_until_parked();
3117
3118    remote.update(cx, |remote, _| {
3119        for update in updates.lock().drain(..) {
3120            remote.as_remote_mut().unwrap().update_from_remote(update);
3121        }
3122    });
3123    cx.executor().run_until_parked();
3124    remote.update(cx, |remote, _| {
3125        assert_eq!(
3126            remote
3127                .paths()
3128                .map(|p| p.to_str().unwrap())
3129                .collect::<Vec<_>>(),
3130            expected_paths
3131        );
3132    });
3133}
3134
3135#[gpui::test(iterations = 10)]
3136async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
3137    init_test(cx);
3138
3139    let fs = FakeFs::new(cx.executor());
3140    fs.insert_tree(
3141        "/dir",
3142        json!({
3143            "a": {
3144                "file1": "",
3145            }
3146        }),
3147    )
3148    .await;
3149
3150    let project = Project::test(fs, [Path::new("/dir")], cx).await;
3151    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3152    let tree_id = tree.update(cx, |tree, _| tree.id());
3153
3154    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3155        project.update(cx, |project, cx| {
3156            let tree = project.worktrees().next().unwrap();
3157            tree.read(cx)
3158                .entry_for_path(path)
3159                .unwrap_or_else(|| panic!("no entry for path {}", path))
3160                .id
3161        })
3162    };
3163
3164    let dir_id = id_for_path("a", cx);
3165    let file_id = id_for_path("a/file1", cx);
3166    let buffer = project
3167        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
3168        .await
3169        .unwrap();
3170    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3171
3172    project
3173        .update(cx, |project, cx| {
3174            project.rename_entry(dir_id, Path::new("b"), cx)
3175        })
3176        .unwrap()
3177        .await
3178        .to_included()
3179        .unwrap();
3180    cx.executor().run_until_parked();
3181
3182    assert_eq!(id_for_path("b", cx), dir_id);
3183    assert_eq!(id_for_path("b/file1", cx), file_id);
3184    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3185}
3186
3187#[gpui::test]
3188async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
3189    init_test(cx);
3190
3191    let fs = FakeFs::new(cx.executor());
3192    fs.insert_tree(
3193        "/dir",
3194        json!({
3195            "a.txt": "a-contents",
3196            "b.txt": "b-contents",
3197        }),
3198    )
3199    .await;
3200
3201    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3202
3203    // Spawn multiple tasks to open paths, repeating some paths.
3204    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
3205        (
3206            p.open_local_buffer("/dir/a.txt", cx),
3207            p.open_local_buffer("/dir/b.txt", cx),
3208            p.open_local_buffer("/dir/a.txt", cx),
3209        )
3210    });
3211
3212    let buffer_a_1 = buffer_a_1.await.unwrap();
3213    let buffer_a_2 = buffer_a_2.await.unwrap();
3214    let buffer_b = buffer_b.await.unwrap();
3215    assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
3216    assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
3217
3218    // There is only one buffer per path.
3219    let buffer_a_id = buffer_a_1.entity_id();
3220    assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
3221
3222    // Open the same path again while it is still open.
3223    drop(buffer_a_1);
3224    let buffer_a_3 = project
3225        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
3226        .await
3227        .unwrap();
3228
3229    // There's still only one buffer per path.
3230    assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
3231}
3232
3233#[gpui::test]
3234async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
3235    init_test(cx);
3236
3237    let fs = FakeFs::new(cx.executor());
3238    fs.insert_tree(
3239        "/dir",
3240        json!({
3241            "file1": "abc",
3242            "file2": "def",
3243            "file3": "ghi",
3244        }),
3245    )
3246    .await;
3247
3248    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3249
3250    let buffer1 = project
3251        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3252        .await
3253        .unwrap();
3254    let events = Arc::new(Mutex::new(Vec::new()));
3255
3256    // initially, the buffer isn't dirty.
3257    buffer1.update(cx, |buffer, cx| {
3258        cx.subscribe(&buffer1, {
3259            let events = events.clone();
3260            move |_, _, event, _| match event {
3261                BufferEvent::Operation(_) => {}
3262                _ => events.lock().push(event.clone()),
3263            }
3264        })
3265        .detach();
3266
3267        assert!(!buffer.is_dirty());
3268        assert!(events.lock().is_empty());
3269
3270        buffer.edit([(1..2, "")], None, cx);
3271    });
3272
3273    // after the first edit, the buffer is dirty, and emits a dirtied event.
3274    buffer1.update(cx, |buffer, cx| {
3275        assert!(buffer.text() == "ac");
3276        assert!(buffer.is_dirty());
3277        assert_eq!(
3278            *events.lock(),
3279            &[language::Event::Edited, language::Event::DirtyChanged]
3280        );
3281        events.lock().clear();
3282        buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
3283    });
3284
3285    // after saving, the buffer is not dirty, and emits a saved event.
3286    buffer1.update(cx, |buffer, cx| {
3287        assert!(!buffer.is_dirty());
3288        assert_eq!(*events.lock(), &[language::Event::Saved]);
3289        events.lock().clear();
3290
3291        buffer.edit([(1..1, "B")], None, cx);
3292        buffer.edit([(2..2, "D")], None, cx);
3293    });
3294
3295    // after editing again, the buffer is dirty, and emits another dirty event.
3296    buffer1.update(cx, |buffer, cx| {
3297        assert!(buffer.text() == "aBDc");
3298        assert!(buffer.is_dirty());
3299        assert_eq!(
3300            *events.lock(),
3301            &[
3302                language::Event::Edited,
3303                language::Event::DirtyChanged,
3304                language::Event::Edited,
3305            ],
3306        );
3307        events.lock().clear();
3308
3309        // After restoring the buffer to its previously-saved state,
3310        // the buffer is not considered dirty anymore.
3311        buffer.edit([(1..3, "")], None, cx);
3312        assert!(buffer.text() == "ac");
3313        assert!(!buffer.is_dirty());
3314    });
3315
3316    assert_eq!(
3317        *events.lock(),
3318        &[language::Event::Edited, language::Event::DirtyChanged]
3319    );
3320
3321    // When a file is deleted, the buffer is considered dirty.
3322    let events = Arc::new(Mutex::new(Vec::new()));
3323    let buffer2 = project
3324        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3325        .await
3326        .unwrap();
3327    buffer2.update(cx, |_, cx| {
3328        cx.subscribe(&buffer2, {
3329            let events = events.clone();
3330            move |_, _, event, _| events.lock().push(event.clone())
3331        })
3332        .detach();
3333    });
3334
3335    fs.remove_file("/dir/file2".as_ref(), Default::default())
3336        .await
3337        .unwrap();
3338    cx.executor().run_until_parked();
3339    buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3340    assert_eq!(
3341        *events.lock(),
3342        &[
3343            language::Event::DirtyChanged,
3344            language::Event::FileHandleChanged
3345        ]
3346    );
3347
3348    // When a file is already dirty when deleted, we don't emit a Dirtied event.
3349    let events = Arc::new(Mutex::new(Vec::new()));
3350    let buffer3 = project
3351        .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3352        .await
3353        .unwrap();
3354    buffer3.update(cx, |_, cx| {
3355        cx.subscribe(&buffer3, {
3356            let events = events.clone();
3357            move |_, _, event, _| events.lock().push(event.clone())
3358        })
3359        .detach();
3360    });
3361
3362    buffer3.update(cx, |buffer, cx| {
3363        buffer.edit([(0..0, "x")], None, cx);
3364    });
3365    events.lock().clear();
3366    fs.remove_file("/dir/file3".as_ref(), Default::default())
3367        .await
3368        .unwrap();
3369    cx.executor().run_until_parked();
3370    assert_eq!(*events.lock(), &[language::Event::FileHandleChanged]);
3371    cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3372}
3373
3374#[gpui::test]
3375async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
3376    init_test(cx);
3377
3378    let initial_contents = "aaa\nbbbbb\nc\n";
3379    let fs = FakeFs::new(cx.executor());
3380    fs.insert_tree(
3381        "/dir",
3382        json!({
3383            "the-file": initial_contents,
3384        }),
3385    )
3386    .await;
3387    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3388    let buffer = project
3389        .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3390        .await
3391        .unwrap();
3392
3393    let anchors = (0..3)
3394        .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3395        .collect::<Vec<_>>();
3396
3397    // Change the file on disk, adding two new lines of text, and removing
3398    // one line.
3399    buffer.update(cx, |buffer, _| {
3400        assert!(!buffer.is_dirty());
3401        assert!(!buffer.has_conflict());
3402    });
3403    let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3404    fs.save(
3405        "/dir/the-file".as_ref(),
3406        &new_contents.into(),
3407        LineEnding::Unix,
3408    )
3409    .await
3410    .unwrap();
3411
3412    // Because the buffer was not modified, it is reloaded from disk. Its
3413    // contents are edited according to the diff between the old and new
3414    // file contents.
3415    cx.executor().run_until_parked();
3416    buffer.update(cx, |buffer, _| {
3417        assert_eq!(buffer.text(), new_contents);
3418        assert!(!buffer.is_dirty());
3419        assert!(!buffer.has_conflict());
3420
3421        let anchor_positions = anchors
3422            .iter()
3423            .map(|anchor| anchor.to_point(&*buffer))
3424            .collect::<Vec<_>>();
3425        assert_eq!(
3426            anchor_positions,
3427            [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3428        );
3429    });
3430
3431    // Modify the buffer
3432    buffer.update(cx, |buffer, cx| {
3433        buffer.edit([(0..0, " ")], None, cx);
3434        assert!(buffer.is_dirty());
3435        assert!(!buffer.has_conflict());
3436    });
3437
3438    // Change the file on disk again, adding blank lines to the beginning.
3439    fs.save(
3440        "/dir/the-file".as_ref(),
3441        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3442        LineEnding::Unix,
3443    )
3444    .await
3445    .unwrap();
3446
3447    // Because the buffer is modified, it doesn't reload from disk, but is
3448    // marked as having a conflict.
3449    cx.executor().run_until_parked();
3450    buffer.update(cx, |buffer, _| {
3451        assert!(buffer.has_conflict());
3452    });
3453}
3454
3455#[gpui::test]
3456async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
3457    init_test(cx);
3458
3459    let fs = FakeFs::new(cx.executor());
3460    fs.insert_tree(
3461        "/dir",
3462        json!({
3463            "file1": "a\nb\nc\n",
3464            "file2": "one\r\ntwo\r\nthree\r\n",
3465        }),
3466    )
3467    .await;
3468
3469    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3470    let buffer1 = project
3471        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3472        .await
3473        .unwrap();
3474    let buffer2 = project
3475        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3476        .await
3477        .unwrap();
3478
3479    buffer1.update(cx, |buffer, _| {
3480        assert_eq!(buffer.text(), "a\nb\nc\n");
3481        assert_eq!(buffer.line_ending(), LineEnding::Unix);
3482    });
3483    buffer2.update(cx, |buffer, _| {
3484        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3485        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3486    });
3487
3488    // Change a file's line endings on disk from unix to windows. The buffer's
3489    // state updates correctly.
3490    fs.save(
3491        "/dir/file1".as_ref(),
3492        &"aaa\nb\nc\n".into(),
3493        LineEnding::Windows,
3494    )
3495    .await
3496    .unwrap();
3497    cx.executor().run_until_parked();
3498    buffer1.update(cx, |buffer, _| {
3499        assert_eq!(buffer.text(), "aaa\nb\nc\n");
3500        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3501    });
3502
3503    // Save a file with windows line endings. The file is written correctly.
3504    buffer2.update(cx, |buffer, cx| {
3505        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3506    });
3507    project
3508        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3509        .await
3510        .unwrap();
3511    assert_eq!(
3512        fs.load("/dir/file2".as_ref()).await.unwrap(),
3513        "one\r\ntwo\r\nthree\r\nfour\r\n",
3514    );
3515}
3516
3517#[gpui::test]
3518async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
3519    init_test(cx);
3520
3521    let fs = FakeFs::new(cx.executor());
3522    fs.insert_tree(
3523        "/the-dir",
3524        json!({
3525            "a.rs": "
3526                fn foo(mut v: Vec<usize>) {
3527                    for x in &v {
3528                        v.push(1);
3529                    }
3530                }
3531            "
3532            .unindent(),
3533        }),
3534    )
3535    .await;
3536
3537    let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3538    let buffer = project
3539        .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3540        .await
3541        .unwrap();
3542
3543    let buffer_uri = Uri::from_file_path("/the-dir/a.rs").unwrap();
3544    let message = lsp::PublishDiagnosticsParams {
3545        uri: buffer_uri.clone().into(),
3546        diagnostics: vec![
3547            lsp::Diagnostic {
3548                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3549                severity: Some(DiagnosticSeverity::WARNING),
3550                message: "error 1".to_string(),
3551                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3552                    location: lsp::Location {
3553                        uri: buffer_uri.clone().into(),
3554                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3555                    },
3556                    message: "error 1 hint 1".to_string(),
3557                }]),
3558                ..Default::default()
3559            },
3560            lsp::Diagnostic {
3561                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3562                severity: Some(DiagnosticSeverity::HINT),
3563                message: "error 1 hint 1".to_string(),
3564                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3565                    location: lsp::Location {
3566                        uri: buffer_uri.clone().into(),
3567                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3568                    },
3569                    message: "original diagnostic".to_string(),
3570                }]),
3571                ..Default::default()
3572            },
3573            lsp::Diagnostic {
3574                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3575                severity: Some(DiagnosticSeverity::ERROR),
3576                message: "error 2".to_string(),
3577                related_information: Some(vec![
3578                    lsp::DiagnosticRelatedInformation {
3579                        location: lsp::Location {
3580                            uri: buffer_uri.clone().into(),
3581                            range: lsp::Range::new(
3582                                lsp::Position::new(1, 13),
3583                                lsp::Position::new(1, 15),
3584                            ),
3585                        },
3586                        message: "error 2 hint 1".to_string(),
3587                    },
3588                    lsp::DiagnosticRelatedInformation {
3589                        location: lsp::Location {
3590                            uri: buffer_uri.clone().into(),
3591                            range: lsp::Range::new(
3592                                lsp::Position::new(1, 13),
3593                                lsp::Position::new(1, 15),
3594                            ),
3595                        },
3596                        message: "error 2 hint 2".to_string(),
3597                    },
3598                ]),
3599                ..Default::default()
3600            },
3601            lsp::Diagnostic {
3602                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3603                severity: Some(DiagnosticSeverity::HINT),
3604                message: "error 2 hint 1".to_string(),
3605                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3606                    location: lsp::Location {
3607                        uri: buffer_uri.clone().into(),
3608                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3609                    },
3610                    message: "original diagnostic".to_string(),
3611                }]),
3612                ..Default::default()
3613            },
3614            lsp::Diagnostic {
3615                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3616                severity: Some(DiagnosticSeverity::HINT),
3617                message: "error 2 hint 2".to_string(),
3618                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3619                    location: lsp::Location {
3620                        uri: buffer_uri.into(),
3621                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3622                    },
3623                    message: "original diagnostic".to_string(),
3624                }]),
3625                ..Default::default()
3626            },
3627        ],
3628        version: None,
3629    };
3630
3631    project
3632        .update(cx, |p, cx| {
3633            p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3634        })
3635        .unwrap();
3636    let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3637
3638    assert_eq!(
3639        buffer
3640            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3641            .collect::<Vec<_>>(),
3642        &[
3643            DiagnosticEntry {
3644                range: Point::new(1, 8)..Point::new(1, 9),
3645                diagnostic: Diagnostic {
3646                    severity: DiagnosticSeverity::WARNING,
3647                    message: "error 1".to_string(),
3648                    group_id: 1,
3649                    is_primary: true,
3650                    ..Default::default()
3651                }
3652            },
3653            DiagnosticEntry {
3654                range: Point::new(1, 8)..Point::new(1, 9),
3655                diagnostic: Diagnostic {
3656                    severity: DiagnosticSeverity::HINT,
3657                    message: "error 1 hint 1".to_string(),
3658                    group_id: 1,
3659                    is_primary: false,
3660                    ..Default::default()
3661                }
3662            },
3663            DiagnosticEntry {
3664                range: Point::new(1, 13)..Point::new(1, 15),
3665                diagnostic: Diagnostic {
3666                    severity: DiagnosticSeverity::HINT,
3667                    message: "error 2 hint 1".to_string(),
3668                    group_id: 0,
3669                    is_primary: false,
3670                    ..Default::default()
3671                }
3672            },
3673            DiagnosticEntry {
3674                range: Point::new(1, 13)..Point::new(1, 15),
3675                diagnostic: Diagnostic {
3676                    severity: DiagnosticSeverity::HINT,
3677                    message: "error 2 hint 2".to_string(),
3678                    group_id: 0,
3679                    is_primary: false,
3680                    ..Default::default()
3681                }
3682            },
3683            DiagnosticEntry {
3684                range: Point::new(2, 8)..Point::new(2, 17),
3685                diagnostic: Diagnostic {
3686                    severity: DiagnosticSeverity::ERROR,
3687                    message: "error 2".to_string(),
3688                    group_id: 0,
3689                    is_primary: true,
3690                    ..Default::default()
3691                }
3692            }
3693        ]
3694    );
3695
3696    assert_eq!(
3697        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3698        &[
3699            DiagnosticEntry {
3700                range: Point::new(1, 13)..Point::new(1, 15),
3701                diagnostic: Diagnostic {
3702                    severity: DiagnosticSeverity::HINT,
3703                    message: "error 2 hint 1".to_string(),
3704                    group_id: 0,
3705                    is_primary: false,
3706                    ..Default::default()
3707                }
3708            },
3709            DiagnosticEntry {
3710                range: Point::new(1, 13)..Point::new(1, 15),
3711                diagnostic: Diagnostic {
3712                    severity: DiagnosticSeverity::HINT,
3713                    message: "error 2 hint 2".to_string(),
3714                    group_id: 0,
3715                    is_primary: false,
3716                    ..Default::default()
3717                }
3718            },
3719            DiagnosticEntry {
3720                range: Point::new(2, 8)..Point::new(2, 17),
3721                diagnostic: Diagnostic {
3722                    severity: DiagnosticSeverity::ERROR,
3723                    message: "error 2".to_string(),
3724                    group_id: 0,
3725                    is_primary: true,
3726                    ..Default::default()
3727                }
3728            }
3729        ]
3730    );
3731
3732    assert_eq!(
3733        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3734        &[
3735            DiagnosticEntry {
3736                range: Point::new(1, 8)..Point::new(1, 9),
3737                diagnostic: Diagnostic {
3738                    severity: DiagnosticSeverity::WARNING,
3739                    message: "error 1".to_string(),
3740                    group_id: 1,
3741                    is_primary: true,
3742                    ..Default::default()
3743                }
3744            },
3745            DiagnosticEntry {
3746                range: Point::new(1, 8)..Point::new(1, 9),
3747                diagnostic: Diagnostic {
3748                    severity: DiagnosticSeverity::HINT,
3749                    message: "error 1 hint 1".to_string(),
3750                    group_id: 1,
3751                    is_primary: false,
3752                    ..Default::default()
3753                }
3754            },
3755        ]
3756    );
3757}
3758
3759#[gpui::test]
3760async fn test_rename(cx: &mut gpui::TestAppContext) {
3761    init_test(cx);
3762
3763    let fs = FakeFs::new(cx.executor());
3764    fs.insert_tree(
3765        "/dir",
3766        json!({
3767            "one.rs": "const ONE: usize = 1;",
3768            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3769        }),
3770    )
3771    .await;
3772
3773    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3774
3775    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3776    language_registry.add(rust_lang());
3777    let mut fake_servers = language_registry.register_fake_lsp_adapter(
3778        "Rust",
3779        FakeLspAdapter {
3780            capabilities: lsp::ServerCapabilities {
3781                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3782                    prepare_provider: Some(true),
3783                    work_done_progress_options: Default::default(),
3784                })),
3785                ..Default::default()
3786            },
3787            ..Default::default()
3788        },
3789    );
3790
3791    let buffer = project
3792        .update(cx, |project, cx| {
3793            project.open_local_buffer("/dir/one.rs", cx)
3794        })
3795        .await
3796        .unwrap();
3797
3798    let fake_server = fake_servers.next().await.unwrap();
3799
3800    let response = project.update(cx, |project, cx| {
3801        project.prepare_rename(buffer.clone(), 7, cx)
3802    });
3803    fake_server
3804        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3805            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3806            assert_eq!(params.position, lsp::Position::new(0, 7));
3807            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3808                lsp::Position::new(0, 6),
3809                lsp::Position::new(0, 9),
3810            ))))
3811        })
3812        .next()
3813        .await
3814        .unwrap();
3815    let range = response.await.unwrap().unwrap();
3816    let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3817    assert_eq!(range, 6..9);
3818
3819    let response = project.update(cx, |project, cx| {
3820        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3821    });
3822    fake_server
3823        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3824            assert_eq!(
3825                params.text_document_position.text_document.uri.as_str(),
3826                "file:///dir/one.rs"
3827            );
3828            assert_eq!(
3829                params.text_document_position.position,
3830                lsp::Position::new(0, 7)
3831            );
3832            assert_eq!(params.new_name, "THREE");
3833            Ok(Some(lsp::WorkspaceEdit {
3834                changes: Some(
3835                    [
3836                        (
3837                            lsp::Uri::from_file_path("/dir/one.rs").unwrap().into(),
3838                            vec![lsp::TextEdit::new(
3839                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3840                                "THREE".to_string(),
3841                            )],
3842                        ),
3843                        (
3844                            lsp::Uri::from_file_path("/dir/two.rs").unwrap().into(),
3845                            vec![
3846                                lsp::TextEdit::new(
3847                                    lsp::Range::new(
3848                                        lsp::Position::new(0, 24),
3849                                        lsp::Position::new(0, 27),
3850                                    ),
3851                                    "THREE".to_string(),
3852                                ),
3853                                lsp::TextEdit::new(
3854                                    lsp::Range::new(
3855                                        lsp::Position::new(0, 35),
3856                                        lsp::Position::new(0, 38),
3857                                    ),
3858                                    "THREE".to_string(),
3859                                ),
3860                            ],
3861                        ),
3862                    ]
3863                    .into_iter()
3864                    .collect(),
3865                ),
3866                ..Default::default()
3867            }))
3868        })
3869        .next()
3870        .await
3871        .unwrap();
3872    let mut transaction = response.await.unwrap().0;
3873    assert_eq!(transaction.len(), 2);
3874    assert_eq!(
3875        transaction
3876            .remove_entry(&buffer)
3877            .unwrap()
3878            .0
3879            .update(cx, |buffer, _| buffer.text()),
3880        "const THREE: usize = 1;"
3881    );
3882    assert_eq!(
3883        transaction
3884            .into_keys()
3885            .next()
3886            .unwrap()
3887            .update(cx, |buffer, _| buffer.text()),
3888        "const TWO: usize = one::THREE + one::THREE;"
3889    );
3890}
3891
3892#[gpui::test]
3893async fn test_search(cx: &mut gpui::TestAppContext) {
3894    init_test(cx);
3895
3896    let fs = FakeFs::new(cx.executor());
3897    fs.insert_tree(
3898        "/dir",
3899        json!({
3900            "one.rs": "const ONE: usize = 1;",
3901            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3902            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3903            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3904        }),
3905    )
3906    .await;
3907    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3908    assert_eq!(
3909        search(
3910            &project,
3911            SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3912            cx
3913        )
3914        .await
3915        .unwrap(),
3916        HashMap::from_iter([
3917            ("dir/two.rs".to_string(), vec![6..9]),
3918            ("dir/three.rs".to_string(), vec![37..40])
3919        ])
3920    );
3921
3922    let buffer_4 = project
3923        .update(cx, |project, cx| {
3924            project.open_local_buffer("/dir/four.rs", cx)
3925        })
3926        .await
3927        .unwrap();
3928    buffer_4.update(cx, |buffer, cx| {
3929        let text = "two::TWO";
3930        buffer.edit([(20..28, text), (31..43, text)], None, cx);
3931    });
3932
3933    assert_eq!(
3934        search(
3935            &project,
3936            SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3937            cx
3938        )
3939        .await
3940        .unwrap(),
3941        HashMap::from_iter([
3942            ("dir/two.rs".to_string(), vec![6..9]),
3943            ("dir/three.rs".to_string(), vec![37..40]),
3944            ("dir/four.rs".to_string(), vec![25..28, 36..39])
3945        ])
3946    );
3947}
3948
3949#[gpui::test]
3950async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
3951    init_test(cx);
3952
3953    let search_query = "file";
3954
3955    let fs = FakeFs::new(cx.executor());
3956    fs.insert_tree(
3957        "/dir",
3958        json!({
3959            "one.rs": r#"// Rust file one"#,
3960            "one.ts": r#"// TypeScript file one"#,
3961            "two.rs": r#"// Rust file two"#,
3962            "two.ts": r#"// TypeScript file two"#,
3963        }),
3964    )
3965    .await;
3966    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3967
3968    assert!(
3969        search(
3970            &project,
3971            SearchQuery::text(
3972                search_query,
3973                false,
3974                true,
3975                false,
3976                vec![PathMatcher::new("*.odd").unwrap()],
3977                Vec::new()
3978            )
3979            .unwrap(),
3980            cx
3981        )
3982        .await
3983        .unwrap()
3984        .is_empty(),
3985        "If no inclusions match, no files should be returned"
3986    );
3987
3988    assert_eq!(
3989        search(
3990            &project,
3991            SearchQuery::text(
3992                search_query,
3993                false,
3994                true,
3995                false,
3996                vec![PathMatcher::new("*.rs").unwrap()],
3997                Vec::new()
3998            )
3999            .unwrap(),
4000            cx
4001        )
4002        .await
4003        .unwrap(),
4004        HashMap::from_iter([
4005            ("dir/one.rs".to_string(), vec![8..12]),
4006            ("dir/two.rs".to_string(), vec![8..12]),
4007        ]),
4008        "Rust only search should give only Rust files"
4009    );
4010
4011    assert_eq!(
4012        search(
4013            &project,
4014            SearchQuery::text(
4015                search_query,
4016                false,
4017                true,
4018                false,
4019                vec![
4020                    PathMatcher::new("*.ts").unwrap(),
4021                    PathMatcher::new("*.odd").unwrap(),
4022                ],
4023                Vec::new()
4024            ).unwrap(),
4025            cx
4026        )
4027        .await
4028        .unwrap(),
4029        HashMap::from_iter([
4030            ("dir/one.ts".to_string(), vec![14..18]),
4031            ("dir/two.ts".to_string(), vec![14..18]),
4032        ]),
4033        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
4034    );
4035
4036    assert_eq!(
4037        search(
4038            &project,
4039            SearchQuery::text(
4040                search_query,
4041                false,
4042                true,
4043                false,
4044                vec![
4045                    PathMatcher::new("*.rs").unwrap(),
4046                    PathMatcher::new("*.ts").unwrap(),
4047                    PathMatcher::new("*.odd").unwrap(),
4048                ],
4049                Vec::new()
4050            ).unwrap(),
4051            cx
4052        )
4053        .await
4054        .unwrap(),
4055        HashMap::from_iter([
4056            ("dir/two.ts".to_string(), vec![14..18]),
4057            ("dir/one.rs".to_string(), vec![8..12]),
4058            ("dir/one.ts".to_string(), vec![14..18]),
4059            ("dir/two.rs".to_string(), vec![8..12]),
4060        ]),
4061        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
4062    );
4063}
4064
4065#[gpui::test]
4066async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
4067    init_test(cx);
4068
4069    let search_query = "file";
4070
4071    let fs = FakeFs::new(cx.executor());
4072    fs.insert_tree(
4073        "/dir",
4074        json!({
4075            "one.rs": r#"// Rust file one"#,
4076            "one.ts": r#"// TypeScript file one"#,
4077            "two.rs": r#"// Rust file two"#,
4078            "two.ts": r#"// TypeScript file two"#,
4079        }),
4080    )
4081    .await;
4082    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4083
4084    assert_eq!(
4085        search(
4086            &project,
4087            SearchQuery::text(
4088                search_query,
4089                false,
4090                true,
4091                false,
4092                Vec::new(),
4093                vec![PathMatcher::new("*.odd").unwrap()],
4094            )
4095            .unwrap(),
4096            cx
4097        )
4098        .await
4099        .unwrap(),
4100        HashMap::from_iter([
4101            ("dir/one.rs".to_string(), vec![8..12]),
4102            ("dir/one.ts".to_string(), vec![14..18]),
4103            ("dir/two.rs".to_string(), vec![8..12]),
4104            ("dir/two.ts".to_string(), vec![14..18]),
4105        ]),
4106        "If no exclusions match, all files should be returned"
4107    );
4108
4109    assert_eq!(
4110        search(
4111            &project,
4112            SearchQuery::text(
4113                search_query,
4114                false,
4115                true,
4116                false,
4117                Vec::new(),
4118                vec![PathMatcher::new("*.rs").unwrap()],
4119            )
4120            .unwrap(),
4121            cx
4122        )
4123        .await
4124        .unwrap(),
4125        HashMap::from_iter([
4126            ("dir/one.ts".to_string(), vec![14..18]),
4127            ("dir/two.ts".to_string(), vec![14..18]),
4128        ]),
4129        "Rust exclusion search should give only TypeScript files"
4130    );
4131
4132    assert_eq!(
4133        search(
4134            &project,
4135            SearchQuery::text(
4136                search_query,
4137                false,
4138                true,
4139                false,
4140                Vec::new(),
4141                vec![
4142                    PathMatcher::new("*.ts").unwrap(),
4143                    PathMatcher::new("*.odd").unwrap(),
4144                ],
4145            ).unwrap(),
4146            cx
4147        )
4148        .await
4149        .unwrap(),
4150        HashMap::from_iter([
4151            ("dir/one.rs".to_string(), vec![8..12]),
4152            ("dir/two.rs".to_string(), vec![8..12]),
4153        ]),
4154        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
4155    );
4156
4157    assert!(
4158        search(
4159            &project,
4160            SearchQuery::text(
4161                search_query,
4162                false,
4163                true,
4164                false,
4165                Vec::new(),
4166                vec![
4167                    PathMatcher::new("*.rs").unwrap(),
4168                    PathMatcher::new("*.ts").unwrap(),
4169                    PathMatcher::new("*.odd").unwrap(),
4170                ],
4171            ).unwrap(),
4172            cx
4173        )
4174        .await
4175        .unwrap().is_empty(),
4176        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
4177    );
4178}
4179
4180#[gpui::test]
4181async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
4182    init_test(cx);
4183
4184    let search_query = "file";
4185
4186    let fs = FakeFs::new(cx.executor());
4187    fs.insert_tree(
4188        "/dir",
4189        json!({
4190            "one.rs": r#"// Rust file one"#,
4191            "one.ts": r#"// TypeScript file one"#,
4192            "two.rs": r#"// Rust file two"#,
4193            "two.ts": r#"// TypeScript file two"#,
4194        }),
4195    )
4196    .await;
4197    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4198
4199    assert!(
4200        search(
4201            &project,
4202            SearchQuery::text(
4203                search_query,
4204                false,
4205                true,
4206                false,
4207                vec![PathMatcher::new("*.odd").unwrap()],
4208                vec![PathMatcher::new("*.odd").unwrap()],
4209            )
4210            .unwrap(),
4211            cx
4212        )
4213        .await
4214        .unwrap()
4215        .is_empty(),
4216        "If both no exclusions and inclusions match, exclusions should win and return nothing"
4217    );
4218
4219    assert!(
4220        search(
4221            &project,
4222            SearchQuery::text(
4223                search_query,
4224                false,
4225                true,
4226                false,
4227                vec![PathMatcher::new("*.ts").unwrap()],
4228                vec![PathMatcher::new("*.ts").unwrap()],
4229            ).unwrap(),
4230            cx
4231        )
4232        .await
4233        .unwrap()
4234        .is_empty(),
4235        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
4236    );
4237
4238    assert!(
4239        search(
4240            &project,
4241            SearchQuery::text(
4242                search_query,
4243                false,
4244                true,
4245                false,
4246                vec![
4247                    PathMatcher::new("*.ts").unwrap(),
4248                    PathMatcher::new("*.odd").unwrap()
4249                ],
4250                vec![
4251                    PathMatcher::new("*.ts").unwrap(),
4252                    PathMatcher::new("*.odd").unwrap()
4253                ],
4254            )
4255            .unwrap(),
4256            cx
4257        )
4258        .await
4259        .unwrap()
4260        .is_empty(),
4261        "Non-matching inclusions and exclusions should not change that."
4262    );
4263
4264    assert_eq!(
4265        search(
4266            &project,
4267            SearchQuery::text(
4268                search_query,
4269                false,
4270                true,
4271                false,
4272                vec![
4273                    PathMatcher::new("*.ts").unwrap(),
4274                    PathMatcher::new("*.odd").unwrap()
4275                ],
4276                vec![
4277                    PathMatcher::new("*.rs").unwrap(),
4278                    PathMatcher::new("*.odd").unwrap()
4279                ],
4280            )
4281            .unwrap(),
4282            cx
4283        )
4284        .await
4285        .unwrap(),
4286        HashMap::from_iter([
4287            ("dir/one.ts".to_string(), vec![14..18]),
4288            ("dir/two.ts".to_string(), vec![14..18]),
4289        ]),
4290        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4291    );
4292}
4293
4294#[gpui::test]
4295async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
4296    init_test(cx);
4297
4298    let fs = FakeFs::new(cx.executor());
4299    fs.insert_tree(
4300        "/worktree-a",
4301        json!({
4302            "haystack.rs": r#"// NEEDLE"#,
4303            "haystack.ts": r#"// NEEDLE"#,
4304        }),
4305    )
4306    .await;
4307    fs.insert_tree(
4308        "/worktree-b",
4309        json!({
4310            "haystack.rs": r#"// NEEDLE"#,
4311            "haystack.ts": r#"// NEEDLE"#,
4312        }),
4313    )
4314    .await;
4315
4316    let project = Project::test(
4317        fs.clone(),
4318        ["/worktree-a".as_ref(), "/worktree-b".as_ref()],
4319        cx,
4320    )
4321    .await;
4322
4323    assert_eq!(
4324        search(
4325            &project,
4326            SearchQuery::text(
4327                "NEEDLE",
4328                false,
4329                true,
4330                false,
4331                vec![PathMatcher::new("worktree-a/*.rs").unwrap()],
4332                Vec::new()
4333            )
4334            .unwrap(),
4335            cx
4336        )
4337        .await
4338        .unwrap(),
4339        HashMap::from_iter([("worktree-a/haystack.rs".to_string(), vec![3..9])]),
4340        "should only return results from included worktree"
4341    );
4342    assert_eq!(
4343        search(
4344            &project,
4345            SearchQuery::text(
4346                "NEEDLE",
4347                false,
4348                true,
4349                false,
4350                vec![PathMatcher::new("worktree-b/*.rs").unwrap()],
4351                Vec::new()
4352            )
4353            .unwrap(),
4354            cx
4355        )
4356        .await
4357        .unwrap(),
4358        HashMap::from_iter([("worktree-b/haystack.rs".to_string(), vec![3..9])]),
4359        "should only return results from included worktree"
4360    );
4361
4362    assert_eq!(
4363        search(
4364            &project,
4365            SearchQuery::text(
4366                "NEEDLE",
4367                false,
4368                true,
4369                false,
4370                vec![PathMatcher::new("*.ts").unwrap()],
4371                Vec::new()
4372            )
4373            .unwrap(),
4374            cx
4375        )
4376        .await
4377        .unwrap(),
4378        HashMap::from_iter([
4379            ("worktree-a/haystack.ts".to_string(), vec![3..9]),
4380            ("worktree-b/haystack.ts".to_string(), vec![3..9])
4381        ]),
4382        "should return results from both worktrees"
4383    );
4384}
4385
4386#[gpui::test]
4387async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
4388    init_test(cx);
4389
4390    let fs = FakeFs::new(cx.background_executor.clone());
4391    fs.insert_tree(
4392        "/dir",
4393        json!({
4394            ".git": {},
4395            ".gitignore": "**/target\n/node_modules\n",
4396            "target": {
4397                "index.txt": "index_key:index_value"
4398            },
4399            "node_modules": {
4400                "eslint": {
4401                    "index.ts": "const eslint_key = 'eslint value'",
4402                    "package.json": r#"{ "some_key": "some value" }"#,
4403                },
4404                "prettier": {
4405                    "index.ts": "const prettier_key = 'prettier value'",
4406                    "package.json": r#"{ "other_key": "other value" }"#,
4407                },
4408            },
4409            "package.json": r#"{ "main_key": "main value" }"#,
4410        }),
4411    )
4412    .await;
4413    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4414
4415    let query = "key";
4416    assert_eq!(
4417        search(
4418            &project,
4419            SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
4420            cx
4421        )
4422        .await
4423        .unwrap(),
4424        HashMap::from_iter([("dir/package.json".to_string(), vec![8..11])]),
4425        "Only one non-ignored file should have the query"
4426    );
4427
4428    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4429    assert_eq!(
4430        search(
4431            &project,
4432            SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
4433            cx
4434        )
4435        .await
4436        .unwrap(),
4437        HashMap::from_iter([
4438            ("dir/package.json".to_string(), vec![8..11]),
4439            ("dir/target/index.txt".to_string(), vec![6..9]),
4440            (
4441                "dir/node_modules/prettier/package.json".to_string(),
4442                vec![9..12]
4443            ),
4444            (
4445                "dir/node_modules/prettier/index.ts".to_string(),
4446                vec![15..18]
4447            ),
4448            ("dir/node_modules/eslint/index.ts".to_string(), vec![13..16]),
4449            (
4450                "dir/node_modules/eslint/package.json".to_string(),
4451                vec![8..11]
4452            ),
4453        ]),
4454        "Unrestricted search with ignored directories should find every file with the query"
4455    );
4456
4457    let files_to_include = vec![PathMatcher::new("/dir/node_modules/prettier/**").unwrap()];
4458    let files_to_exclude = vec![PathMatcher::new("*.ts").unwrap()];
4459    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4460    assert_eq!(
4461        search(
4462            &project,
4463            SearchQuery::text(
4464                query,
4465                false,
4466                false,
4467                true,
4468                files_to_include,
4469                files_to_exclude,
4470            )
4471            .unwrap(),
4472            cx
4473        )
4474        .await
4475        .unwrap(),
4476        HashMap::from_iter([(
4477            "dir/node_modules/prettier/package.json".to_string(),
4478            vec![9..12]
4479        )]),
4480        "With search including ignored prettier directory and excluding TS files, only one file should be found"
4481    );
4482}
4483
4484#[test]
4485fn test_glob_literal_prefix() {
4486    assert_eq!(glob_literal_prefix("**/*.js"), "");
4487    assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4488    assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4489    assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4490}
4491
4492#[gpui::test]
4493async fn test_create_entry(cx: &mut gpui::TestAppContext) {
4494    init_test(cx);
4495
4496    let fs = FakeFs::new(cx.executor().clone());
4497    fs.insert_tree(
4498        "/one/two",
4499        json!({
4500            "three": {
4501                "a.txt": "",
4502                "four": {}
4503            },
4504            "c.rs": ""
4505        }),
4506    )
4507    .await;
4508
4509    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
4510    project
4511        .update(cx, |project, cx| {
4512            let id = project.worktrees().next().unwrap().read(cx).id();
4513            project.create_entry((id, "b.."), true, cx)
4514        })
4515        .unwrap()
4516        .await
4517        .to_included()
4518        .unwrap();
4519
4520    // Can't create paths outside the project
4521    let result = project
4522        .update(cx, |project, cx| {
4523            let id = project.worktrees().next().unwrap().read(cx).id();
4524            project.create_entry((id, "../../boop"), true, cx)
4525        })
4526        .await;
4527    assert!(result.is_err());
4528
4529    // Can't create paths with '..'
4530    let result = project
4531        .update(cx, |project, cx| {
4532            let id = project.worktrees().next().unwrap().read(cx).id();
4533            project.create_entry((id, "four/../beep"), true, cx)
4534        })
4535        .await;
4536    assert!(result.is_err());
4537
4538    assert_eq!(
4539        fs.paths(true),
4540        vec![
4541            PathBuf::from("/"),
4542            PathBuf::from("/one"),
4543            PathBuf::from("/one/two"),
4544            PathBuf::from("/one/two/c.rs"),
4545            PathBuf::from("/one/two/three"),
4546            PathBuf::from("/one/two/three/a.txt"),
4547            PathBuf::from("/one/two/three/b.."),
4548            PathBuf::from("/one/two/three/four"),
4549        ]
4550    );
4551
4552    // And we cannot open buffers with '..'
4553    let result = project
4554        .update(cx, |project, cx| {
4555            let id = project.worktrees().next().unwrap().read(cx).id();
4556            project.open_buffer((id, "../c.rs"), cx)
4557        })
4558        .await;
4559    assert!(result.is_err())
4560}
4561
4562#[gpui::test]
4563async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
4564    init_test(cx);
4565
4566    let fs = FakeFs::new(cx.executor());
4567    fs.insert_tree(
4568        "/dir",
4569        json!({
4570            "a.tsx": "a",
4571        }),
4572    )
4573    .await;
4574
4575    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4576
4577    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4578    language_registry.add(tsx_lang());
4579    let language_server_names = [
4580        "TypeScriptServer",
4581        "TailwindServer",
4582        "ESLintServer",
4583        "NoHoverCapabilitiesServer",
4584    ];
4585    let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4586        "tsx",
4587        true,
4588        FakeLspAdapter {
4589            name: &language_server_names[0],
4590            capabilities: lsp::ServerCapabilities {
4591                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4592                ..lsp::ServerCapabilities::default()
4593            },
4594            ..FakeLspAdapter::default()
4595        },
4596    );
4597    let _a = language_registry.register_specific_fake_lsp_adapter(
4598        "tsx",
4599        false,
4600        FakeLspAdapter {
4601            name: &language_server_names[1],
4602            capabilities: lsp::ServerCapabilities {
4603                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4604                ..lsp::ServerCapabilities::default()
4605            },
4606            ..FakeLspAdapter::default()
4607        },
4608    );
4609    let _b = language_registry.register_specific_fake_lsp_adapter(
4610        "tsx",
4611        false,
4612        FakeLspAdapter {
4613            name: &language_server_names[2],
4614            capabilities: lsp::ServerCapabilities {
4615                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4616                ..lsp::ServerCapabilities::default()
4617            },
4618            ..FakeLspAdapter::default()
4619        },
4620    );
4621    let _c = language_registry.register_specific_fake_lsp_adapter(
4622        "tsx",
4623        false,
4624        FakeLspAdapter {
4625            name: &language_server_names[3],
4626            capabilities: lsp::ServerCapabilities {
4627                hover_provider: None,
4628                ..lsp::ServerCapabilities::default()
4629            },
4630            ..FakeLspAdapter::default()
4631        },
4632    );
4633
4634    let buffer = project
4635        .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4636        .await
4637        .unwrap();
4638    cx.executor().run_until_parked();
4639
4640    let mut servers_with_hover_requests = HashMap::default();
4641    for i in 0..language_server_names.len() {
4642        let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4643            panic!(
4644                "Failed to get language server #{i} with name {}",
4645                &language_server_names[i]
4646            )
4647        });
4648        let new_server_name = new_server.server.name();
4649        assert!(
4650            !servers_with_hover_requests.contains_key(new_server_name),
4651            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4652        );
4653        let new_server_name = new_server_name.to_string();
4654        match new_server_name.as_str() {
4655            "TailwindServer" | "TypeScriptServer" => {
4656                servers_with_hover_requests.insert(
4657                    new_server_name.clone(),
4658                    new_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| {
4659                        let name = new_server_name.clone();
4660                        async move {
4661                            Ok(Some(lsp::Hover {
4662                                contents: lsp::HoverContents::Scalar(lsp::MarkedString::String(
4663                                    format!("{name} hover"),
4664                                )),
4665                                range: None,
4666                            }))
4667                        }
4668                    }),
4669                );
4670            }
4671            "ESLintServer" => {
4672                servers_with_hover_requests.insert(
4673                    new_server_name,
4674                    new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4675                        |_, _| async move { Ok(None) },
4676                    ),
4677                );
4678            }
4679            "NoHoverCapabilitiesServer" => {
4680                let _never_handled = new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4681                    |_, _| async move {
4682                        panic!(
4683                            "Should not call for hovers server with no corresponding capabilities"
4684                        )
4685                    },
4686                );
4687            }
4688            unexpected => panic!("Unexpected server name: {unexpected}"),
4689        }
4690    }
4691
4692    let hover_task = project.update(cx, |project, cx| {
4693        project.hover(&buffer, Point::new(0, 0), cx)
4694    });
4695    let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
4696        |mut hover_request| async move {
4697            hover_request
4698                .next()
4699                .await
4700                .expect("All hover requests should have been triggered")
4701        },
4702    ))
4703    .await;
4704    assert_eq!(
4705        vec!["TailwindServer hover", "TypeScriptServer hover"],
4706        hover_task
4707            .await
4708            .into_iter()
4709            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4710            .sorted()
4711            .collect::<Vec<_>>(),
4712        "Should receive hover responses from all related servers with hover capabilities"
4713    );
4714}
4715
4716#[gpui::test]
4717async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
4718    init_test(cx);
4719
4720    let fs = FakeFs::new(cx.executor());
4721    fs.insert_tree(
4722        "/dir",
4723        json!({
4724            "a.ts": "a",
4725        }),
4726    )
4727    .await;
4728
4729    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4730
4731    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4732    language_registry.add(typescript_lang());
4733    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
4734        "TypeScript",
4735        FakeLspAdapter {
4736            capabilities: lsp::ServerCapabilities {
4737                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4738                ..lsp::ServerCapabilities::default()
4739            },
4740            ..FakeLspAdapter::default()
4741        },
4742    );
4743
4744    let buffer = project
4745        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
4746        .await
4747        .unwrap();
4748    cx.executor().run_until_parked();
4749
4750    let fake_server = fake_language_servers
4751        .next()
4752        .await
4753        .expect("failed to get the language server");
4754
4755    let mut request_handled =
4756        fake_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
4757            Ok(Some(lsp::Hover {
4758                contents: lsp::HoverContents::Array(vec![
4759                    lsp::MarkedString::String("".to_string()),
4760                    lsp::MarkedString::String("      ".to_string()),
4761                    lsp::MarkedString::String("\n\n\n".to_string()),
4762                ]),
4763                range: None,
4764            }))
4765        });
4766
4767    let hover_task = project.update(cx, |project, cx| {
4768        project.hover(&buffer, Point::new(0, 0), cx)
4769    });
4770    let () = request_handled
4771        .next()
4772        .await
4773        .expect("All hover requests should have been triggered");
4774    assert_eq!(
4775        Vec::<String>::new(),
4776        hover_task
4777            .await
4778            .into_iter()
4779            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4780            .sorted()
4781            .collect::<Vec<_>>(),
4782        "Empty hover parts should be ignored"
4783    );
4784}
4785
4786#[gpui::test]
4787async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
4788    init_test(cx);
4789
4790    let fs = FakeFs::new(cx.executor());
4791    fs.insert_tree(
4792        "/dir",
4793        json!({
4794            "a.tsx": "a",
4795        }),
4796    )
4797    .await;
4798
4799    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4800
4801    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4802    language_registry.add(tsx_lang());
4803    let language_server_names = [
4804        "TypeScriptServer",
4805        "TailwindServer",
4806        "ESLintServer",
4807        "NoActionsCapabilitiesServer",
4808    ];
4809    let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4810        "tsx",
4811        true,
4812        FakeLspAdapter {
4813            name: &language_server_names[0],
4814            capabilities: lsp::ServerCapabilities {
4815                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4816                ..lsp::ServerCapabilities::default()
4817            },
4818            ..FakeLspAdapter::default()
4819        },
4820    );
4821    let _a = language_registry.register_specific_fake_lsp_adapter(
4822        "tsx",
4823        false,
4824        FakeLspAdapter {
4825            name: &language_server_names[1],
4826            capabilities: lsp::ServerCapabilities {
4827                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4828                ..lsp::ServerCapabilities::default()
4829            },
4830            ..FakeLspAdapter::default()
4831        },
4832    );
4833    let _b = language_registry.register_specific_fake_lsp_adapter(
4834        "tsx",
4835        false,
4836        FakeLspAdapter {
4837            name: &language_server_names[2],
4838            capabilities: lsp::ServerCapabilities {
4839                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4840                ..lsp::ServerCapabilities::default()
4841            },
4842            ..FakeLspAdapter::default()
4843        },
4844    );
4845    let _c = language_registry.register_specific_fake_lsp_adapter(
4846        "tsx",
4847        false,
4848        FakeLspAdapter {
4849            name: &language_server_names[3],
4850            capabilities: lsp::ServerCapabilities {
4851                code_action_provider: None,
4852                ..lsp::ServerCapabilities::default()
4853            },
4854            ..FakeLspAdapter::default()
4855        },
4856    );
4857
4858    let buffer = project
4859        .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4860        .await
4861        .unwrap();
4862    cx.executor().run_until_parked();
4863
4864    let mut servers_with_actions_requests = HashMap::default();
4865    for i in 0..language_server_names.len() {
4866        let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4867            panic!(
4868                "Failed to get language server #{i} with name {}",
4869                &language_server_names[i]
4870            )
4871        });
4872        let new_server_name = new_server.server.name();
4873        assert!(
4874            !servers_with_actions_requests.contains_key(new_server_name),
4875            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4876        );
4877        let new_server_name = new_server_name.to_string();
4878        match new_server_name.as_str() {
4879            "TailwindServer" | "TypeScriptServer" => {
4880                servers_with_actions_requests.insert(
4881                    new_server_name.clone(),
4882                    new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4883                        move |_, _| {
4884                            let name = new_server_name.clone();
4885                            async move {
4886                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4887                                    lsp::CodeAction {
4888                                        title: format!("{name} code action"),
4889                                        ..lsp::CodeAction::default()
4890                                    },
4891                                )]))
4892                            }
4893                        },
4894                    ),
4895                );
4896            }
4897            "ESLintServer" => {
4898                servers_with_actions_requests.insert(
4899                    new_server_name,
4900                    new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4901                        |_, _| async move { Ok(None) },
4902                    ),
4903                );
4904            }
4905            "NoActionsCapabilitiesServer" => {
4906                let _never_handled = new_server
4907                    .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
4908                        panic!(
4909                            "Should not call for code actions server with no corresponding capabilities"
4910                        )
4911                    });
4912            }
4913            unexpected => panic!("Unexpected server name: {unexpected}"),
4914        }
4915    }
4916
4917    let code_actions_task = project.update(cx, |project, cx| {
4918        project.code_actions(&buffer, 0..buffer.read(cx).len(), cx)
4919    });
4920    let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
4921        |mut code_actions_request| async move {
4922            code_actions_request
4923                .next()
4924                .await
4925                .expect("All code actions requests should have been triggered")
4926        },
4927    ))
4928    .await;
4929    assert_eq!(
4930        vec!["TailwindServer code action", "TypeScriptServer code action"],
4931        code_actions_task
4932            .await
4933            .into_iter()
4934            .map(|code_action| code_action.lsp_action.title)
4935            .sorted()
4936            .collect::<Vec<_>>(),
4937        "Should receive code actions responses from all related servers with hover capabilities"
4938    );
4939}
4940
4941#[gpui::test]
4942async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
4943    init_test(cx);
4944
4945    let fs = FakeFs::new(cx.executor());
4946    fs.insert_tree(
4947        "/dir",
4948        json!({
4949            "a.rs": "let a = 1;",
4950            "b.rs": "let b = 2;",
4951            "c.rs": "let c = 2;",
4952        }),
4953    )
4954    .await;
4955
4956    let project = Project::test(
4957        fs,
4958        [
4959            "/dir/a.rs".as_ref(),
4960            "/dir/b.rs".as_ref(),
4961            "/dir/c.rs".as_ref(),
4962        ],
4963        cx,
4964    )
4965    .await;
4966
4967    // check the initial state and get the worktrees
4968    let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
4969        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
4970        assert_eq!(worktrees.len(), 3);
4971
4972        let worktree_a = worktrees[0].read(cx);
4973        let worktree_b = worktrees[1].read(cx);
4974        let worktree_c = worktrees[2].read(cx);
4975
4976        // check they start in the right order
4977        assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
4978        assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
4979        assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
4980
4981        (
4982            worktrees[0].clone(),
4983            worktrees[1].clone(),
4984            worktrees[2].clone(),
4985        )
4986    });
4987
4988    // move first worktree to after the second
4989    // [a, b, c] -> [b, a, c]
4990    project
4991        .update(cx, |project, cx| {
4992            let first = worktree_a.read(cx);
4993            let second = worktree_b.read(cx);
4994            project.move_worktree(first.id(), second.id(), cx)
4995        })
4996        .expect("moving first after second");
4997
4998    // check the state after moving
4999    project.update(cx, |project, cx| {
5000        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5001        assert_eq!(worktrees.len(), 3);
5002
5003        let first = worktrees[0].read(cx);
5004        let second = worktrees[1].read(cx);
5005        let third = worktrees[2].read(cx);
5006
5007        // check they are now in the right order
5008        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5009        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
5010        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5011    });
5012
5013    // move the second worktree to before the first
5014    // [b, a, c] -> [a, b, c]
5015    project
5016        .update(cx, |project, cx| {
5017            let second = worktree_a.read(cx);
5018            let first = worktree_b.read(cx);
5019            project.move_worktree(first.id(), second.id(), cx)
5020        })
5021        .expect("moving second before first");
5022
5023    // check the state after moving
5024    project.update(cx, |project, cx| {
5025        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5026        assert_eq!(worktrees.len(), 3);
5027
5028        let first = worktrees[0].read(cx);
5029        let second = worktrees[1].read(cx);
5030        let third = worktrees[2].read(cx);
5031
5032        // check they are now in the right order
5033        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5034        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5035        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5036    });
5037
5038    // move the second worktree to after the third
5039    // [a, b, c] -> [a, c, b]
5040    project
5041        .update(cx, |project, cx| {
5042            let second = worktree_b.read(cx);
5043            let third = worktree_c.read(cx);
5044            project.move_worktree(second.id(), third.id(), cx)
5045        })
5046        .expect("moving second after third");
5047
5048    // check the state after moving
5049    project.update(cx, |project, cx| {
5050        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5051        assert_eq!(worktrees.len(), 3);
5052
5053        let first = worktrees[0].read(cx);
5054        let second = worktrees[1].read(cx);
5055        let third = worktrees[2].read(cx);
5056
5057        // check they are now in the right order
5058        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5059        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5060        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
5061    });
5062
5063    // move the third worktree to before the second
5064    // [a, c, b] -> [a, b, c]
5065    project
5066        .update(cx, |project, cx| {
5067            let third = worktree_c.read(cx);
5068            let second = worktree_b.read(cx);
5069            project.move_worktree(third.id(), second.id(), cx)
5070        })
5071        .expect("moving third before second");
5072
5073    // check the state after moving
5074    project.update(cx, |project, cx| {
5075        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5076        assert_eq!(worktrees.len(), 3);
5077
5078        let first = worktrees[0].read(cx);
5079        let second = worktrees[1].read(cx);
5080        let third = worktrees[2].read(cx);
5081
5082        // check they are now in the right order
5083        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5084        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5085        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5086    });
5087
5088    // move the first worktree to after the third
5089    // [a, b, c] -> [b, c, a]
5090    project
5091        .update(cx, |project, cx| {
5092            let first = worktree_a.read(cx);
5093            let third = worktree_c.read(cx);
5094            project.move_worktree(first.id(), third.id(), cx)
5095        })
5096        .expect("moving first after third");
5097
5098    // check the state after moving
5099    project.update(cx, |project, cx| {
5100        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5101        assert_eq!(worktrees.len(), 3);
5102
5103        let first = worktrees[0].read(cx);
5104        let second = worktrees[1].read(cx);
5105        let third = worktrees[2].read(cx);
5106
5107        // check they are now in the right order
5108        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5109        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5110        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
5111    });
5112
5113    // move the third worktree to before the first
5114    // [b, c, a] -> [a, b, c]
5115    project
5116        .update(cx, |project, cx| {
5117            let third = worktree_a.read(cx);
5118            let first = worktree_b.read(cx);
5119            project.move_worktree(third.id(), first.id(), cx)
5120        })
5121        .expect("moving third before first");
5122
5123    // check the state after moving
5124    project.update(cx, |project, cx| {
5125        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5126        assert_eq!(worktrees.len(), 3);
5127
5128        let first = worktrees[0].read(cx);
5129        let second = worktrees[1].read(cx);
5130        let third = worktrees[2].read(cx);
5131
5132        // check they are now in the right order
5133        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5134        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5135        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5136    });
5137}
5138
5139async fn search(
5140    project: &Model<Project>,
5141    query: SearchQuery,
5142    cx: &mut gpui::TestAppContext,
5143) -> Result<HashMap<String, Vec<Range<usize>>>> {
5144    let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
5145    let mut results = HashMap::default();
5146    while let Some(search_result) = search_rx.next().await {
5147        match search_result {
5148            SearchResult::Buffer { buffer, ranges } => {
5149                results.entry(buffer).or_insert(ranges);
5150            }
5151            SearchResult::LimitReached => {}
5152        }
5153    }
5154    Ok(results
5155        .into_iter()
5156        .map(|(buffer, ranges)| {
5157            buffer.update(cx, |buffer, cx| {
5158                let path = buffer
5159                    .file()
5160                    .unwrap()
5161                    .full_path(cx)
5162                    .to_string_lossy()
5163                    .to_string();
5164                let ranges = ranges
5165                    .into_iter()
5166                    .map(|range| range.to_offset(buffer))
5167                    .collect::<Vec<_>>();
5168                (path, ranges)
5169            })
5170        })
5171        .collect())
5172}
5173
5174fn init_test(cx: &mut gpui::TestAppContext) {
5175    if std::env::var("RUST_LOG").is_ok() {
5176        env_logger::try_init().ok();
5177    }
5178
5179    cx.update(|cx| {
5180        let settings_store = SettingsStore::test(cx);
5181        cx.set_global(settings_store);
5182        release_channel::init(SemanticVersion::default(), cx);
5183        language::init(cx);
5184        Project::init_settings(cx);
5185    });
5186}
5187
5188fn json_lang() -> Arc<Language> {
5189    Arc::new(Language::new(
5190        LanguageConfig {
5191            name: "JSON".into(),
5192            matcher: LanguageMatcher {
5193                path_suffixes: vec!["json".to_string()],
5194                ..Default::default()
5195            },
5196            ..Default::default()
5197        },
5198        None,
5199    ))
5200}
5201
5202fn js_lang() -> Arc<Language> {
5203    Arc::new(Language::new(
5204        LanguageConfig {
5205            name: Arc::from("JavaScript"),
5206            matcher: LanguageMatcher {
5207                path_suffixes: vec!["js".to_string()],
5208                ..Default::default()
5209            },
5210            ..Default::default()
5211        },
5212        None,
5213    ))
5214}
5215
5216fn rust_lang() -> Arc<Language> {
5217    Arc::new(Language::new(
5218        LanguageConfig {
5219            name: "Rust".into(),
5220            matcher: LanguageMatcher {
5221                path_suffixes: vec!["rs".to_string()],
5222                ..Default::default()
5223            },
5224            ..Default::default()
5225        },
5226        Some(tree_sitter_rust::language()),
5227    ))
5228}
5229
5230fn typescript_lang() -> Arc<Language> {
5231    Arc::new(Language::new(
5232        LanguageConfig {
5233            name: "TypeScript".into(),
5234            matcher: LanguageMatcher {
5235                path_suffixes: vec!["ts".to_string()],
5236                ..Default::default()
5237            },
5238            ..Default::default()
5239        },
5240        Some(tree_sitter_typescript::language_typescript()),
5241    ))
5242}
5243
5244fn tsx_lang() -> Arc<Language> {
5245    Arc::new(Language::new(
5246        LanguageConfig {
5247            name: "tsx".into(),
5248            matcher: LanguageMatcher {
5249                path_suffixes: vec!["tsx".to_string()],
5250                ..Default::default()
5251            },
5252            ..Default::default()
5253        },
5254        Some(tree_sitter_typescript::language_tsx()),
5255    ))
5256}
5257
5258fn get_all_tasks(
5259    project: &Model<Project>,
5260    worktree_id: Option<WorktreeId>,
5261    task_context: &TaskContext,
5262    cx: &mut AppContext,
5263) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
5264    let resolved_tasks = project.update(cx, |project, cx| {
5265        project
5266            .task_inventory()
5267            .read(cx)
5268            .used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx)
5269    });
5270
5271    cx.spawn(|_| async move {
5272        let (mut old, new) = resolved_tasks.await;
5273        old.extend(new);
5274        old
5275    })
5276}