project_tests.rs

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