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// This test is currently disabled on Linux as it fails fails pretty consistently on that target.
2981#[cfg(not(target_os = "linux"))]
2982#[gpui::test(retries = 5)]
2983async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
2984    use worktree::WorktreeModelHandle as _;
2985
2986    init_test(cx);
2987    cx.executor().allow_parking();
2988
2989    let dir = temp_tree(json!({
2990        "a": {
2991            "file1": "",
2992            "file2": "",
2993            "file3": "",
2994        },
2995        "b": {
2996            "c": {
2997                "file4": "",
2998                "file5": "",
2999            }
3000        }
3001    }));
3002
3003    let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
3004
3005    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3006        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
3007        async move { buffer.await.unwrap() }
3008    };
3009    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3010        project.update(cx, |project, cx| {
3011            let tree = project.worktrees().next().unwrap();
3012            tree.read(cx)
3013                .entry_for_path(path)
3014                .unwrap_or_else(|| panic!("no entry for path {}", path))
3015                .id
3016        })
3017    };
3018
3019    let buffer2 = buffer_for_path("a/file2", cx).await;
3020    let buffer3 = buffer_for_path("a/file3", cx).await;
3021    let buffer4 = buffer_for_path("b/c/file4", cx).await;
3022    let buffer5 = buffer_for_path("b/c/file5", cx).await;
3023
3024    let file2_id = id_for_path("a/file2", cx);
3025    let file3_id = id_for_path("a/file3", cx);
3026    let file4_id = id_for_path("b/c/file4", cx);
3027
3028    // Create a remote copy of this worktree.
3029    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3030    let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
3031
3032    let updates = Arc::new(Mutex::new(Vec::new()));
3033    tree.update(cx, |tree, cx| {
3034        let updates = updates.clone();
3035        tree.observe_updates(0, cx, move |update| {
3036            updates.lock().push(update);
3037            async { true }
3038        });
3039    });
3040
3041    let remote = cx.update(|cx| {
3042        Worktree::remote(
3043            0,
3044            1,
3045            metadata,
3046            Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
3047            cx,
3048        )
3049    });
3050
3051    cx.executor().run_until_parked();
3052
3053    cx.update(|cx| {
3054        assert!(!buffer2.read(cx).is_dirty());
3055        assert!(!buffer3.read(cx).is_dirty());
3056        assert!(!buffer4.read(cx).is_dirty());
3057        assert!(!buffer5.read(cx).is_dirty());
3058    });
3059
3060    // Rename and delete files and directories.
3061    tree.flush_fs_events(cx).await;
3062    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
3063    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
3064    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
3065    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
3066    tree.flush_fs_events(cx).await;
3067
3068    let expected_paths = vec![
3069        "a",
3070        "a/file1",
3071        "a/file2.new",
3072        "b",
3073        "d",
3074        "d/file3",
3075        "d/file4",
3076    ];
3077
3078    cx.update(|app| {
3079        assert_eq!(
3080            tree.read(app)
3081                .paths()
3082                .map(|p| p.to_str().unwrap())
3083                .collect::<Vec<_>>(),
3084            expected_paths
3085        );
3086    });
3087
3088    assert_eq!(id_for_path("a/file2.new", cx), file2_id);
3089    assert_eq!(id_for_path("d/file3", cx), file3_id);
3090    assert_eq!(id_for_path("d/file4", cx), file4_id);
3091
3092    cx.update(|cx| {
3093        assert_eq!(
3094            buffer2.read(cx).file().unwrap().path().as_ref(),
3095            Path::new("a/file2.new")
3096        );
3097        assert_eq!(
3098            buffer3.read(cx).file().unwrap().path().as_ref(),
3099            Path::new("d/file3")
3100        );
3101        assert_eq!(
3102            buffer4.read(cx).file().unwrap().path().as_ref(),
3103            Path::new("d/file4")
3104        );
3105        assert_eq!(
3106            buffer5.read(cx).file().unwrap().path().as_ref(),
3107            Path::new("b/c/file5")
3108        );
3109
3110        assert!(!buffer2.read(cx).file().unwrap().is_deleted());
3111        assert!(!buffer3.read(cx).file().unwrap().is_deleted());
3112        assert!(!buffer4.read(cx).file().unwrap().is_deleted());
3113        assert!(buffer5.read(cx).file().unwrap().is_deleted());
3114    });
3115
3116    // Update the remote worktree. Check that it becomes consistent with the
3117    // local worktree.
3118    cx.executor().run_until_parked();
3119
3120    remote.update(cx, |remote, _| {
3121        for update in updates.lock().drain(..) {
3122            remote.as_remote_mut().unwrap().update_from_remote(update);
3123        }
3124    });
3125    cx.executor().run_until_parked();
3126    remote.update(cx, |remote, _| {
3127        assert_eq!(
3128            remote
3129                .paths()
3130                .map(|p| p.to_str().unwrap())
3131                .collect::<Vec<_>>(),
3132            expected_paths
3133        );
3134    });
3135}
3136
3137#[gpui::test(iterations = 10)]
3138async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
3139    init_test(cx);
3140
3141    let fs = FakeFs::new(cx.executor());
3142    fs.insert_tree(
3143        "/dir",
3144        json!({
3145            "a": {
3146                "file1": "",
3147            }
3148        }),
3149    )
3150    .await;
3151
3152    let project = Project::test(fs, [Path::new("/dir")], cx).await;
3153    let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3154    let tree_id = tree.update(cx, |tree, _| tree.id());
3155
3156    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3157        project.update(cx, |project, cx| {
3158            let tree = project.worktrees().next().unwrap();
3159            tree.read(cx)
3160                .entry_for_path(path)
3161                .unwrap_or_else(|| panic!("no entry for path {}", path))
3162                .id
3163        })
3164    };
3165
3166    let dir_id = id_for_path("a", cx);
3167    let file_id = id_for_path("a/file1", cx);
3168    let buffer = project
3169        .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
3170        .await
3171        .unwrap();
3172    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3173
3174    project
3175        .update(cx, |project, cx| {
3176            project.rename_entry(dir_id, Path::new("b"), cx)
3177        })
3178        .unwrap()
3179        .await
3180        .to_included()
3181        .unwrap();
3182    cx.executor().run_until_parked();
3183
3184    assert_eq!(id_for_path("b", cx), dir_id);
3185    assert_eq!(id_for_path("b/file1", cx), file_id);
3186    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3187}
3188
3189#[gpui::test]
3190async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
3191    init_test(cx);
3192
3193    let fs = FakeFs::new(cx.executor());
3194    fs.insert_tree(
3195        "/dir",
3196        json!({
3197            "a.txt": "a-contents",
3198            "b.txt": "b-contents",
3199        }),
3200    )
3201    .await;
3202
3203    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3204
3205    // Spawn multiple tasks to open paths, repeating some paths.
3206    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
3207        (
3208            p.open_local_buffer("/dir/a.txt", cx),
3209            p.open_local_buffer("/dir/b.txt", cx),
3210            p.open_local_buffer("/dir/a.txt", cx),
3211        )
3212    });
3213
3214    let buffer_a_1 = buffer_a_1.await.unwrap();
3215    let buffer_a_2 = buffer_a_2.await.unwrap();
3216    let buffer_b = buffer_b.await.unwrap();
3217    assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
3218    assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
3219
3220    // There is only one buffer per path.
3221    let buffer_a_id = buffer_a_1.entity_id();
3222    assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
3223
3224    // Open the same path again while it is still open.
3225    drop(buffer_a_1);
3226    let buffer_a_3 = project
3227        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
3228        .await
3229        .unwrap();
3230
3231    // There's still only one buffer per path.
3232    assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
3233}
3234
3235#[gpui::test]
3236async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
3237    init_test(cx);
3238
3239    let fs = FakeFs::new(cx.executor());
3240    fs.insert_tree(
3241        "/dir",
3242        json!({
3243            "file1": "abc",
3244            "file2": "def",
3245            "file3": "ghi",
3246        }),
3247    )
3248    .await;
3249
3250    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3251
3252    let buffer1 = project
3253        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3254        .await
3255        .unwrap();
3256    let events = Arc::new(Mutex::new(Vec::new()));
3257
3258    // initially, the buffer isn't dirty.
3259    buffer1.update(cx, |buffer, cx| {
3260        cx.subscribe(&buffer1, {
3261            let events = events.clone();
3262            move |_, _, event, _| match event {
3263                BufferEvent::Operation(_) => {}
3264                _ => events.lock().push(event.clone()),
3265            }
3266        })
3267        .detach();
3268
3269        assert!(!buffer.is_dirty());
3270        assert!(events.lock().is_empty());
3271
3272        buffer.edit([(1..2, "")], None, cx);
3273    });
3274
3275    // after the first edit, the buffer is dirty, and emits a dirtied event.
3276    buffer1.update(cx, |buffer, cx| {
3277        assert!(buffer.text() == "ac");
3278        assert!(buffer.is_dirty());
3279        assert_eq!(
3280            *events.lock(),
3281            &[language::Event::Edited, language::Event::DirtyChanged]
3282        );
3283        events.lock().clear();
3284        buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
3285    });
3286
3287    // after saving, the buffer is not dirty, and emits a saved event.
3288    buffer1.update(cx, |buffer, cx| {
3289        assert!(!buffer.is_dirty());
3290        assert_eq!(*events.lock(), &[language::Event::Saved]);
3291        events.lock().clear();
3292
3293        buffer.edit([(1..1, "B")], None, cx);
3294        buffer.edit([(2..2, "D")], None, cx);
3295    });
3296
3297    // after editing again, the buffer is dirty, and emits another dirty event.
3298    buffer1.update(cx, |buffer, cx| {
3299        assert!(buffer.text() == "aBDc");
3300        assert!(buffer.is_dirty());
3301        assert_eq!(
3302            *events.lock(),
3303            &[
3304                language::Event::Edited,
3305                language::Event::DirtyChanged,
3306                language::Event::Edited,
3307            ],
3308        );
3309        events.lock().clear();
3310
3311        // After restoring the buffer to its previously-saved state,
3312        // the buffer is not considered dirty anymore.
3313        buffer.edit([(1..3, "")], None, cx);
3314        assert!(buffer.text() == "ac");
3315        assert!(!buffer.is_dirty());
3316    });
3317
3318    assert_eq!(
3319        *events.lock(),
3320        &[language::Event::Edited, language::Event::DirtyChanged]
3321    );
3322
3323    // When a file is deleted, the buffer is considered dirty.
3324    let events = Arc::new(Mutex::new(Vec::new()));
3325    let buffer2 = project
3326        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3327        .await
3328        .unwrap();
3329    buffer2.update(cx, |_, cx| {
3330        cx.subscribe(&buffer2, {
3331            let events = events.clone();
3332            move |_, _, event, _| events.lock().push(event.clone())
3333        })
3334        .detach();
3335    });
3336
3337    fs.remove_file("/dir/file2".as_ref(), Default::default())
3338        .await
3339        .unwrap();
3340    cx.executor().run_until_parked();
3341    buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3342    assert_eq!(
3343        *events.lock(),
3344        &[
3345            language::Event::DirtyChanged,
3346            language::Event::FileHandleChanged
3347        ]
3348    );
3349
3350    // When a file is already dirty when deleted, we don't emit a Dirtied event.
3351    let events = Arc::new(Mutex::new(Vec::new()));
3352    let buffer3 = project
3353        .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3354        .await
3355        .unwrap();
3356    buffer3.update(cx, |_, cx| {
3357        cx.subscribe(&buffer3, {
3358            let events = events.clone();
3359            move |_, _, event, _| events.lock().push(event.clone())
3360        })
3361        .detach();
3362    });
3363
3364    buffer3.update(cx, |buffer, cx| {
3365        buffer.edit([(0..0, "x")], None, cx);
3366    });
3367    events.lock().clear();
3368    fs.remove_file("/dir/file3".as_ref(), Default::default())
3369        .await
3370        .unwrap();
3371    cx.executor().run_until_parked();
3372    assert_eq!(*events.lock(), &[language::Event::FileHandleChanged]);
3373    cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3374}
3375
3376#[gpui::test]
3377async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
3378    init_test(cx);
3379
3380    let initial_contents = "aaa\nbbbbb\nc\n";
3381    let fs = FakeFs::new(cx.executor());
3382    fs.insert_tree(
3383        "/dir",
3384        json!({
3385            "the-file": initial_contents,
3386        }),
3387    )
3388    .await;
3389    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3390    let buffer = project
3391        .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3392        .await
3393        .unwrap();
3394
3395    let anchors = (0..3)
3396        .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3397        .collect::<Vec<_>>();
3398
3399    // Change the file on disk, adding two new lines of text, and removing
3400    // one line.
3401    buffer.update(cx, |buffer, _| {
3402        assert!(!buffer.is_dirty());
3403        assert!(!buffer.has_conflict());
3404    });
3405    let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3406    fs.save(
3407        "/dir/the-file".as_ref(),
3408        &new_contents.into(),
3409        LineEnding::Unix,
3410    )
3411    .await
3412    .unwrap();
3413
3414    // Because the buffer was not modified, it is reloaded from disk. Its
3415    // contents are edited according to the diff between the old and new
3416    // file contents.
3417    cx.executor().run_until_parked();
3418    buffer.update(cx, |buffer, _| {
3419        assert_eq!(buffer.text(), new_contents);
3420        assert!(!buffer.is_dirty());
3421        assert!(!buffer.has_conflict());
3422
3423        let anchor_positions = anchors
3424            .iter()
3425            .map(|anchor| anchor.to_point(&*buffer))
3426            .collect::<Vec<_>>();
3427        assert_eq!(
3428            anchor_positions,
3429            [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3430        );
3431    });
3432
3433    // Modify the buffer
3434    buffer.update(cx, |buffer, cx| {
3435        buffer.edit([(0..0, " ")], None, cx);
3436        assert!(buffer.is_dirty());
3437        assert!(!buffer.has_conflict());
3438    });
3439
3440    // Change the file on disk again, adding blank lines to the beginning.
3441    fs.save(
3442        "/dir/the-file".as_ref(),
3443        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3444        LineEnding::Unix,
3445    )
3446    .await
3447    .unwrap();
3448
3449    // Because the buffer is modified, it doesn't reload from disk, but is
3450    // marked as having a conflict.
3451    cx.executor().run_until_parked();
3452    buffer.update(cx, |buffer, _| {
3453        assert!(buffer.has_conflict());
3454    });
3455}
3456
3457#[gpui::test]
3458async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
3459    init_test(cx);
3460
3461    let fs = FakeFs::new(cx.executor());
3462    fs.insert_tree(
3463        "/dir",
3464        json!({
3465            "file1": "a\nb\nc\n",
3466            "file2": "one\r\ntwo\r\nthree\r\n",
3467        }),
3468    )
3469    .await;
3470
3471    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3472    let buffer1 = project
3473        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3474        .await
3475        .unwrap();
3476    let buffer2 = project
3477        .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3478        .await
3479        .unwrap();
3480
3481    buffer1.update(cx, |buffer, _| {
3482        assert_eq!(buffer.text(), "a\nb\nc\n");
3483        assert_eq!(buffer.line_ending(), LineEnding::Unix);
3484    });
3485    buffer2.update(cx, |buffer, _| {
3486        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3487        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3488    });
3489
3490    // Change a file's line endings on disk from unix to windows. The buffer's
3491    // state updates correctly.
3492    fs.save(
3493        "/dir/file1".as_ref(),
3494        &"aaa\nb\nc\n".into(),
3495        LineEnding::Windows,
3496    )
3497    .await
3498    .unwrap();
3499    cx.executor().run_until_parked();
3500    buffer1.update(cx, |buffer, _| {
3501        assert_eq!(buffer.text(), "aaa\nb\nc\n");
3502        assert_eq!(buffer.line_ending(), LineEnding::Windows);
3503    });
3504
3505    // Save a file with windows line endings. The file is written correctly.
3506    buffer2.update(cx, |buffer, cx| {
3507        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3508    });
3509    project
3510        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3511        .await
3512        .unwrap();
3513    assert_eq!(
3514        fs.load("/dir/file2".as_ref()).await.unwrap(),
3515        "one\r\ntwo\r\nthree\r\nfour\r\n",
3516    );
3517}
3518
3519#[gpui::test]
3520async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
3521    init_test(cx);
3522
3523    let fs = FakeFs::new(cx.executor());
3524    fs.insert_tree(
3525        "/the-dir",
3526        json!({
3527            "a.rs": "
3528                fn foo(mut v: Vec<usize>) {
3529                    for x in &v {
3530                        v.push(1);
3531                    }
3532                }
3533            "
3534            .unindent(),
3535        }),
3536    )
3537    .await;
3538
3539    let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3540    let buffer = project
3541        .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3542        .await
3543        .unwrap();
3544
3545    let buffer_uri = Uri::from_file_path("/the-dir/a.rs").unwrap();
3546    let message = lsp::PublishDiagnosticsParams {
3547        uri: buffer_uri.clone().into(),
3548        diagnostics: vec![
3549            lsp::Diagnostic {
3550                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3551                severity: Some(DiagnosticSeverity::WARNING),
3552                message: "error 1".to_string(),
3553                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3554                    location: lsp::Location {
3555                        uri: buffer_uri.clone().into(),
3556                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3557                    },
3558                    message: "error 1 hint 1".to_string(),
3559                }]),
3560                ..Default::default()
3561            },
3562            lsp::Diagnostic {
3563                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3564                severity: Some(DiagnosticSeverity::HINT),
3565                message: "error 1 hint 1".to_string(),
3566                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3567                    location: lsp::Location {
3568                        uri: buffer_uri.clone().into(),
3569                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3570                    },
3571                    message: "original diagnostic".to_string(),
3572                }]),
3573                ..Default::default()
3574            },
3575            lsp::Diagnostic {
3576                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3577                severity: Some(DiagnosticSeverity::ERROR),
3578                message: "error 2".to_string(),
3579                related_information: Some(vec![
3580                    lsp::DiagnosticRelatedInformation {
3581                        location: lsp::Location {
3582                            uri: buffer_uri.clone().into(),
3583                            range: lsp::Range::new(
3584                                lsp::Position::new(1, 13),
3585                                lsp::Position::new(1, 15),
3586                            ),
3587                        },
3588                        message: "error 2 hint 1".to_string(),
3589                    },
3590                    lsp::DiagnosticRelatedInformation {
3591                        location: lsp::Location {
3592                            uri: buffer_uri.clone().into(),
3593                            range: lsp::Range::new(
3594                                lsp::Position::new(1, 13),
3595                                lsp::Position::new(1, 15),
3596                            ),
3597                        },
3598                        message: "error 2 hint 2".to_string(),
3599                    },
3600                ]),
3601                ..Default::default()
3602            },
3603            lsp::Diagnostic {
3604                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3605                severity: Some(DiagnosticSeverity::HINT),
3606                message: "error 2 hint 1".to_string(),
3607                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3608                    location: lsp::Location {
3609                        uri: buffer_uri.clone().into(),
3610                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3611                    },
3612                    message: "original diagnostic".to_string(),
3613                }]),
3614                ..Default::default()
3615            },
3616            lsp::Diagnostic {
3617                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3618                severity: Some(DiagnosticSeverity::HINT),
3619                message: "error 2 hint 2".to_string(),
3620                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3621                    location: lsp::Location {
3622                        uri: buffer_uri.into(),
3623                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3624                    },
3625                    message: "original diagnostic".to_string(),
3626                }]),
3627                ..Default::default()
3628            },
3629        ],
3630        version: None,
3631    };
3632
3633    project
3634        .update(cx, |p, cx| {
3635            p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3636        })
3637        .unwrap();
3638    let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3639
3640    assert_eq!(
3641        buffer
3642            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3643            .collect::<Vec<_>>(),
3644        &[
3645            DiagnosticEntry {
3646                range: Point::new(1, 8)..Point::new(1, 9),
3647                diagnostic: Diagnostic {
3648                    severity: DiagnosticSeverity::WARNING,
3649                    message: "error 1".to_string(),
3650                    group_id: 1,
3651                    is_primary: true,
3652                    ..Default::default()
3653                }
3654            },
3655            DiagnosticEntry {
3656                range: Point::new(1, 8)..Point::new(1, 9),
3657                diagnostic: Diagnostic {
3658                    severity: DiagnosticSeverity::HINT,
3659                    message: "error 1 hint 1".to_string(),
3660                    group_id: 1,
3661                    is_primary: false,
3662                    ..Default::default()
3663                }
3664            },
3665            DiagnosticEntry {
3666                range: Point::new(1, 13)..Point::new(1, 15),
3667                diagnostic: Diagnostic {
3668                    severity: DiagnosticSeverity::HINT,
3669                    message: "error 2 hint 1".to_string(),
3670                    group_id: 0,
3671                    is_primary: false,
3672                    ..Default::default()
3673                }
3674            },
3675            DiagnosticEntry {
3676                range: Point::new(1, 13)..Point::new(1, 15),
3677                diagnostic: Diagnostic {
3678                    severity: DiagnosticSeverity::HINT,
3679                    message: "error 2 hint 2".to_string(),
3680                    group_id: 0,
3681                    is_primary: false,
3682                    ..Default::default()
3683                }
3684            },
3685            DiagnosticEntry {
3686                range: Point::new(2, 8)..Point::new(2, 17),
3687                diagnostic: Diagnostic {
3688                    severity: DiagnosticSeverity::ERROR,
3689                    message: "error 2".to_string(),
3690                    group_id: 0,
3691                    is_primary: true,
3692                    ..Default::default()
3693                }
3694            }
3695        ]
3696    );
3697
3698    assert_eq!(
3699        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3700        &[
3701            DiagnosticEntry {
3702                range: Point::new(1, 13)..Point::new(1, 15),
3703                diagnostic: Diagnostic {
3704                    severity: DiagnosticSeverity::HINT,
3705                    message: "error 2 hint 1".to_string(),
3706                    group_id: 0,
3707                    is_primary: false,
3708                    ..Default::default()
3709                }
3710            },
3711            DiagnosticEntry {
3712                range: Point::new(1, 13)..Point::new(1, 15),
3713                diagnostic: Diagnostic {
3714                    severity: DiagnosticSeverity::HINT,
3715                    message: "error 2 hint 2".to_string(),
3716                    group_id: 0,
3717                    is_primary: false,
3718                    ..Default::default()
3719                }
3720            },
3721            DiagnosticEntry {
3722                range: Point::new(2, 8)..Point::new(2, 17),
3723                diagnostic: Diagnostic {
3724                    severity: DiagnosticSeverity::ERROR,
3725                    message: "error 2".to_string(),
3726                    group_id: 0,
3727                    is_primary: true,
3728                    ..Default::default()
3729                }
3730            }
3731        ]
3732    );
3733
3734    assert_eq!(
3735        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3736        &[
3737            DiagnosticEntry {
3738                range: Point::new(1, 8)..Point::new(1, 9),
3739                diagnostic: Diagnostic {
3740                    severity: DiagnosticSeverity::WARNING,
3741                    message: "error 1".to_string(),
3742                    group_id: 1,
3743                    is_primary: true,
3744                    ..Default::default()
3745                }
3746            },
3747            DiagnosticEntry {
3748                range: Point::new(1, 8)..Point::new(1, 9),
3749                diagnostic: Diagnostic {
3750                    severity: DiagnosticSeverity::HINT,
3751                    message: "error 1 hint 1".to_string(),
3752                    group_id: 1,
3753                    is_primary: false,
3754                    ..Default::default()
3755                }
3756            },
3757        ]
3758    );
3759}
3760
3761#[gpui::test]
3762async fn test_rename(cx: &mut gpui::TestAppContext) {
3763    init_test(cx);
3764
3765    let fs = FakeFs::new(cx.executor());
3766    fs.insert_tree(
3767        "/dir",
3768        json!({
3769            "one.rs": "const ONE: usize = 1;",
3770            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3771        }),
3772    )
3773    .await;
3774
3775    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3776
3777    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3778    language_registry.add(rust_lang());
3779    let mut fake_servers = language_registry.register_fake_lsp_adapter(
3780        "Rust",
3781        FakeLspAdapter {
3782            capabilities: lsp::ServerCapabilities {
3783                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3784                    prepare_provider: Some(true),
3785                    work_done_progress_options: Default::default(),
3786                })),
3787                ..Default::default()
3788            },
3789            ..Default::default()
3790        },
3791    );
3792
3793    let buffer = project
3794        .update(cx, |project, cx| {
3795            project.open_local_buffer("/dir/one.rs", cx)
3796        })
3797        .await
3798        .unwrap();
3799
3800    let fake_server = fake_servers.next().await.unwrap();
3801
3802    let response = project.update(cx, |project, cx| {
3803        project.prepare_rename(buffer.clone(), 7, cx)
3804    });
3805    fake_server
3806        .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3807            assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3808            assert_eq!(params.position, lsp::Position::new(0, 7));
3809            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3810                lsp::Position::new(0, 6),
3811                lsp::Position::new(0, 9),
3812            ))))
3813        })
3814        .next()
3815        .await
3816        .unwrap();
3817    let range = response.await.unwrap().unwrap();
3818    let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3819    assert_eq!(range, 6..9);
3820
3821    let response = project.update(cx, |project, cx| {
3822        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3823    });
3824    fake_server
3825        .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3826            assert_eq!(
3827                params.text_document_position.text_document.uri.as_str(),
3828                "file:///dir/one.rs"
3829            );
3830            assert_eq!(
3831                params.text_document_position.position,
3832                lsp::Position::new(0, 7)
3833            );
3834            assert_eq!(params.new_name, "THREE");
3835            Ok(Some(lsp::WorkspaceEdit {
3836                changes: Some(
3837                    [
3838                        (
3839                            lsp::Uri::from_file_path("/dir/one.rs").unwrap().into(),
3840                            vec![lsp::TextEdit::new(
3841                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3842                                "THREE".to_string(),
3843                            )],
3844                        ),
3845                        (
3846                            lsp::Uri::from_file_path("/dir/two.rs").unwrap().into(),
3847                            vec![
3848                                lsp::TextEdit::new(
3849                                    lsp::Range::new(
3850                                        lsp::Position::new(0, 24),
3851                                        lsp::Position::new(0, 27),
3852                                    ),
3853                                    "THREE".to_string(),
3854                                ),
3855                                lsp::TextEdit::new(
3856                                    lsp::Range::new(
3857                                        lsp::Position::new(0, 35),
3858                                        lsp::Position::new(0, 38),
3859                                    ),
3860                                    "THREE".to_string(),
3861                                ),
3862                            ],
3863                        ),
3864                    ]
3865                    .into_iter()
3866                    .collect(),
3867                ),
3868                ..Default::default()
3869            }))
3870        })
3871        .next()
3872        .await
3873        .unwrap();
3874    let mut transaction = response.await.unwrap().0;
3875    assert_eq!(transaction.len(), 2);
3876    assert_eq!(
3877        transaction
3878            .remove_entry(&buffer)
3879            .unwrap()
3880            .0
3881            .update(cx, |buffer, _| buffer.text()),
3882        "const THREE: usize = 1;"
3883    );
3884    assert_eq!(
3885        transaction
3886            .into_keys()
3887            .next()
3888            .unwrap()
3889            .update(cx, |buffer, _| buffer.text()),
3890        "const TWO: usize = one::THREE + one::THREE;"
3891    );
3892}
3893
3894#[gpui::test]
3895async fn test_search(cx: &mut gpui::TestAppContext) {
3896    init_test(cx);
3897
3898    let fs = FakeFs::new(cx.executor());
3899    fs.insert_tree(
3900        "/dir",
3901        json!({
3902            "one.rs": "const ONE: usize = 1;",
3903            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3904            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3905            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3906        }),
3907    )
3908    .await;
3909    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3910    assert_eq!(
3911        search(
3912            &project,
3913            SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3914            cx
3915        )
3916        .await
3917        .unwrap(),
3918        HashMap::from_iter([
3919            ("dir/two.rs".to_string(), vec![6..9]),
3920            ("dir/three.rs".to_string(), vec![37..40])
3921        ])
3922    );
3923
3924    let buffer_4 = project
3925        .update(cx, |project, cx| {
3926            project.open_local_buffer("/dir/four.rs", cx)
3927        })
3928        .await
3929        .unwrap();
3930    buffer_4.update(cx, |buffer, cx| {
3931        let text = "two::TWO";
3932        buffer.edit([(20..28, text), (31..43, text)], None, cx);
3933    });
3934
3935    assert_eq!(
3936        search(
3937            &project,
3938            SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3939            cx
3940        )
3941        .await
3942        .unwrap(),
3943        HashMap::from_iter([
3944            ("dir/two.rs".to_string(), vec![6..9]),
3945            ("dir/three.rs".to_string(), vec![37..40]),
3946            ("dir/four.rs".to_string(), vec![25..28, 36..39])
3947        ])
3948    );
3949}
3950
3951#[gpui::test]
3952async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
3953    init_test(cx);
3954
3955    let search_query = "file";
3956
3957    let fs = FakeFs::new(cx.executor());
3958    fs.insert_tree(
3959        "/dir",
3960        json!({
3961            "one.rs": r#"// Rust file one"#,
3962            "one.ts": r#"// TypeScript file one"#,
3963            "two.rs": r#"// Rust file two"#,
3964            "two.ts": r#"// TypeScript file two"#,
3965        }),
3966    )
3967    .await;
3968    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3969
3970    assert!(
3971        search(
3972            &project,
3973            SearchQuery::text(
3974                search_query,
3975                false,
3976                true,
3977                false,
3978                vec![PathMatcher::new("*.odd").unwrap()],
3979                Vec::new()
3980            )
3981            .unwrap(),
3982            cx
3983        )
3984        .await
3985        .unwrap()
3986        .is_empty(),
3987        "If no inclusions match, no files should be returned"
3988    );
3989
3990    assert_eq!(
3991        search(
3992            &project,
3993            SearchQuery::text(
3994                search_query,
3995                false,
3996                true,
3997                false,
3998                vec![PathMatcher::new("*.rs").unwrap()],
3999                Vec::new()
4000            )
4001            .unwrap(),
4002            cx
4003        )
4004        .await
4005        .unwrap(),
4006        HashMap::from_iter([
4007            ("dir/one.rs".to_string(), vec![8..12]),
4008            ("dir/two.rs".to_string(), vec![8..12]),
4009        ]),
4010        "Rust only search should give only Rust files"
4011    );
4012
4013    assert_eq!(
4014        search(
4015            &project,
4016            SearchQuery::text(
4017                search_query,
4018                false,
4019                true,
4020                false,
4021                vec![
4022                    PathMatcher::new("*.ts").unwrap(),
4023                    PathMatcher::new("*.odd").unwrap(),
4024                ],
4025                Vec::new()
4026            ).unwrap(),
4027            cx
4028        )
4029        .await
4030        .unwrap(),
4031        HashMap::from_iter([
4032            ("dir/one.ts".to_string(), vec![14..18]),
4033            ("dir/two.ts".to_string(), vec![14..18]),
4034        ]),
4035        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
4036    );
4037
4038    assert_eq!(
4039        search(
4040            &project,
4041            SearchQuery::text(
4042                search_query,
4043                false,
4044                true,
4045                false,
4046                vec![
4047                    PathMatcher::new("*.rs").unwrap(),
4048                    PathMatcher::new("*.ts").unwrap(),
4049                    PathMatcher::new("*.odd").unwrap(),
4050                ],
4051                Vec::new()
4052            ).unwrap(),
4053            cx
4054        )
4055        .await
4056        .unwrap(),
4057        HashMap::from_iter([
4058            ("dir/two.ts".to_string(), vec![14..18]),
4059            ("dir/one.rs".to_string(), vec![8..12]),
4060            ("dir/one.ts".to_string(), vec![14..18]),
4061            ("dir/two.rs".to_string(), vec![8..12]),
4062        ]),
4063        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
4064    );
4065}
4066
4067#[gpui::test]
4068async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
4069    init_test(cx);
4070
4071    let search_query = "file";
4072
4073    let fs = FakeFs::new(cx.executor());
4074    fs.insert_tree(
4075        "/dir",
4076        json!({
4077            "one.rs": r#"// Rust file one"#,
4078            "one.ts": r#"// TypeScript file one"#,
4079            "two.rs": r#"// Rust file two"#,
4080            "two.ts": r#"// TypeScript file two"#,
4081        }),
4082    )
4083    .await;
4084    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4085
4086    assert_eq!(
4087        search(
4088            &project,
4089            SearchQuery::text(
4090                search_query,
4091                false,
4092                true,
4093                false,
4094                Vec::new(),
4095                vec![PathMatcher::new("*.odd").unwrap()],
4096            )
4097            .unwrap(),
4098            cx
4099        )
4100        .await
4101        .unwrap(),
4102        HashMap::from_iter([
4103            ("dir/one.rs".to_string(), vec![8..12]),
4104            ("dir/one.ts".to_string(), vec![14..18]),
4105            ("dir/two.rs".to_string(), vec![8..12]),
4106            ("dir/two.ts".to_string(), vec![14..18]),
4107        ]),
4108        "If no exclusions match, all files should be returned"
4109    );
4110
4111    assert_eq!(
4112        search(
4113            &project,
4114            SearchQuery::text(
4115                search_query,
4116                false,
4117                true,
4118                false,
4119                Vec::new(),
4120                vec![PathMatcher::new("*.rs").unwrap()],
4121            )
4122            .unwrap(),
4123            cx
4124        )
4125        .await
4126        .unwrap(),
4127        HashMap::from_iter([
4128            ("dir/one.ts".to_string(), vec![14..18]),
4129            ("dir/two.ts".to_string(), vec![14..18]),
4130        ]),
4131        "Rust exclusion search should give only TypeScript files"
4132    );
4133
4134    assert_eq!(
4135        search(
4136            &project,
4137            SearchQuery::text(
4138                search_query,
4139                false,
4140                true,
4141                false,
4142                Vec::new(),
4143                vec![
4144                    PathMatcher::new("*.ts").unwrap(),
4145                    PathMatcher::new("*.odd").unwrap(),
4146                ],
4147            ).unwrap(),
4148            cx
4149        )
4150        .await
4151        .unwrap(),
4152        HashMap::from_iter([
4153            ("dir/one.rs".to_string(), vec![8..12]),
4154            ("dir/two.rs".to_string(), vec![8..12]),
4155        ]),
4156        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
4157    );
4158
4159    assert!(
4160        search(
4161            &project,
4162            SearchQuery::text(
4163                search_query,
4164                false,
4165                true,
4166                false,
4167                Vec::new(),
4168                vec![
4169                    PathMatcher::new("*.rs").unwrap(),
4170                    PathMatcher::new("*.ts").unwrap(),
4171                    PathMatcher::new("*.odd").unwrap(),
4172                ],
4173            ).unwrap(),
4174            cx
4175        )
4176        .await
4177        .unwrap().is_empty(),
4178        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
4179    );
4180}
4181
4182#[gpui::test]
4183async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
4184    init_test(cx);
4185
4186    let search_query = "file";
4187
4188    let fs = FakeFs::new(cx.executor());
4189    fs.insert_tree(
4190        "/dir",
4191        json!({
4192            "one.rs": r#"// Rust file one"#,
4193            "one.ts": r#"// TypeScript file one"#,
4194            "two.rs": r#"// Rust file two"#,
4195            "two.ts": r#"// TypeScript file two"#,
4196        }),
4197    )
4198    .await;
4199    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4200
4201    assert!(
4202        search(
4203            &project,
4204            SearchQuery::text(
4205                search_query,
4206                false,
4207                true,
4208                false,
4209                vec![PathMatcher::new("*.odd").unwrap()],
4210                vec![PathMatcher::new("*.odd").unwrap()],
4211            )
4212            .unwrap(),
4213            cx
4214        )
4215        .await
4216        .unwrap()
4217        .is_empty(),
4218        "If both no exclusions and inclusions match, exclusions should win and return nothing"
4219    );
4220
4221    assert!(
4222        search(
4223            &project,
4224            SearchQuery::text(
4225                search_query,
4226                false,
4227                true,
4228                false,
4229                vec![PathMatcher::new("*.ts").unwrap()],
4230                vec![PathMatcher::new("*.ts").unwrap()],
4231            ).unwrap(),
4232            cx
4233        )
4234        .await
4235        .unwrap()
4236        .is_empty(),
4237        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
4238    );
4239
4240    assert!(
4241        search(
4242            &project,
4243            SearchQuery::text(
4244                search_query,
4245                false,
4246                true,
4247                false,
4248                vec![
4249                    PathMatcher::new("*.ts").unwrap(),
4250                    PathMatcher::new("*.odd").unwrap()
4251                ],
4252                vec![
4253                    PathMatcher::new("*.ts").unwrap(),
4254                    PathMatcher::new("*.odd").unwrap()
4255                ],
4256            )
4257            .unwrap(),
4258            cx
4259        )
4260        .await
4261        .unwrap()
4262        .is_empty(),
4263        "Non-matching inclusions and exclusions should not change that."
4264    );
4265
4266    assert_eq!(
4267        search(
4268            &project,
4269            SearchQuery::text(
4270                search_query,
4271                false,
4272                true,
4273                false,
4274                vec![
4275                    PathMatcher::new("*.ts").unwrap(),
4276                    PathMatcher::new("*.odd").unwrap()
4277                ],
4278                vec![
4279                    PathMatcher::new("*.rs").unwrap(),
4280                    PathMatcher::new("*.odd").unwrap()
4281                ],
4282            )
4283            .unwrap(),
4284            cx
4285        )
4286        .await
4287        .unwrap(),
4288        HashMap::from_iter([
4289            ("dir/one.ts".to_string(), vec![14..18]),
4290            ("dir/two.ts".to_string(), vec![14..18]),
4291        ]),
4292        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4293    );
4294}
4295
4296#[gpui::test]
4297async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
4298    init_test(cx);
4299
4300    let fs = FakeFs::new(cx.executor());
4301    fs.insert_tree(
4302        "/worktree-a",
4303        json!({
4304            "haystack.rs": r#"// NEEDLE"#,
4305            "haystack.ts": r#"// NEEDLE"#,
4306        }),
4307    )
4308    .await;
4309    fs.insert_tree(
4310        "/worktree-b",
4311        json!({
4312            "haystack.rs": r#"// NEEDLE"#,
4313            "haystack.ts": r#"// NEEDLE"#,
4314        }),
4315    )
4316    .await;
4317
4318    let project = Project::test(
4319        fs.clone(),
4320        ["/worktree-a".as_ref(), "/worktree-b".as_ref()],
4321        cx,
4322    )
4323    .await;
4324
4325    assert_eq!(
4326        search(
4327            &project,
4328            SearchQuery::text(
4329                "NEEDLE",
4330                false,
4331                true,
4332                false,
4333                vec![PathMatcher::new("worktree-a/*.rs").unwrap()],
4334                Vec::new()
4335            )
4336            .unwrap(),
4337            cx
4338        )
4339        .await
4340        .unwrap(),
4341        HashMap::from_iter([("worktree-a/haystack.rs".to_string(), vec![3..9])]),
4342        "should only return results from included worktree"
4343    );
4344    assert_eq!(
4345        search(
4346            &project,
4347            SearchQuery::text(
4348                "NEEDLE",
4349                false,
4350                true,
4351                false,
4352                vec![PathMatcher::new("worktree-b/*.rs").unwrap()],
4353                Vec::new()
4354            )
4355            .unwrap(),
4356            cx
4357        )
4358        .await
4359        .unwrap(),
4360        HashMap::from_iter([("worktree-b/haystack.rs".to_string(), vec![3..9])]),
4361        "should only return results from included worktree"
4362    );
4363
4364    assert_eq!(
4365        search(
4366            &project,
4367            SearchQuery::text(
4368                "NEEDLE",
4369                false,
4370                true,
4371                false,
4372                vec![PathMatcher::new("*.ts").unwrap()],
4373                Vec::new()
4374            )
4375            .unwrap(),
4376            cx
4377        )
4378        .await
4379        .unwrap(),
4380        HashMap::from_iter([
4381            ("worktree-a/haystack.ts".to_string(), vec![3..9]),
4382            ("worktree-b/haystack.ts".to_string(), vec![3..9])
4383        ]),
4384        "should return results from both worktrees"
4385    );
4386}
4387
4388#[gpui::test]
4389async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
4390    init_test(cx);
4391
4392    let fs = FakeFs::new(cx.background_executor.clone());
4393    fs.insert_tree(
4394        "/dir",
4395        json!({
4396            ".git": {},
4397            ".gitignore": "**/target\n/node_modules\n",
4398            "target": {
4399                "index.txt": "index_key:index_value"
4400            },
4401            "node_modules": {
4402                "eslint": {
4403                    "index.ts": "const eslint_key = 'eslint value'",
4404                    "package.json": r#"{ "some_key": "some value" }"#,
4405                },
4406                "prettier": {
4407                    "index.ts": "const prettier_key = 'prettier value'",
4408                    "package.json": r#"{ "other_key": "other value" }"#,
4409                },
4410            },
4411            "package.json": r#"{ "main_key": "main value" }"#,
4412        }),
4413    )
4414    .await;
4415    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4416
4417    let query = "key";
4418    assert_eq!(
4419        search(
4420            &project,
4421            SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
4422            cx
4423        )
4424        .await
4425        .unwrap(),
4426        HashMap::from_iter([("dir/package.json".to_string(), vec![8..11])]),
4427        "Only one non-ignored file should have the query"
4428    );
4429
4430    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4431    assert_eq!(
4432        search(
4433            &project,
4434            SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
4435            cx
4436        )
4437        .await
4438        .unwrap(),
4439        HashMap::from_iter([
4440            ("dir/package.json".to_string(), vec![8..11]),
4441            ("dir/target/index.txt".to_string(), vec![6..9]),
4442            (
4443                "dir/node_modules/prettier/package.json".to_string(),
4444                vec![9..12]
4445            ),
4446            (
4447                "dir/node_modules/prettier/index.ts".to_string(),
4448                vec![15..18]
4449            ),
4450            ("dir/node_modules/eslint/index.ts".to_string(), vec![13..16]),
4451            (
4452                "dir/node_modules/eslint/package.json".to_string(),
4453                vec![8..11]
4454            ),
4455        ]),
4456        "Unrestricted search with ignored directories should find every file with the query"
4457    );
4458
4459    let files_to_include = vec![PathMatcher::new("/dir/node_modules/prettier/**").unwrap()];
4460    let files_to_exclude = vec![PathMatcher::new("*.ts").unwrap()];
4461    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4462    assert_eq!(
4463        search(
4464            &project,
4465            SearchQuery::text(
4466                query,
4467                false,
4468                false,
4469                true,
4470                files_to_include,
4471                files_to_exclude,
4472            )
4473            .unwrap(),
4474            cx
4475        )
4476        .await
4477        .unwrap(),
4478        HashMap::from_iter([(
4479            "dir/node_modules/prettier/package.json".to_string(),
4480            vec![9..12]
4481        )]),
4482        "With search including ignored prettier directory and excluding TS files, only one file should be found"
4483    );
4484}
4485
4486#[test]
4487fn test_glob_literal_prefix() {
4488    assert_eq!(glob_literal_prefix("**/*.js"), "");
4489    assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4490    assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4491    assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4492}
4493
4494#[gpui::test]
4495async fn test_create_entry(cx: &mut gpui::TestAppContext) {
4496    init_test(cx);
4497
4498    let fs = FakeFs::new(cx.executor().clone());
4499    fs.insert_tree(
4500        "/one/two",
4501        json!({
4502            "three": {
4503                "a.txt": "",
4504                "four": {}
4505            },
4506            "c.rs": ""
4507        }),
4508    )
4509    .await;
4510
4511    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
4512    project
4513        .update(cx, |project, cx| {
4514            let id = project.worktrees().next().unwrap().read(cx).id();
4515            project.create_entry((id, "b.."), true, cx)
4516        })
4517        .unwrap()
4518        .await
4519        .to_included()
4520        .unwrap();
4521
4522    // Can't create paths outside the project
4523    let result = project
4524        .update(cx, |project, cx| {
4525            let id = project.worktrees().next().unwrap().read(cx).id();
4526            project.create_entry((id, "../../boop"), true, cx)
4527        })
4528        .await;
4529    assert!(result.is_err());
4530
4531    // Can't create paths with '..'
4532    let result = project
4533        .update(cx, |project, cx| {
4534            let id = project.worktrees().next().unwrap().read(cx).id();
4535            project.create_entry((id, "four/../beep"), true, cx)
4536        })
4537        .await;
4538    assert!(result.is_err());
4539
4540    assert_eq!(
4541        fs.paths(true),
4542        vec![
4543            PathBuf::from("/"),
4544            PathBuf::from("/one"),
4545            PathBuf::from("/one/two"),
4546            PathBuf::from("/one/two/c.rs"),
4547            PathBuf::from("/one/two/three"),
4548            PathBuf::from("/one/two/three/a.txt"),
4549            PathBuf::from("/one/two/three/b.."),
4550            PathBuf::from("/one/two/three/four"),
4551        ]
4552    );
4553
4554    // And we cannot open buffers with '..'
4555    let result = project
4556        .update(cx, |project, cx| {
4557            let id = project.worktrees().next().unwrap().read(cx).id();
4558            project.open_buffer((id, "../c.rs"), cx)
4559        })
4560        .await;
4561    assert!(result.is_err())
4562}
4563
4564#[gpui::test]
4565async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
4566    init_test(cx);
4567
4568    let fs = FakeFs::new(cx.executor());
4569    fs.insert_tree(
4570        "/dir",
4571        json!({
4572            "a.tsx": "a",
4573        }),
4574    )
4575    .await;
4576
4577    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4578
4579    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4580    language_registry.add(tsx_lang());
4581    let language_server_names = [
4582        "TypeScriptServer",
4583        "TailwindServer",
4584        "ESLintServer",
4585        "NoHoverCapabilitiesServer",
4586    ];
4587    let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4588        "tsx",
4589        true,
4590        FakeLspAdapter {
4591            name: &language_server_names[0],
4592            capabilities: lsp::ServerCapabilities {
4593                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4594                ..lsp::ServerCapabilities::default()
4595            },
4596            ..FakeLspAdapter::default()
4597        },
4598    );
4599    let _a = language_registry.register_specific_fake_lsp_adapter(
4600        "tsx",
4601        false,
4602        FakeLspAdapter {
4603            name: &language_server_names[1],
4604            capabilities: lsp::ServerCapabilities {
4605                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4606                ..lsp::ServerCapabilities::default()
4607            },
4608            ..FakeLspAdapter::default()
4609        },
4610    );
4611    let _b = language_registry.register_specific_fake_lsp_adapter(
4612        "tsx",
4613        false,
4614        FakeLspAdapter {
4615            name: &language_server_names[2],
4616            capabilities: lsp::ServerCapabilities {
4617                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4618                ..lsp::ServerCapabilities::default()
4619            },
4620            ..FakeLspAdapter::default()
4621        },
4622    );
4623    let _c = language_registry.register_specific_fake_lsp_adapter(
4624        "tsx",
4625        false,
4626        FakeLspAdapter {
4627            name: &language_server_names[3],
4628            capabilities: lsp::ServerCapabilities {
4629                hover_provider: None,
4630                ..lsp::ServerCapabilities::default()
4631            },
4632            ..FakeLspAdapter::default()
4633        },
4634    );
4635
4636    let buffer = project
4637        .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4638        .await
4639        .unwrap();
4640    cx.executor().run_until_parked();
4641
4642    let mut servers_with_hover_requests = HashMap::default();
4643    for i in 0..language_server_names.len() {
4644        let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4645            panic!(
4646                "Failed to get language server #{i} with name {}",
4647                &language_server_names[i]
4648            )
4649        });
4650        let new_server_name = new_server.server.name();
4651        assert!(
4652            !servers_with_hover_requests.contains_key(new_server_name),
4653            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4654        );
4655        let new_server_name = new_server_name.to_string();
4656        match new_server_name.as_str() {
4657            "TailwindServer" | "TypeScriptServer" => {
4658                servers_with_hover_requests.insert(
4659                    new_server_name.clone(),
4660                    new_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| {
4661                        let name = new_server_name.clone();
4662                        async move {
4663                            Ok(Some(lsp::Hover {
4664                                contents: lsp::HoverContents::Scalar(lsp::MarkedString::String(
4665                                    format!("{name} hover"),
4666                                )),
4667                                range: None,
4668                            }))
4669                        }
4670                    }),
4671                );
4672            }
4673            "ESLintServer" => {
4674                servers_with_hover_requests.insert(
4675                    new_server_name,
4676                    new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4677                        |_, _| async move { Ok(None) },
4678                    ),
4679                );
4680            }
4681            "NoHoverCapabilitiesServer" => {
4682                let _never_handled = new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4683                    |_, _| async move {
4684                        panic!(
4685                            "Should not call for hovers server with no corresponding capabilities"
4686                        )
4687                    },
4688                );
4689            }
4690            unexpected => panic!("Unexpected server name: {unexpected}"),
4691        }
4692    }
4693
4694    let hover_task = project.update(cx, |project, cx| {
4695        project.hover(&buffer, Point::new(0, 0), cx)
4696    });
4697    let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
4698        |mut hover_request| async move {
4699            hover_request
4700                .next()
4701                .await
4702                .expect("All hover requests should have been triggered")
4703        },
4704    ))
4705    .await;
4706    assert_eq!(
4707        vec!["TailwindServer hover", "TypeScriptServer hover"],
4708        hover_task
4709            .await
4710            .into_iter()
4711            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4712            .sorted()
4713            .collect::<Vec<_>>(),
4714        "Should receive hover responses from all related servers with hover capabilities"
4715    );
4716}
4717
4718#[gpui::test]
4719async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
4720    init_test(cx);
4721
4722    let fs = FakeFs::new(cx.executor());
4723    fs.insert_tree(
4724        "/dir",
4725        json!({
4726            "a.ts": "a",
4727        }),
4728    )
4729    .await;
4730
4731    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4732
4733    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4734    language_registry.add(typescript_lang());
4735    let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
4736        "TypeScript",
4737        FakeLspAdapter {
4738            capabilities: lsp::ServerCapabilities {
4739                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4740                ..lsp::ServerCapabilities::default()
4741            },
4742            ..FakeLspAdapter::default()
4743        },
4744    );
4745
4746    let buffer = project
4747        .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
4748        .await
4749        .unwrap();
4750    cx.executor().run_until_parked();
4751
4752    let fake_server = fake_language_servers
4753        .next()
4754        .await
4755        .expect("failed to get the language server");
4756
4757    let mut request_handled =
4758        fake_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
4759            Ok(Some(lsp::Hover {
4760                contents: lsp::HoverContents::Array(vec![
4761                    lsp::MarkedString::String("".to_string()),
4762                    lsp::MarkedString::String("      ".to_string()),
4763                    lsp::MarkedString::String("\n\n\n".to_string()),
4764                ]),
4765                range: None,
4766            }))
4767        });
4768
4769    let hover_task = project.update(cx, |project, cx| {
4770        project.hover(&buffer, Point::new(0, 0), cx)
4771    });
4772    let () = request_handled
4773        .next()
4774        .await
4775        .expect("All hover requests should have been triggered");
4776    assert_eq!(
4777        Vec::<String>::new(),
4778        hover_task
4779            .await
4780            .into_iter()
4781            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4782            .sorted()
4783            .collect::<Vec<_>>(),
4784        "Empty hover parts should be ignored"
4785    );
4786}
4787
4788#[gpui::test]
4789async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
4790    init_test(cx);
4791
4792    let fs = FakeFs::new(cx.executor());
4793    fs.insert_tree(
4794        "/dir",
4795        json!({
4796            "a.tsx": "a",
4797        }),
4798    )
4799    .await;
4800
4801    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4802
4803    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4804    language_registry.add(tsx_lang());
4805    let language_server_names = [
4806        "TypeScriptServer",
4807        "TailwindServer",
4808        "ESLintServer",
4809        "NoActionsCapabilitiesServer",
4810    ];
4811    let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4812        "tsx",
4813        true,
4814        FakeLspAdapter {
4815            name: &language_server_names[0],
4816            capabilities: lsp::ServerCapabilities {
4817                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4818                ..lsp::ServerCapabilities::default()
4819            },
4820            ..FakeLspAdapter::default()
4821        },
4822    );
4823    let _a = language_registry.register_specific_fake_lsp_adapter(
4824        "tsx",
4825        false,
4826        FakeLspAdapter {
4827            name: &language_server_names[1],
4828            capabilities: lsp::ServerCapabilities {
4829                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4830                ..lsp::ServerCapabilities::default()
4831            },
4832            ..FakeLspAdapter::default()
4833        },
4834    );
4835    let _b = language_registry.register_specific_fake_lsp_adapter(
4836        "tsx",
4837        false,
4838        FakeLspAdapter {
4839            name: &language_server_names[2],
4840            capabilities: lsp::ServerCapabilities {
4841                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4842                ..lsp::ServerCapabilities::default()
4843            },
4844            ..FakeLspAdapter::default()
4845        },
4846    );
4847    let _c = language_registry.register_specific_fake_lsp_adapter(
4848        "tsx",
4849        false,
4850        FakeLspAdapter {
4851            name: &language_server_names[3],
4852            capabilities: lsp::ServerCapabilities {
4853                code_action_provider: None,
4854                ..lsp::ServerCapabilities::default()
4855            },
4856            ..FakeLspAdapter::default()
4857        },
4858    );
4859
4860    let buffer = project
4861        .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4862        .await
4863        .unwrap();
4864    cx.executor().run_until_parked();
4865
4866    let mut servers_with_actions_requests = HashMap::default();
4867    for i in 0..language_server_names.len() {
4868        let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4869            panic!(
4870                "Failed to get language server #{i} with name {}",
4871                &language_server_names[i]
4872            )
4873        });
4874        let new_server_name = new_server.server.name();
4875        assert!(
4876            !servers_with_actions_requests.contains_key(new_server_name),
4877            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4878        );
4879        let new_server_name = new_server_name.to_string();
4880        match new_server_name.as_str() {
4881            "TailwindServer" | "TypeScriptServer" => {
4882                servers_with_actions_requests.insert(
4883                    new_server_name.clone(),
4884                    new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4885                        move |_, _| {
4886                            let name = new_server_name.clone();
4887                            async move {
4888                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4889                                    lsp::CodeAction {
4890                                        title: format!("{name} code action"),
4891                                        ..lsp::CodeAction::default()
4892                                    },
4893                                )]))
4894                            }
4895                        },
4896                    ),
4897                );
4898            }
4899            "ESLintServer" => {
4900                servers_with_actions_requests.insert(
4901                    new_server_name,
4902                    new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4903                        |_, _| async move { Ok(None) },
4904                    ),
4905                );
4906            }
4907            "NoActionsCapabilitiesServer" => {
4908                let _never_handled = new_server
4909                    .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
4910                        panic!(
4911                            "Should not call for code actions server with no corresponding capabilities"
4912                        )
4913                    });
4914            }
4915            unexpected => panic!("Unexpected server name: {unexpected}"),
4916        }
4917    }
4918
4919    let code_actions_task = project.update(cx, |project, cx| {
4920        project.code_actions(&buffer, 0..buffer.read(cx).len(), cx)
4921    });
4922    let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
4923        |mut code_actions_request| async move {
4924            code_actions_request
4925                .next()
4926                .await
4927                .expect("All code actions requests should have been triggered")
4928        },
4929    ))
4930    .await;
4931    assert_eq!(
4932        vec!["TailwindServer code action", "TypeScriptServer code action"],
4933        code_actions_task
4934            .await
4935            .into_iter()
4936            .map(|code_action| code_action.lsp_action.title)
4937            .sorted()
4938            .collect::<Vec<_>>(),
4939        "Should receive code actions responses from all related servers with hover capabilities"
4940    );
4941}
4942
4943#[gpui::test]
4944async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
4945    init_test(cx);
4946
4947    let fs = FakeFs::new(cx.executor());
4948    fs.insert_tree(
4949        "/dir",
4950        json!({
4951            "a.rs": "let a = 1;",
4952            "b.rs": "let b = 2;",
4953            "c.rs": "let c = 2;",
4954        }),
4955    )
4956    .await;
4957
4958    let project = Project::test(
4959        fs,
4960        [
4961            "/dir/a.rs".as_ref(),
4962            "/dir/b.rs".as_ref(),
4963            "/dir/c.rs".as_ref(),
4964        ],
4965        cx,
4966    )
4967    .await;
4968
4969    // check the initial state and get the worktrees
4970    let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
4971        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
4972        assert_eq!(worktrees.len(), 3);
4973
4974        let worktree_a = worktrees[0].read(cx);
4975        let worktree_b = worktrees[1].read(cx);
4976        let worktree_c = worktrees[2].read(cx);
4977
4978        // check they start in the right order
4979        assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
4980        assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
4981        assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
4982
4983        (
4984            worktrees[0].clone(),
4985            worktrees[1].clone(),
4986            worktrees[2].clone(),
4987        )
4988    });
4989
4990    // move first worktree to after the second
4991    // [a, b, c] -> [b, a, c]
4992    project
4993        .update(cx, |project, cx| {
4994            let first = worktree_a.read(cx);
4995            let second = worktree_b.read(cx);
4996            project.move_worktree(first.id(), second.id(), cx)
4997        })
4998        .expect("moving first after second");
4999
5000    // check the state after moving
5001    project.update(cx, |project, cx| {
5002        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5003        assert_eq!(worktrees.len(), 3);
5004
5005        let first = worktrees[0].read(cx);
5006        let second = worktrees[1].read(cx);
5007        let third = worktrees[2].read(cx);
5008
5009        // check they are now in the right order
5010        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5011        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
5012        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5013    });
5014
5015    // move the second worktree to before the first
5016    // [b, a, c] -> [a, b, c]
5017    project
5018        .update(cx, |project, cx| {
5019            let second = worktree_a.read(cx);
5020            let first = worktree_b.read(cx);
5021            project.move_worktree(first.id(), second.id(), cx)
5022        })
5023        .expect("moving second before first");
5024
5025    // check the state after moving
5026    project.update(cx, |project, cx| {
5027        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5028        assert_eq!(worktrees.len(), 3);
5029
5030        let first = worktrees[0].read(cx);
5031        let second = worktrees[1].read(cx);
5032        let third = worktrees[2].read(cx);
5033
5034        // check they are now in the right order
5035        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5036        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5037        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5038    });
5039
5040    // move the second worktree to after the third
5041    // [a, b, c] -> [a, c, b]
5042    project
5043        .update(cx, |project, cx| {
5044            let second = worktree_b.read(cx);
5045            let third = worktree_c.read(cx);
5046            project.move_worktree(second.id(), third.id(), cx)
5047        })
5048        .expect("moving second after third");
5049
5050    // check the state after moving
5051    project.update(cx, |project, cx| {
5052        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5053        assert_eq!(worktrees.len(), 3);
5054
5055        let first = worktrees[0].read(cx);
5056        let second = worktrees[1].read(cx);
5057        let third = worktrees[2].read(cx);
5058
5059        // check they are now in the right order
5060        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5061        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5062        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
5063    });
5064
5065    // move the third worktree to before the second
5066    // [a, c, b] -> [a, b, c]
5067    project
5068        .update(cx, |project, cx| {
5069            let third = worktree_c.read(cx);
5070            let second = worktree_b.read(cx);
5071            project.move_worktree(third.id(), second.id(), cx)
5072        })
5073        .expect("moving third before second");
5074
5075    // check the state after moving
5076    project.update(cx, |project, cx| {
5077        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5078        assert_eq!(worktrees.len(), 3);
5079
5080        let first = worktrees[0].read(cx);
5081        let second = worktrees[1].read(cx);
5082        let third = worktrees[2].read(cx);
5083
5084        // check they are now in the right order
5085        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5086        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5087        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5088    });
5089
5090    // move the first worktree to after the third
5091    // [a, b, c] -> [b, c, a]
5092    project
5093        .update(cx, |project, cx| {
5094            let first = worktree_a.read(cx);
5095            let third = worktree_c.read(cx);
5096            project.move_worktree(first.id(), third.id(), cx)
5097        })
5098        .expect("moving first after third");
5099
5100    // check the state after moving
5101    project.update(cx, |project, cx| {
5102        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5103        assert_eq!(worktrees.len(), 3);
5104
5105        let first = worktrees[0].read(cx);
5106        let second = worktrees[1].read(cx);
5107        let third = worktrees[2].read(cx);
5108
5109        // check they are now in the right order
5110        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5111        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5112        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
5113    });
5114
5115    // move the third worktree to before the first
5116    // [b, c, a] -> [a, b, c]
5117    project
5118        .update(cx, |project, cx| {
5119            let third = worktree_a.read(cx);
5120            let first = worktree_b.read(cx);
5121            project.move_worktree(third.id(), first.id(), cx)
5122        })
5123        .expect("moving third before first");
5124
5125    // check the state after moving
5126    project.update(cx, |project, cx| {
5127        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5128        assert_eq!(worktrees.len(), 3);
5129
5130        let first = worktrees[0].read(cx);
5131        let second = worktrees[1].read(cx);
5132        let third = worktrees[2].read(cx);
5133
5134        // check they are now in the right order
5135        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5136        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5137        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5138    });
5139}
5140
5141async fn search(
5142    project: &Model<Project>,
5143    query: SearchQuery,
5144    cx: &mut gpui::TestAppContext,
5145) -> Result<HashMap<String, Vec<Range<usize>>>> {
5146    let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
5147    let mut results = HashMap::default();
5148    while let Some(search_result) = search_rx.next().await {
5149        match search_result {
5150            SearchResult::Buffer { buffer, ranges } => {
5151                results.entry(buffer).or_insert(ranges);
5152            }
5153            SearchResult::LimitReached => {}
5154        }
5155    }
5156    Ok(results
5157        .into_iter()
5158        .map(|(buffer, ranges)| {
5159            buffer.update(cx, |buffer, cx| {
5160                let path = buffer
5161                    .file()
5162                    .unwrap()
5163                    .full_path(cx)
5164                    .to_string_lossy()
5165                    .to_string();
5166                let ranges = ranges
5167                    .into_iter()
5168                    .map(|range| range.to_offset(buffer))
5169                    .collect::<Vec<_>>();
5170                (path, ranges)
5171            })
5172        })
5173        .collect())
5174}
5175
5176fn init_test(cx: &mut gpui::TestAppContext) {
5177    if std::env::var("RUST_LOG").is_ok() {
5178        env_logger::try_init().ok();
5179    }
5180
5181    cx.update(|cx| {
5182        let settings_store = SettingsStore::test(cx);
5183        cx.set_global(settings_store);
5184        release_channel::init(SemanticVersion::default(), cx);
5185        language::init(cx);
5186        Project::init_settings(cx);
5187    });
5188}
5189
5190fn json_lang() -> Arc<Language> {
5191    Arc::new(Language::new(
5192        LanguageConfig {
5193            name: "JSON".into(),
5194            matcher: LanguageMatcher {
5195                path_suffixes: vec!["json".to_string()],
5196                ..Default::default()
5197            },
5198            ..Default::default()
5199        },
5200        None,
5201    ))
5202}
5203
5204fn js_lang() -> Arc<Language> {
5205    Arc::new(Language::new(
5206        LanguageConfig {
5207            name: Arc::from("JavaScript"),
5208            matcher: LanguageMatcher {
5209                path_suffixes: vec!["js".to_string()],
5210                ..Default::default()
5211            },
5212            ..Default::default()
5213        },
5214        None,
5215    ))
5216}
5217
5218fn rust_lang() -> Arc<Language> {
5219    Arc::new(Language::new(
5220        LanguageConfig {
5221            name: "Rust".into(),
5222            matcher: LanguageMatcher {
5223                path_suffixes: vec!["rs".to_string()],
5224                ..Default::default()
5225            },
5226            ..Default::default()
5227        },
5228        Some(tree_sitter_rust::language()),
5229    ))
5230}
5231
5232fn typescript_lang() -> Arc<Language> {
5233    Arc::new(Language::new(
5234        LanguageConfig {
5235            name: "TypeScript".into(),
5236            matcher: LanguageMatcher {
5237                path_suffixes: vec!["ts".to_string()],
5238                ..Default::default()
5239            },
5240            ..Default::default()
5241        },
5242        Some(tree_sitter_typescript::language_typescript()),
5243    ))
5244}
5245
5246fn tsx_lang() -> Arc<Language> {
5247    Arc::new(Language::new(
5248        LanguageConfig {
5249            name: "tsx".into(),
5250            matcher: LanguageMatcher {
5251                path_suffixes: vec!["tsx".to_string()],
5252                ..Default::default()
5253            },
5254            ..Default::default()
5255        },
5256        Some(tree_sitter_typescript::language_tsx()),
5257    ))
5258}
5259
5260fn get_all_tasks(
5261    project: &Model<Project>,
5262    worktree_id: Option<WorktreeId>,
5263    task_context: &TaskContext,
5264    cx: &mut AppContext,
5265) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
5266    let resolved_tasks = project.update(cx, |project, cx| {
5267        project
5268            .task_inventory()
5269            .read(cx)
5270            .used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx)
5271    });
5272
5273    cx.spawn(|_| async move {
5274        let (mut old, new) = resolved_tasks.await;
5275        old.extend(new);
5276        old
5277    })
5278}