remote_editing_tests.rs

   1/// todo(windows)
   2/// The tests in this file assume that server_cx is running on Windows too.
   3/// We neead to find a way to test Windows-Non-Windows interactions.
   4use crate::headless_project::HeadlessProject;
   5use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput};
   6use client::{Client, UserStore};
   7use clock::FakeSystemClock;
   8use collections::{HashMap, HashSet};
   9use language_model::LanguageModelToolResultContent;
  10use languages::rust_lang;
  11
  12use extension::ExtensionHostProxy;
  13use fs::{FakeFs, Fs};
  14use git::repository::Worktree as GitWorktree;
  15use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
  16use http_client::{BlockedHttpClient, FakeHttpClient};
  17use language::{
  18    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
  19    language_settings::{AllLanguageSettings, LanguageSettings},
  20};
  21use lsp::{
  22    CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
  23    LanguageServerName,
  24};
  25use node_runtime::NodeRuntime;
  26use project::{
  27    ProgressToken, Project,
  28    agent_server_store::AgentServerCommand,
  29    search::{SearchQuery, SearchResult},
  30};
  31use remote::RemoteClient;
  32use serde_json::json;
  33use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
  34use smol::stream::StreamExt;
  35use std::{
  36    path::{Path, PathBuf},
  37    sync::Arc,
  38};
  39use unindent::Unindent as _;
  40use util::{path, paths::PathMatcher, rel_path::rel_path};
  41
  42#[gpui::test]
  43async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  44    let fs = FakeFs::new(server_cx.executor());
  45    fs.insert_tree(
  46        path!("/code"),
  47        json!({
  48            "project1": {
  49                ".git": {},
  50                "README.md": "# project 1",
  51                "src": {
  52                    "lib.rs": "fn one() -> usize { 1 }"
  53                }
  54            },
  55            "project2": {
  56                "README.md": "# project 2",
  57            },
  58        }),
  59    )
  60    .await;
  61    fs.set_index_for_repo(
  62        Path::new(path!("/code/project1/.git")),
  63        &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
  64    );
  65
  66    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  67    let (worktree, _) = project
  68        .update(cx, |project, cx| {
  69            project.find_or_create_worktree(path!("/code/project1"), true, cx)
  70        })
  71        .await
  72        .unwrap();
  73
  74    // The client sees the worktree's contents.
  75    cx.executor().run_until_parked();
  76    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  77    worktree.update(cx, |worktree, _cx| {
  78        assert_eq!(
  79            worktree.paths().collect::<Vec<_>>(),
  80            vec![
  81                rel_path("README.md"),
  82                rel_path("src"),
  83                rel_path("src/lib.rs"),
  84            ]
  85        );
  86    });
  87
  88    // The user opens a buffer in the remote worktree. The buffer's
  89    // contents are loaded from the remote filesystem.
  90    let buffer = project
  91        .update(cx, |project, cx| {
  92            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
  93        })
  94        .await
  95        .unwrap();
  96    let diff = project
  97        .update(cx, |project, cx| {
  98            project.open_unstaged_diff(buffer.clone(), cx)
  99        })
 100        .await
 101        .unwrap();
 102
 103    diff.update(cx, |diff, cx| {
 104        assert_eq!(
 105            diff.base_text_string(cx).unwrap(),
 106            "fn one() -> usize { 0 }"
 107        );
 108    });
 109
 110    buffer.update(cx, |buffer, cx| {
 111        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 112        let ix = buffer.text().find('1').unwrap();
 113        buffer.edit([(ix..ix + 1, "100")], None, cx);
 114    });
 115
 116    // The user saves the buffer. The new contents are written to the
 117    // remote filesystem.
 118    project
 119        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 120        .await
 121        .unwrap();
 122    assert_eq!(
 123        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 124        "fn one() -> usize { 100 }"
 125    );
 126
 127    // A new file is created in the remote filesystem. The user
 128    // sees the new file.
 129    fs.save(
 130        path!("/code/project1/src/main.rs").as_ref(),
 131        &"fn main() {}".into(),
 132        Default::default(),
 133    )
 134    .await
 135    .unwrap();
 136    cx.executor().run_until_parked();
 137    worktree.update(cx, |worktree, _cx| {
 138        assert_eq!(
 139            worktree.paths().collect::<Vec<_>>(),
 140            vec![
 141                rel_path("README.md"),
 142                rel_path("src"),
 143                rel_path("src/lib.rs"),
 144                rel_path("src/main.rs"),
 145            ]
 146        );
 147    });
 148
 149    // A file that is currently open in a buffer is renamed.
 150    fs.rename(
 151        path!("/code/project1/src/lib.rs").as_ref(),
 152        path!("/code/project1/src/lib2.rs").as_ref(),
 153        Default::default(),
 154    )
 155    .await
 156    .unwrap();
 157    cx.executor().run_until_parked();
 158    buffer.update(cx, |buffer, _| {
 159        assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
 160    });
 161
 162    fs.set_index_for_repo(
 163        Path::new(path!("/code/project1/.git")),
 164        &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
 165    );
 166    cx.executor().run_until_parked();
 167    diff.update(cx, |diff, cx| {
 168        assert_eq!(
 169            diff.base_text_string(cx).unwrap(),
 170            "fn one() -> usize { 100 }"
 171        );
 172    });
 173}
 174
 175async fn do_search_and_assert(
 176    project: &Entity<Project>,
 177    query: &str,
 178    files_to_include: PathMatcher,
 179    match_full_paths: bool,
 180    expected_paths: &[&str],
 181    mut cx: TestAppContext,
 182) -> Vec<Entity<Buffer>> {
 183    let query = query.to_string();
 184    let receiver = project.update(&mut cx, |project, cx| {
 185        project.search(
 186            SearchQuery::text(
 187                query,
 188                false,
 189                true,
 190                false,
 191                files_to_include,
 192                Default::default(),
 193                match_full_paths,
 194                None,
 195            )
 196            .unwrap(),
 197            cx,
 198        )
 199    });
 200
 201    let mut buffers = Vec::new();
 202    for expected_path in expected_paths {
 203        let response = receiver.rx.recv().await.unwrap();
 204        let SearchResult::Buffer { buffer, .. } = response else {
 205            panic!("incorrect result");
 206        };
 207        buffer.update(&mut cx, |buffer, cx| {
 208            assert_eq!(
 209                buffer.file().unwrap().full_path(cx).to_string_lossy(),
 210                *expected_path
 211            )
 212        });
 213        buffers.push(buffer);
 214    }
 215
 216    assert!(receiver.rx.recv().await.is_err());
 217    buffers
 218}
 219
 220#[gpui::test]
 221async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 222    let fs = FakeFs::new(server_cx.executor());
 223    fs.insert_tree(
 224        path!("/code"),
 225        json!({
 226            "project1": {
 227                ".git": {},
 228                "README.md": "# project 1",
 229                "src": {
 230                    "lib.rs": "fn one() -> usize { 1 }"
 231                }
 232            },
 233        }),
 234    )
 235    .await;
 236
 237    let (project, headless) = init_test(&fs, cx, server_cx).await;
 238
 239    project
 240        .update(cx, |project, cx| {
 241            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 242        })
 243        .await
 244        .unwrap();
 245
 246    cx.run_until_parked();
 247
 248    let buffers = do_search_and_assert(
 249        &project,
 250        "project",
 251        Default::default(),
 252        false,
 253        &[path!("project1/README.md")],
 254        cx.clone(),
 255    )
 256    .await;
 257    let buffer = buffers.into_iter().next().unwrap();
 258
 259    // test that the headless server is tracking which buffers we have open correctly.
 260    cx.run_until_parked();
 261    headless.update(server_cx, |headless, cx| {
 262        assert!(headless.buffer_store.read(cx).has_shared_buffers())
 263    });
 264    do_search_and_assert(
 265        &project,
 266        "project",
 267        Default::default(),
 268        false,
 269        &[path!("project1/README.md")],
 270        cx.clone(),
 271    )
 272    .await;
 273    server_cx.run_until_parked();
 274    cx.update(|_| {
 275        drop(buffer);
 276    });
 277    cx.run_until_parked();
 278    server_cx.run_until_parked();
 279    headless.update(server_cx, |headless, cx| {
 280        assert!(!headless.buffer_store.read(cx).has_shared_buffers())
 281    });
 282
 283    do_search_and_assert(
 284        &project,
 285        "project",
 286        Default::default(),
 287        false,
 288        &[path!("project1/README.md")],
 289        cx.clone(),
 290    )
 291    .await;
 292}
 293
 294#[gpui::test]
 295async fn test_remote_project_search_single_cpu(
 296    cx: &mut TestAppContext,
 297    server_cx: &mut TestAppContext,
 298) {
 299    let fs = FakeFs::new(server_cx.executor());
 300    fs.insert_tree(
 301        path!("/code"),
 302        json!({
 303            "project1": {
 304                ".git": {},
 305                "README.md": "# project 1",
 306                "src": {
 307                    "lib.rs": "fn one() -> usize { 1 }"
 308                }
 309            },
 310        }),
 311    )
 312    .await;
 313
 314    // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
 315    // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
 316    // which silently drops all search channels and produces zero results.
 317    server_cx.executor().set_num_cpus(1);
 318
 319    let (project, _) = init_test(&fs, cx, server_cx).await;
 320
 321    project
 322        .update(cx, |project, cx| {
 323            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 324        })
 325        .await
 326        .unwrap();
 327
 328    cx.run_until_parked();
 329
 330    do_search_and_assert(
 331        &project,
 332        "project",
 333        Default::default(),
 334        false,
 335        &[path!("project1/README.md")],
 336        cx.clone(),
 337    )
 338    .await;
 339}
 340
 341#[gpui::test]
 342async fn test_remote_project_search_inclusion(
 343    cx: &mut TestAppContext,
 344    server_cx: &mut TestAppContext,
 345) {
 346    let fs = FakeFs::new(server_cx.executor());
 347    fs.insert_tree(
 348        path!("/code"),
 349        json!({
 350            "project1": {
 351                "README.md": "# project 1",
 352            },
 353            "project2": {
 354                "README.md": "# project 2",
 355            },
 356        }),
 357    )
 358    .await;
 359
 360    let (project, _) = init_test(&fs, cx, server_cx).await;
 361
 362    project
 363        .update(cx, |project, cx| {
 364            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 365        })
 366        .await
 367        .unwrap();
 368
 369    project
 370        .update(cx, |project, cx| {
 371            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 372        })
 373        .await
 374        .unwrap();
 375
 376    cx.run_until_parked();
 377
 378    // Case 1: Test search with path matcher limiting to only one worktree
 379    let path_matcher = PathMatcher::new(
 380        &["project1/*.md".to_owned()],
 381        util::paths::PathStyle::local(),
 382    )
 383    .unwrap();
 384    do_search_and_assert(
 385        &project,
 386        "project",
 387        path_matcher,
 388        true, // should be true in case of multiple worktrees
 389        &[path!("project1/README.md")],
 390        cx.clone(),
 391    )
 392    .await;
 393
 394    // Case 2: Test search without path matcher, matching both worktrees
 395    do_search_and_assert(
 396        &project,
 397        "project",
 398        Default::default(),
 399        true, // should be true in case of multiple worktrees
 400        &[path!("project1/README.md"), path!("project2/README.md")],
 401        cx.clone(),
 402    )
 403    .await;
 404}
 405
 406#[gpui::test]
 407async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 408    let fs = FakeFs::new(server_cx.executor());
 409    fs.insert_tree(
 410        "/code",
 411        json!({
 412            "project1": {
 413                ".git": {},
 414                "README.md": "# project 1",
 415                "src": {
 416                    "lib.rs": "fn one() -> usize { 1 }"
 417                }
 418            },
 419        }),
 420    )
 421    .await;
 422
 423    let (project, headless) = init_test(&fs, cx, server_cx).await;
 424
 425    cx.update_global(|settings_store: &mut SettingsStore, cx| {
 426        settings_store.set_user_settings(
 427            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
 428            cx,
 429        )
 430    })
 431    .unwrap();
 432
 433    cx.run_until_parked();
 434
 435    server_cx.read(|cx| {
 436        assert_eq!(
 437            AllLanguageSettings::get_global(cx)
 438                .language(None, Some(&"Rust".into()), cx)
 439                .language_servers,
 440            ["from-local-settings"],
 441            "User language settings should be synchronized with the server settings"
 442        )
 443    });
 444
 445    server_cx
 446        .update_global(|settings_store: &mut SettingsStore, cx| {
 447            settings_store.set_server_settings(
 448                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
 449                cx,
 450            )
 451        })
 452        .unwrap();
 453
 454    cx.run_until_parked();
 455
 456    server_cx.read(|cx| {
 457        assert_eq!(
 458            AllLanguageSettings::get_global(cx)
 459                .language(None, Some(&"Rust".into()), cx)
 460                .language_servers,
 461            ["from-server-settings".to_string()],
 462            "Server language settings should take precedence over the user settings"
 463        )
 464    });
 465
 466    fs.insert_tree(
 467        "/code/project1/.zed",
 468        json!({
 469            "settings.json": r#"
 470                  {
 471                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
 472                    "lsp": {
 473                      "override-rust-analyzer": {
 474                        "binary": {
 475                          "path": "~/.cargo/bin/rust-analyzer"
 476                        }
 477                      }
 478                    }
 479                  }"#
 480        }),
 481    )
 482    .await;
 483
 484    let worktree_id = project
 485        .update(cx, |project, cx| {
 486            project.languages().add(rust_lang());
 487            project.find_or_create_worktree("/code/project1", true, cx)
 488        })
 489        .await
 490        .unwrap()
 491        .0
 492        .read_with(cx, |worktree, _| worktree.id());
 493
 494    let buffer = project
 495        .update(cx, |project, cx| {
 496            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 497        })
 498        .await
 499        .unwrap();
 500    cx.run_until_parked();
 501
 502    server_cx.read(|cx| {
 503        let worktree_id = headless
 504            .read(cx)
 505            .worktree_store
 506            .read(cx)
 507            .worktrees()
 508            .next()
 509            .unwrap()
 510            .read(cx)
 511            .id();
 512        assert_eq!(
 513            AllLanguageSettings::get(
 514                Some(SettingsLocation {
 515                    worktree_id,
 516                    path: rel_path("src/lib.rs")
 517                }),
 518                cx
 519            )
 520            .language(None, Some(&"Rust".into()), cx)
 521            .language_servers,
 522            ["override-rust-analyzer".to_string()]
 523        )
 524    });
 525
 526    cx.read(|cx| {
 527        assert_eq!(
 528            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
 529            ["override-rust-analyzer".to_string()]
 530        )
 531    });
 532}
 533
 534#[gpui::test]
 535async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 536    let fs = FakeFs::new(server_cx.executor());
 537    fs.insert_tree(
 538        path!("/code"),
 539        json!({
 540            "project1": {
 541                ".git": {},
 542                "README.md": "# project 1",
 543                "src": {
 544                    "lib.rs": "fn one() -> usize { 1 }"
 545                }
 546            },
 547        }),
 548    )
 549    .await;
 550
 551    let (project, headless) = init_test(&fs, cx, server_cx).await;
 552
 553    fs.insert_tree(
 554        path!("/code/project1/.zed"),
 555        json!({
 556            "settings.json": r#"
 557          {
 558            "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
 559            "lsp": {
 560              "rust-analyzer": {
 561                "binary": {
 562                  "path": "~/.cargo/bin/rust-analyzer"
 563                }
 564              },
 565              "fake-analyzer": {
 566               "binary": {
 567                "path": "~/.cargo/bin/rust-analyzer"
 568               }
 569              }
 570            }
 571          }"#
 572        }),
 573    )
 574    .await;
 575
 576    cx.update_entity(&project, |project, _| {
 577        project.languages().register_test_language(LanguageConfig {
 578            name: "Rust".into(),
 579            matcher: LanguageMatcher {
 580                path_suffixes: vec!["rs".into()],
 581                ..Default::default()
 582            },
 583            ..Default::default()
 584        });
 585        project.languages().register_fake_lsp_adapter(
 586            "Rust",
 587            FakeLspAdapter {
 588                name: "rust-analyzer",
 589                capabilities: lsp::ServerCapabilities {
 590                    completion_provider: Some(lsp::CompletionOptions::default()),
 591                    rename_provider: Some(lsp::OneOf::Left(true)),
 592                    ..lsp::ServerCapabilities::default()
 593                },
 594                ..FakeLspAdapter::default()
 595            },
 596        );
 597        project.languages().register_fake_lsp_adapter(
 598            "Rust",
 599            FakeLspAdapter {
 600                name: "fake-analyzer",
 601                capabilities: lsp::ServerCapabilities {
 602                    completion_provider: Some(lsp::CompletionOptions::default()),
 603                    rename_provider: Some(lsp::OneOf::Left(true)),
 604                    ..lsp::ServerCapabilities::default()
 605                },
 606                ..FakeLspAdapter::default()
 607            },
 608        )
 609    });
 610
 611    let mut fake_lsp = server_cx.update(|cx| {
 612        headless.read(cx).languages.register_fake_lsp_server(
 613            LanguageServerName("rust-analyzer".into()),
 614            lsp::ServerCapabilities {
 615                completion_provider: Some(lsp::CompletionOptions::default()),
 616                rename_provider: Some(lsp::OneOf::Left(true)),
 617                ..lsp::ServerCapabilities::default()
 618            },
 619            None,
 620        )
 621    });
 622
 623    let mut fake_second_lsp = server_cx.update(|cx| {
 624        headless.read(cx).languages.register_fake_lsp_adapter(
 625            "Rust",
 626            FakeLspAdapter {
 627                name: "fake-analyzer",
 628                capabilities: lsp::ServerCapabilities {
 629                    completion_provider: Some(lsp::CompletionOptions::default()),
 630                    rename_provider: Some(lsp::OneOf::Left(true)),
 631                    ..lsp::ServerCapabilities::default()
 632                },
 633                ..FakeLspAdapter::default()
 634            },
 635        );
 636        headless.read(cx).languages.register_fake_lsp_server(
 637            LanguageServerName("fake-analyzer".into()),
 638            lsp::ServerCapabilities {
 639                completion_provider: Some(lsp::CompletionOptions::default()),
 640                rename_provider: Some(lsp::OneOf::Left(true)),
 641                ..lsp::ServerCapabilities::default()
 642            },
 643            None,
 644        )
 645    });
 646
 647    cx.run_until_parked();
 648
 649    let worktree_id = project
 650        .update(cx, |project, cx| {
 651            project.languages().add(rust_lang());
 652            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 653        })
 654        .await
 655        .unwrap()
 656        .0
 657        .read_with(cx, |worktree, _| worktree.id());
 658
 659    // Wait for the settings to synchronize
 660    cx.run_until_parked();
 661
 662    let (buffer, _handle) = project
 663        .update(cx, |project, cx| {
 664            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
 665        })
 666        .await
 667        .unwrap();
 668    cx.run_until_parked();
 669
 670    let fake_lsp = fake_lsp.next().await.unwrap();
 671    let fake_second_lsp = fake_second_lsp.next().await.unwrap();
 672
 673    cx.read(|cx| {
 674        assert_eq!(
 675            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
 676            ["rust-analyzer".to_string(), "fake-analyzer".to_string()]
 677        )
 678    });
 679
 680    let buffer_id = cx.read(|cx| {
 681        let buffer = buffer.read(cx);
 682        assert_eq!(buffer.language().unwrap().name(), "Rust");
 683        buffer.remote_id()
 684    });
 685
 686    server_cx.read(|cx| {
 687        let buffer = headless
 688            .read(cx)
 689            .buffer_store
 690            .read(cx)
 691            .get(buffer_id)
 692            .unwrap();
 693
 694        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
 695    });
 696
 697    server_cx.read(|cx| {
 698        let lsp_store = headless.read(cx).lsp_store.read(cx);
 699        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
 700    });
 701
 702    fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 703        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 704            label: "boop".to_string(),
 705            ..Default::default()
 706        }])))
 707    });
 708
 709    fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 710        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 711            label: "beep".to_string(),
 712            ..Default::default()
 713        }])))
 714    });
 715
 716    let result = project
 717        .update(cx, |project, cx| {
 718            project.completions(
 719                &buffer,
 720                0,
 721                CompletionContext {
 722                    trigger_kind: CompletionTriggerKind::INVOKED,
 723                    trigger_character: None,
 724                },
 725                cx,
 726            )
 727        })
 728        .await
 729        .unwrap();
 730
 731    assert_eq!(
 732        result
 733            .into_iter()
 734            .flat_map(|response| response.completions)
 735            .map(|c| c.label.text)
 736            .collect::<Vec<_>>(),
 737        vec!["boop".to_string(), "beep".to_string()]
 738    );
 739
 740    fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
 741        Ok(Some(lsp::WorkspaceEdit {
 742            changes: Some(
 743                [(
 744                    lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
 745                    vec![lsp::TextEdit::new(
 746                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 747                        "two".to_string(),
 748                    )],
 749                )]
 750                .into_iter()
 751                .collect(),
 752            ),
 753            ..Default::default()
 754        }))
 755    });
 756
 757    project
 758        .update(cx, |project, cx| {
 759            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 760        })
 761        .await
 762        .unwrap();
 763
 764    cx.run_until_parked();
 765    buffer.update(cx, |buffer, _| {
 766        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 767    })
 768}
 769
 770#[gpui::test]
 771async fn test_remote_cancel_language_server_work(
 772    cx: &mut TestAppContext,
 773    server_cx: &mut TestAppContext,
 774) {
 775    let fs = FakeFs::new(server_cx.executor());
 776    fs.insert_tree(
 777        path!("/code"),
 778        json!({
 779            "project1": {
 780                ".git": {},
 781                "README.md": "# project 1",
 782                "src": {
 783                    "lib.rs": "fn one() -> usize { 1 }"
 784                }
 785            },
 786        }),
 787    )
 788    .await;
 789
 790    let (project, headless) = init_test(&fs, cx, server_cx).await;
 791
 792    fs.insert_tree(
 793        path!("/code/project1/.zed"),
 794        json!({
 795            "settings.json": r#"
 796          {
 797            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 798            "lsp": {
 799              "rust-analyzer": {
 800                "binary": {
 801                  "path": "~/.cargo/bin/rust-analyzer"
 802                }
 803              }
 804            }
 805          }"#
 806        }),
 807    )
 808    .await;
 809
 810    cx.update_entity(&project, |project, _| {
 811        project.languages().register_test_language(LanguageConfig {
 812            name: "Rust".into(),
 813            matcher: LanguageMatcher {
 814                path_suffixes: vec!["rs".into()],
 815                ..Default::default()
 816            },
 817            ..Default::default()
 818        });
 819        project.languages().register_fake_lsp_adapter(
 820            "Rust",
 821            FakeLspAdapter {
 822                name: "rust-analyzer",
 823                ..Default::default()
 824            },
 825        )
 826    });
 827
 828    let mut fake_lsp = server_cx.update(|cx| {
 829        headless.read(cx).languages.register_fake_lsp_server(
 830            LanguageServerName("rust-analyzer".into()),
 831            Default::default(),
 832            None,
 833        )
 834    });
 835
 836    cx.run_until_parked();
 837
 838    let worktree_id = project
 839        .update(cx, |project, cx| {
 840            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 841        })
 842        .await
 843        .unwrap()
 844        .0
 845        .read_with(cx, |worktree, _| worktree.id());
 846
 847    cx.run_until_parked();
 848
 849    let (buffer, _handle) = project
 850        .update(cx, |project, cx| {
 851            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
 852        })
 853        .await
 854        .unwrap();
 855
 856    cx.run_until_parked();
 857
 858    let mut fake_lsp = fake_lsp.next().await.unwrap();
 859
 860    // Cancelling all language server work for a given buffer
 861    {
 862        // Two operations, one cancellable and one not.
 863        fake_lsp
 864            .start_progress_with(
 865                "another-token",
 866                lsp::WorkDoneProgressBegin {
 867                    cancellable: Some(false),
 868                    ..Default::default()
 869                },
 870                DEFAULT_LSP_REQUEST_TIMEOUT,
 871            )
 872            .await;
 873
 874        let progress_token = "the-progress-token";
 875        fake_lsp
 876            .start_progress_with(
 877                progress_token,
 878                lsp::WorkDoneProgressBegin {
 879                    cancellable: Some(true),
 880                    ..Default::default()
 881                },
 882                DEFAULT_LSP_REQUEST_TIMEOUT,
 883            )
 884            .await;
 885
 886        cx.executor().run_until_parked();
 887
 888        project.update(cx, |project, cx| {
 889            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 890        });
 891
 892        cx.executor().run_until_parked();
 893
 894        // Verify the cancellation was received on the server side
 895        let cancel_notification = fake_lsp
 896            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 897            .await;
 898        assert_eq!(
 899            cancel_notification.token,
 900            lsp::NumberOrString::String(progress_token.into())
 901        );
 902    }
 903
 904    // Cancelling work by server_id and token
 905    {
 906        let server_id = fake_lsp.server.server_id();
 907        let progress_token = "the-progress-token";
 908
 909        fake_lsp
 910            .start_progress_with(
 911                progress_token,
 912                lsp::WorkDoneProgressBegin {
 913                    cancellable: Some(true),
 914                    ..Default::default()
 915                },
 916                DEFAULT_LSP_REQUEST_TIMEOUT,
 917            )
 918            .await;
 919
 920        cx.executor().run_until_parked();
 921
 922        project.update(cx, |project, cx| {
 923            project.cancel_language_server_work(
 924                server_id,
 925                Some(ProgressToken::String(SharedString::from(progress_token))),
 926                cx,
 927            )
 928        });
 929
 930        cx.executor().run_until_parked();
 931
 932        // Verify the cancellation was received on the server side
 933        let cancel_notification = fake_lsp
 934            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 935            .await;
 936        assert_eq!(
 937            cancel_notification.token,
 938            lsp::NumberOrString::String(progress_token.to_owned())
 939        );
 940    }
 941}
 942
 943#[gpui::test]
 944async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 945    let fs = FakeFs::new(server_cx.executor());
 946    fs.insert_tree(
 947        path!("/code"),
 948        json!({
 949            "project1": {
 950                ".git": {},
 951                "README.md": "# project 1",
 952                "src": {
 953                    "lib.rs": "fn one() -> usize { 1 }"
 954                }
 955            },
 956        }),
 957    )
 958    .await;
 959
 960    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 961    let (worktree, _) = project
 962        .update(cx, |project, cx| {
 963            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 964        })
 965        .await
 966        .unwrap();
 967
 968    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 969
 970    let buffer = project
 971        .update(cx, |project, cx| {
 972            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 973        })
 974        .await
 975        .unwrap();
 976
 977    fs.save(
 978        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 979        &("bangles".to_string().into()),
 980        LineEnding::Unix,
 981    )
 982    .await
 983    .unwrap();
 984
 985    cx.run_until_parked();
 986
 987    buffer.update(cx, |buffer, cx| {
 988        assert_eq!(buffer.text(), "bangles");
 989        buffer.edit([(0..0, "a")], None, cx);
 990    });
 991
 992    fs.save(
 993        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 994        &("bloop".to_string().into()),
 995        LineEnding::Unix,
 996    )
 997    .await
 998    .unwrap();
 999
1000    cx.run_until_parked();
1001    cx.update(|cx| {
1002        assert!(buffer.read(cx).has_conflict());
1003    });
1004
1005    project
1006        .update(cx, |project, cx| {
1007            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
1008        })
1009        .await
1010        .unwrap();
1011    cx.run_until_parked();
1012
1013    cx.update(|cx| {
1014        assert!(!buffer.read(cx).has_conflict());
1015    });
1016}
1017
1018#[gpui::test]
1019async fn test_remote_resolve_path_in_buffer(
1020    cx: &mut TestAppContext,
1021    server_cx: &mut TestAppContext,
1022) {
1023    let fs = FakeFs::new(server_cx.executor());
1024    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
1025    fs.insert_tree(
1026        path!("/code"),
1027        json!({
1028            "project1": {
1029                ".git": {},
1030                "README.md": "# project 1",
1031                "src": {
1032                    "lib.rs": "fn one() -> usize { 1 }"
1033                }
1034            },
1035            "project2": {
1036                ".git": {},
1037                "README.md": "# project 2",
1038                "src": {
1039                    "lib.rs": "fn two() -> usize { 2 }"
1040                }
1041            }
1042        }),
1043    )
1044    .await;
1045
1046    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1047
1048    let _ = project
1049        .update(cx, |project, cx| {
1050            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1051        })
1052        .await
1053        .unwrap();
1054
1055    let (worktree2, _) = project
1056        .update(cx, |project, cx| {
1057            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1058        })
1059        .await
1060        .unwrap();
1061
1062    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
1063
1064    cx.run_until_parked();
1065
1066    let buffer2 = project
1067        .update(cx, |project, cx| {
1068            project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
1069        })
1070        .await
1071        .unwrap();
1072
1073    let path = project
1074        .update(cx, |project, cx| {
1075            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
1076        })
1077        .await
1078        .unwrap();
1079    assert!(path.is_file());
1080    assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
1081
1082    let path = project
1083        .update(cx, |project, cx| {
1084            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
1085        })
1086        .await
1087        .unwrap();
1088    assert!(path.is_file());
1089    assert_eq!(
1090        path.project_path().unwrap().clone(),
1091        (worktree2_id, rel_path("README.md")).into()
1092    );
1093
1094    let path = project
1095        .update(cx, |project, cx| {
1096            project.resolve_path_in_buffer("../src", &buffer2, cx)
1097        })
1098        .await
1099        .unwrap();
1100    assert_eq!(
1101        path.project_path().unwrap().clone(),
1102        (worktree2_id, rel_path("src")).into()
1103    );
1104    assert!(path.is_dir());
1105}
1106
1107#[gpui::test]
1108async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1109    let fs = FakeFs::new(server_cx.executor());
1110    fs.insert_tree(
1111        path!("/code"),
1112        json!({
1113            "project1": {
1114                ".git": {},
1115                "README.md": "# project 1",
1116                "src": {
1117                    "lib.rs": "fn one() -> usize { 1 }"
1118                }
1119            },
1120        }),
1121    )
1122    .await;
1123
1124    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1125
1126    let path = project
1127        .update(cx, |project, cx| {
1128            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
1129        })
1130        .await
1131        .unwrap();
1132
1133    assert!(path.is_file());
1134    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
1135
1136    let path = project
1137        .update(cx, |project, cx| {
1138            project.resolve_abs_path(path!("/code/project1/src"), cx)
1139        })
1140        .await
1141        .unwrap();
1142
1143    assert!(path.is_dir());
1144    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
1145
1146    let path = project
1147        .update(cx, |project, cx| {
1148            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
1149        })
1150        .await;
1151    assert!(path.is_none());
1152}
1153
1154#[gpui::test(iterations = 10)]
1155async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1156    let fs = FakeFs::new(server_cx.executor());
1157    fs.insert_tree(
1158        "/code",
1159        json!({
1160            "project1": {
1161                ".git": {},
1162                "README.md": "# project 1",
1163                "src": {
1164                    "lib.rs": "fn one() -> usize { 1 }"
1165                }
1166            },
1167        }),
1168    )
1169    .await;
1170
1171    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1172    let (worktree, _) = project
1173        .update(cx, |project, cx| {
1174            project.find_or_create_worktree("/code/project1", true, cx)
1175        })
1176        .await
1177        .unwrap();
1178    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1179
1180    // Open a buffer on the client but cancel after a random amount of time.
1181    let buffer = project.update(cx, |p, cx| {
1182        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1183    });
1184    cx.executor().simulate_random_delay().await;
1185    drop(buffer);
1186
1187    // Try opening the same buffer again as the client, and ensure we can
1188    // still do it despite the cancellation above.
1189    let buffer = project
1190        .update(cx, |p, cx| {
1191            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1192        })
1193        .await
1194        .unwrap();
1195
1196    buffer.read_with(cx, |buf, _| {
1197        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
1198    });
1199}
1200
1201#[gpui::test]
1202async fn test_adding_then_removing_then_adding_worktrees(
1203    cx: &mut TestAppContext,
1204    server_cx: &mut TestAppContext,
1205) {
1206    let fs = FakeFs::new(server_cx.executor());
1207    fs.insert_tree(
1208        path!("/code"),
1209        json!({
1210            "project1": {
1211                ".git": {},
1212                "README.md": "# project 1",
1213                "src": {
1214                    "lib.rs": "fn one() -> usize { 1 }"
1215                }
1216            },
1217            "project2": {
1218                "README.md": "# project 2",
1219            },
1220        }),
1221    )
1222    .await;
1223
1224    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1225    let (_worktree, _) = project
1226        .update(cx, |project, cx| {
1227            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1228        })
1229        .await
1230        .unwrap();
1231
1232    let (worktree_2, _) = project
1233        .update(cx, |project, cx| {
1234            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1235        })
1236        .await
1237        .unwrap();
1238    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1239
1240    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1241
1242    let (worktree_2, _) = project
1243        .update(cx, |project, cx| {
1244            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1245        })
1246        .await
1247        .unwrap();
1248
1249    cx.run_until_parked();
1250    worktree_2.update(cx, |worktree, _cx| {
1251        assert!(worktree.is_visible());
1252        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1253        assert_eq!(entries.len(), 2);
1254        assert_eq!(entries[1].path.as_unix_str(), "README.md")
1255    })
1256}
1257
1258#[gpui::test]
1259async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1260    let fs = FakeFs::new(server_cx.executor());
1261    fs.insert_tree(
1262        path!("/code"),
1263        json!({
1264            "project1": {
1265                ".git": {},
1266                "README.md": "# project 1",
1267                "src": {
1268                    "lib.rs": "fn one() -> usize { 1 }"
1269                }
1270            },
1271        }),
1272    )
1273    .await;
1274
1275    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1276    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1277    cx.executor().run_until_parked();
1278
1279    let buffer = buffer.await.unwrap();
1280
1281    cx.update(|cx| {
1282        assert_eq!(
1283            buffer.read(cx).text(),
1284            initial_server_settings_content()
1285                .to_string()
1286                .replace("\r\n", "\n")
1287        )
1288    })
1289}
1290
1291#[gpui::test(iterations = 20)]
1292async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1293    let fs = FakeFs::new(server_cx.executor());
1294    fs.insert_tree(
1295        path!("/code"),
1296        json!({
1297            "project1": {
1298                ".git": {},
1299                "README.md": "# project 1",
1300                "src": {
1301                    "lib.rs": "fn one() -> usize { 1 }"
1302                }
1303            },
1304        }),
1305    )
1306    .await;
1307
1308    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1309
1310    let (worktree, _) = project
1311        .update(cx, |project, cx| {
1312            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1313        })
1314        .await
1315        .unwrap();
1316
1317    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1318    let buffer = project
1319        .update(cx, |project, cx| {
1320            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1321        })
1322        .await
1323        .unwrap();
1324
1325    buffer.update(cx, |buffer, cx| {
1326        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1327        let ix = buffer.text().find('1').unwrap();
1328        buffer.edit([(ix..ix + 1, "100")], None, cx);
1329    });
1330
1331    let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1332    client
1333        .update(cx, |client, cx| client.simulate_disconnect(cx))
1334        .detach();
1335
1336    project
1337        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1338        .await
1339        .unwrap();
1340
1341    assert_eq!(
1342        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1343            .await
1344            .unwrap(),
1345        "fn one() -> usize { 100 }"
1346    );
1347}
1348
1349#[gpui::test]
1350async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1351    let fs = FakeFs::new(server_cx.executor());
1352    fs.insert_tree(
1353        "/code",
1354        json!({
1355            "project1": {
1356                ".git": {},
1357                "README.md": "# project 1",
1358            },
1359        }),
1360    )
1361    .await;
1362
1363    let (project, _) = init_test(&fs, cx, server_cx).await;
1364
1365    let (worktree, _) = project
1366        .update(cx, |project, cx| {
1367            project.find_or_create_worktree("/code/project1", true, cx)
1368        })
1369        .await
1370        .unwrap();
1371
1372    cx.run_until_parked();
1373
1374    fs.rename(
1375        &PathBuf::from("/code/project1"),
1376        &PathBuf::from("/code/project2"),
1377        Default::default(),
1378    )
1379    .await
1380    .unwrap();
1381
1382    cx.run_until_parked();
1383    worktree.update(cx, |worktree, _| {
1384        assert_eq!(worktree.root_name(), "project2")
1385    })
1386}
1387
1388#[gpui::test]
1389async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1390    let fs = FakeFs::new(server_cx.executor());
1391    fs.insert_tree(
1392        "/code",
1393        json!({
1394            "project1": {
1395                ".git": {},
1396                "README.md": "# project 1",
1397            },
1398        }),
1399    )
1400    .await;
1401
1402    let (project, _) = init_test(&fs, cx, server_cx).await;
1403    let (worktree, _) = project
1404        .update(cx, |project, cx| {
1405            project.find_or_create_worktree("/code/project1", true, cx)
1406        })
1407        .await
1408        .unwrap();
1409
1410    cx.run_until_parked();
1411
1412    let entry = project
1413        .update(cx, |project, cx| {
1414            let worktree = worktree.read(cx);
1415            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1416            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1417        })
1418        .await
1419        .unwrap()
1420        .into_included()
1421        .unwrap();
1422
1423    cx.run_until_parked();
1424
1425    worktree.update(cx, |worktree, _| {
1426        assert_eq!(
1427            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1428            entry.id
1429        )
1430    });
1431}
1432
1433#[gpui::test]
1434async fn test_copy_file_into_remote_project(
1435    cx: &mut TestAppContext,
1436    server_cx: &mut TestAppContext,
1437) {
1438    let remote_fs = FakeFs::new(server_cx.executor());
1439    remote_fs
1440        .insert_tree(
1441            path!("/code"),
1442            json!({
1443                "project1": {
1444                    ".git": {},
1445                    "README.md": "# project 1",
1446                    "src": {
1447                        "main.rs": ""
1448                    }
1449                },
1450            }),
1451        )
1452        .await;
1453
1454    let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1455    let (worktree, _) = project
1456        .update(cx, |project, cx| {
1457            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1458        })
1459        .await
1460        .unwrap();
1461
1462    cx.run_until_parked();
1463
1464    let local_fs = project
1465        .read_with(cx, |project, _| project.fs().clone())
1466        .as_fake();
1467    local_fs
1468        .insert_tree(
1469            path!("/local-code"),
1470            json!({
1471                "dir1": {
1472                    "file1": "file 1 content",
1473                    "dir2": {
1474                        "file2": "file 2 content",
1475                        "dir3": {
1476                            "file3": ""
1477                        },
1478                        "dir4": {}
1479                    },
1480                    "dir5": {}
1481                },
1482                "file4": "file 4 content"
1483            }),
1484        )
1485        .await;
1486
1487    worktree
1488        .update(cx, |worktree, cx| {
1489            worktree.copy_external_entries(
1490                rel_path("src").into(),
1491                vec![
1492                    Path::new(path!("/local-code/dir1/file1")).into(),
1493                    Path::new(path!("/local-code/dir1/dir2")).into(),
1494                ],
1495                local_fs.clone(),
1496                cx,
1497            )
1498        })
1499        .await
1500        .unwrap();
1501
1502    assert_eq!(
1503        remote_fs.paths(true),
1504        vec![
1505            PathBuf::from(path!("/")),
1506            PathBuf::from(path!("/code")),
1507            PathBuf::from(path!("/code/project1")),
1508            PathBuf::from(path!("/code/project1/.git")),
1509            PathBuf::from(path!("/code/project1/README.md")),
1510            PathBuf::from(path!("/code/project1/src")),
1511            PathBuf::from(path!("/code/project1/src/dir2")),
1512            PathBuf::from(path!("/code/project1/src/file1")),
1513            PathBuf::from(path!("/code/project1/src/main.rs")),
1514            PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1515            PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1516            PathBuf::from(path!("/code/project1/src/dir2/file2")),
1517            PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1518        ]
1519    );
1520    assert_eq!(
1521        remote_fs
1522            .load(path!("/code/project1/src/file1").as_ref())
1523            .await
1524            .unwrap(),
1525        "file 1 content"
1526    );
1527    assert_eq!(
1528        remote_fs
1529            .load(path!("/code/project1/src/dir2/file2").as_ref())
1530            .await
1531            .unwrap(),
1532        "file 2 content"
1533    );
1534    assert_eq!(
1535        remote_fs
1536            .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1537            .await
1538            .unwrap(),
1539        ""
1540    );
1541}
1542
1543#[gpui::test]
1544async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1545    let fs = FakeFs::new(server_cx.executor());
1546    fs.insert_tree(
1547        "/code",
1548        json!({
1549            "main_repo": {
1550                ".git": {},
1551                "file.txt": "content",
1552            },
1553            "no_git": {
1554                "file.txt": "content",
1555            },
1556        }),
1557    )
1558    .await;
1559
1560    // Create a linked worktree that points back to main_repo's .git.
1561    fs.add_linked_worktree_for_repo(
1562        Path::new("/code/main_repo/.git"),
1563        false,
1564        GitWorktree {
1565            path: PathBuf::from("/code/linked_worktree"),
1566            ref_name: Some("refs/heads/feature-branch".into()),
1567            sha: "abc123".into(),
1568            is_main: false,
1569            is_bare: false,
1570        },
1571    )
1572    .await;
1573
1574    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1575
1576    // Main repo: root_repo_common_dir should be the .git directory itself.
1577    let (worktree_main, _) = project
1578        .update(cx, |project, cx| {
1579            project.find_or_create_worktree("/code/main_repo", true, cx)
1580        })
1581        .await
1582        .unwrap();
1583    cx.executor().run_until_parked();
1584
1585    let common_dir = worktree_main.read_with(cx, |worktree, _| {
1586        worktree.snapshot().root_repo_common_dir().cloned()
1587    });
1588    assert_eq!(
1589        common_dir.as_deref(),
1590        Some(Path::new("/code/main_repo/.git")),
1591    );
1592
1593    // Linked worktree: root_repo_common_dir should point to the main repo's .git.
1594    let (worktree_linked, _) = project
1595        .update(cx, |project, cx| {
1596            project.find_or_create_worktree("/code/linked_worktree", true, cx)
1597        })
1598        .await
1599        .unwrap();
1600    cx.executor().run_until_parked();
1601
1602    let common_dir = worktree_linked.read_with(cx, |worktree, _| {
1603        worktree.snapshot().root_repo_common_dir().cloned()
1604    });
1605    assert_eq!(
1606        common_dir.as_deref(),
1607        Some(Path::new("/code/main_repo/.git")),
1608    );
1609
1610    // No git repo: root_repo_common_dir should be None.
1611    let (worktree_no_git, _) = project
1612        .update(cx, |project, cx| {
1613            project.find_or_create_worktree("/code/no_git", true, cx)
1614        })
1615        .await
1616        .unwrap();
1617    cx.executor().run_until_parked();
1618
1619    let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
1620        worktree.snapshot().root_repo_common_dir().cloned()
1621    });
1622    assert_eq!(common_dir, None);
1623}
1624
1625#[gpui::test]
1626async fn test_remote_archive_git_operations_are_supported(
1627    cx: &mut TestAppContext,
1628    server_cx: &mut TestAppContext,
1629) {
1630    let fs = FakeFs::new(server_cx.executor());
1631    fs.insert_tree(
1632        "/project",
1633        json!({
1634            ".git": {},
1635            "file.txt": "content",
1636        }),
1637    )
1638    .await;
1639    fs.set_branch_name(Path::new("/project/.git"), Some("main"));
1640    fs.set_head_for_repo(
1641        Path::new("/project/.git"),
1642        &[("file.txt", "content".into())],
1643        "head-sha",
1644    );
1645
1646    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1647    project
1648        .update(cx, |project, cx| {
1649            project.find_or_create_worktree(Path::new("/project"), true, cx)
1650        })
1651        .await
1652        .expect("should open remote worktree");
1653    cx.run_until_parked();
1654
1655    let repository = project.read_with(cx, |project, cx| {
1656        project
1657            .active_repository(cx)
1658            .expect("remote project should have an active repository")
1659    });
1660
1661    let head_sha = cx
1662        .update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
1663        .await
1664        .expect("head_sha request should complete")
1665        .expect("head_sha should succeed")
1666        .expect("HEAD should exist");
1667
1668    cx.run_until_parked();
1669
1670    cx.update(|cx| {
1671        repository.update(cx, |repository, _| {
1672            repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
1673        })
1674    })
1675    .await
1676    .expect("update_ref request should complete")
1677    .expect("update_ref should succeed for remote repository");
1678
1679    cx.run_until_parked();
1680
1681    cx.update(|cx| {
1682        repository.update(cx, |repository, _| {
1683            repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
1684        })
1685    })
1686    .await
1687    .expect("delete_ref request should complete")
1688    .expect("delete_ref should succeed for remote repository");
1689
1690    cx.run_until_parked();
1691
1692    cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
1693        .await
1694        .expect("repair_worktrees request should complete")
1695        .expect("repair_worktrees should succeed for remote repository");
1696
1697    cx.run_until_parked();
1698
1699    let (staged_commit_sha, unstaged_commit_sha) = cx
1700        .update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
1701        .await
1702        .expect("create_archive_checkpoint request should complete")
1703        .expect("create_archive_checkpoint should succeed for remote repository");
1704
1705    cx.run_until_parked();
1706
1707    cx.update(|cx| {
1708        repository.update(cx, |repository, _| {
1709            repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
1710        })
1711    })
1712    .await
1713    .expect("restore_archive_checkpoint request should complete")
1714    .expect("restore_archive_checkpoint should succeed for remote repository");
1715}
1716
1717#[gpui::test]
1718async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1719    let text_2 = "
1720        fn one() -> usize {
1721            1
1722        }
1723    "
1724    .unindent();
1725    let text_1 = "
1726        fn one() -> usize {
1727            0
1728        }
1729    "
1730    .unindent();
1731
1732    let fs = FakeFs::new(server_cx.executor());
1733    fs.insert_tree(
1734        "/code",
1735        json!({
1736            "project1": {
1737                ".git": {},
1738                "src": {
1739                    "lib.rs": text_2
1740                },
1741                "README.md": "# project 1",
1742            },
1743        }),
1744    )
1745    .await;
1746    fs.set_index_for_repo(
1747        Path::new("/code/project1/.git"),
1748        &[("src/lib.rs", text_1.clone())],
1749    );
1750    fs.set_head_for_repo(
1751        Path::new("/code/project1/.git"),
1752        &[("src/lib.rs", text_1.clone())],
1753        "deadbeef",
1754    );
1755
1756    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1757    let (worktree, _) = project
1758        .update(cx, |project, cx| {
1759            project.find_or_create_worktree("/code/project1", true, cx)
1760        })
1761        .await
1762        .unwrap();
1763    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1764    cx.executor().run_until_parked();
1765
1766    let buffer = project
1767        .update(cx, |project, cx| {
1768            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1769        })
1770        .await
1771        .unwrap();
1772    let diff = project
1773        .update(cx, |project, cx| {
1774            project.open_uncommitted_diff(buffer.clone(), cx)
1775        })
1776        .await
1777        .unwrap();
1778
1779    diff.read_with(cx, |diff, cx| {
1780        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1781        assert_eq!(
1782            diff.secondary_diff()
1783                .unwrap()
1784                .read(cx)
1785                .base_text_string(cx)
1786                .unwrap(),
1787            text_1
1788        );
1789    });
1790
1791    // stage the current buffer's contents
1792    fs.set_index_for_repo(
1793        Path::new("/code/project1/.git"),
1794        &[("src/lib.rs", text_2.clone())],
1795    );
1796
1797    cx.executor().run_until_parked();
1798    diff.read_with(cx, |diff, cx| {
1799        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1800        assert_eq!(
1801            diff.secondary_diff()
1802                .unwrap()
1803                .read(cx)
1804                .base_text_string(cx)
1805                .unwrap(),
1806            text_2
1807        );
1808    });
1809
1810    // commit the current buffer's contents
1811    fs.set_head_for_repo(
1812        Path::new("/code/project1/.git"),
1813        &[("src/lib.rs", text_2.clone())],
1814        "deadbeef",
1815    );
1816
1817    cx.executor().run_until_parked();
1818    diff.read_with(cx, |diff, cx| {
1819        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1820        assert_eq!(
1821            diff.secondary_diff()
1822                .unwrap()
1823                .read(cx)
1824                .base_text_string(cx)
1825                .unwrap(),
1826            text_2
1827        );
1828    });
1829}
1830
1831#[gpui::test]
1832async fn test_remote_git_diffs_when_recv_update_repository_delay(
1833    cx: &mut TestAppContext,
1834    server_cx: &mut TestAppContext,
1835) {
1836    cx.update(|cx| {
1837        let settings_store = SettingsStore::test(cx);
1838        cx.set_global(settings_store);
1839        theme_settings::init(theme::LoadThemes::JustBase, cx);
1840        release_channel::init(semver::Version::new(0, 0, 0), cx);
1841        editor::init(cx);
1842    });
1843
1844    use editor::Editor;
1845    use gpui::VisualContext;
1846    let text_2 = "
1847        fn one() -> usize {
1848            1
1849        }
1850    "
1851    .unindent();
1852    let text_1 = "
1853        fn one() -> usize {
1854            0
1855        }
1856    "
1857    .unindent();
1858
1859    let fs = FakeFs::new(server_cx.executor());
1860    fs.insert_tree(
1861        path!("/code"),
1862        json!({
1863            "project1": {
1864                "src": {
1865                    "lib.rs": text_2
1866                },
1867                "README.md": "# project 1",
1868            },
1869        }),
1870    )
1871    .await;
1872
1873    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1874    let (worktree, _) = project
1875        .update(cx, |project, cx| {
1876            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1877        })
1878        .await
1879        .unwrap();
1880    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1881    let buffer = project
1882        .update(cx, |project, cx| {
1883            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1884        })
1885        .await
1886        .unwrap();
1887    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1888
1889    let cx = cx.add_empty_window();
1890    let editor = cx.new_window_entity(|window, cx| {
1891        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1892    });
1893
1894    // Remote server will send proto::UpdateRepository after the instance of Editor create.
1895    fs.insert_tree(
1896        path!("/code"),
1897        json!({
1898            "project1": {
1899                ".git": {},
1900            },
1901        }),
1902    )
1903    .await;
1904
1905    fs.set_index_for_repo(
1906        Path::new(path!("/code/project1/.git")),
1907        &[("src/lib.rs", text_1.clone())],
1908    );
1909    fs.set_head_for_repo(
1910        Path::new(path!("/code/project1/.git")),
1911        &[("src/lib.rs", text_1.clone())],
1912        "sha",
1913    );
1914
1915    cx.executor().run_until_parked();
1916    let diff = editor
1917        .read_with(cx, |editor, cx| {
1918            editor
1919                .buffer()
1920                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1921        })
1922        .unwrap();
1923
1924    diff.read_with(cx, |diff, cx| {
1925        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1926        assert_eq!(
1927            diff.secondary_diff()
1928                .unwrap()
1929                .read(cx)
1930                .base_text_string(cx)
1931                .unwrap(),
1932            text_1
1933        );
1934    });
1935
1936    // stage the current buffer's contents
1937    fs.set_index_for_repo(
1938        Path::new(path!("/code/project1/.git")),
1939        &[("src/lib.rs", text_2.clone())],
1940    );
1941
1942    cx.executor().run_until_parked();
1943    diff.read_with(cx, |diff, cx| {
1944        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1945        assert_eq!(
1946            diff.secondary_diff()
1947                .unwrap()
1948                .read(cx)
1949                .base_text_string(cx)
1950                .unwrap(),
1951            text_2
1952        );
1953    });
1954
1955    // commit the current buffer's contents
1956    fs.set_head_for_repo(
1957        Path::new(path!("/code/project1/.git")),
1958        &[("src/lib.rs", text_2.clone())],
1959        "sha",
1960    );
1961
1962    cx.executor().run_until_parked();
1963    diff.read_with(cx, |diff, cx| {
1964        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1965        assert_eq!(
1966            diff.secondary_diff()
1967                .unwrap()
1968                .read(cx)
1969                .base_text_string(cx)
1970                .unwrap(),
1971            text_2
1972        );
1973    });
1974}
1975
1976#[gpui::test]
1977async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1978    let fs = FakeFs::new(server_cx.executor());
1979    fs.insert_tree(
1980        path!("/code"),
1981        json!({
1982            "project1": {
1983                ".git": {},
1984                "README.md": "# project 1",
1985            },
1986        }),
1987    )
1988    .await;
1989
1990    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1991    let branches = ["main", "dev", "feature-1"];
1992    let branches_set = branches
1993        .iter()
1994        .map(ToString::to_string)
1995        .collect::<HashSet<_>>();
1996    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1997
1998    let (_worktree, _) = project
1999        .update(cx, |project, cx| {
2000            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2001        })
2002        .await
2003        .unwrap();
2004    // Give the worktree a bit of time to index the file system
2005    cx.run_until_parked();
2006
2007    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2008
2009    let remote_branches = repository
2010        .update(cx, |repository, _| repository.branches())
2011        .await
2012        .unwrap()
2013        .unwrap();
2014
2015    let new_branch = branches[2];
2016
2017    let remote_branches = remote_branches
2018        .into_iter()
2019        .map(|branch| branch.name().to_string())
2020        .collect::<HashSet<_>>();
2021
2022    assert_eq!(&remote_branches, &branches_set);
2023
2024    cx.update(|cx| {
2025        repository.update(cx, |repository, _cx| {
2026            repository.change_branch(new_branch.to_string())
2027        })
2028    })
2029    .await
2030    .unwrap()
2031    .unwrap();
2032
2033    cx.run_until_parked();
2034
2035    let server_branch = server_cx.update(|cx| {
2036        headless_project.update(cx, |headless_project, cx| {
2037            headless_project.git_store.update(cx, |git_store, cx| {
2038                git_store
2039                    .repositories()
2040                    .values()
2041                    .next()
2042                    .unwrap()
2043                    .read(cx)
2044                    .branch
2045                    .as_ref()
2046                    .unwrap()
2047                    .clone()
2048            })
2049        })
2050    });
2051
2052    assert_eq!(server_branch.name(), branches[2]);
2053
2054    // Also try creating a new branch
2055    cx.update(|cx| {
2056        repository.update(cx, |repo, _cx| {
2057            repo.create_branch("totally-new-branch".to_string(), None)
2058        })
2059    })
2060    .await
2061    .unwrap()
2062    .unwrap();
2063
2064    cx.update(|cx| {
2065        repository.update(cx, |repo, _cx| {
2066            repo.change_branch("totally-new-branch".to_string())
2067        })
2068    })
2069    .await
2070    .unwrap()
2071    .unwrap();
2072
2073    cx.run_until_parked();
2074
2075    let server_branch = server_cx.update(|cx| {
2076        headless_project.update(cx, |headless_project, cx| {
2077            headless_project.git_store.update(cx, |git_store, cx| {
2078                git_store
2079                    .repositories()
2080                    .values()
2081                    .next()
2082                    .unwrap()
2083                    .read(cx)
2084                    .branch
2085                    .as_ref()
2086                    .unwrap()
2087                    .clone()
2088            })
2089        })
2090    });
2091
2092    assert_eq!(server_branch.name(), "totally-new-branch");
2093}
2094
2095#[gpui::test]
2096async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2097    let fs = FakeFs::new(server_cx.executor());
2098    fs.insert_tree(
2099        path!("/code"),
2100        json!({
2101            "project1": {
2102                ".git": {},
2103                "file.txt": "original content",
2104            },
2105        }),
2106    )
2107    .await;
2108
2109    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2110
2111    let (_worktree, _) = project
2112        .update(cx, |project, cx| {
2113            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2114        })
2115        .await
2116        .unwrap();
2117    cx.run_until_parked();
2118
2119    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2120
2121    // 1. Create a checkpoint of the original state
2122    let checkpoint_1 = repository
2123        .update(cx, |repository, _| repository.checkpoint())
2124        .await
2125        .unwrap()
2126        .unwrap();
2127
2128    // 2. Modify a file on the server-side fs
2129    fs.write(
2130        Path::new(path!("/code/project1/file.txt")),
2131        b"modified content",
2132    )
2133    .await
2134    .unwrap();
2135
2136    // 3. Create a second checkpoint with the modified state
2137    let checkpoint_2 = repository
2138        .update(cx, |repository, _| repository.checkpoint())
2139        .await
2140        .unwrap()
2141        .unwrap();
2142
2143    // 4. compare_checkpoints: same checkpoint with itself => equal
2144    let equal = repository
2145        .update(cx, |repository, _| {
2146            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2147        })
2148        .await
2149        .unwrap()
2150        .unwrap();
2151    assert!(equal, "a checkpoint compared with itself should be equal");
2152
2153    // 5. compare_checkpoints: different states => not equal
2154    let equal = repository
2155        .update(cx, |repository, _| {
2156            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2157        })
2158        .await
2159        .unwrap()
2160        .unwrap();
2161    assert!(
2162        !equal,
2163        "checkpoints of different states should not be equal"
2164    );
2165
2166    // 6. diff_checkpoints: same checkpoint => empty diff
2167    let diff = repository
2168        .update(cx, |repository, _| {
2169            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2170        })
2171        .await
2172        .unwrap()
2173        .unwrap();
2174    assert!(
2175        diff.is_empty(),
2176        "diff of identical checkpoints should be empty"
2177    );
2178
2179    // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
2180    let diff = repository
2181        .update(cx, |repository, _| {
2182            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2183        })
2184        .await
2185        .unwrap()
2186        .unwrap();
2187    assert!(
2188        !diff.is_empty(),
2189        "diff of different checkpoints should be non-empty"
2190    );
2191    assert!(
2192        diff.contains("file.txt"),
2193        "diff should mention the changed file"
2194    );
2195    assert!(
2196        diff.contains("original content"),
2197        "diff should contain removed content"
2198    );
2199    assert!(
2200        diff.contains("modified content"),
2201        "diff should contain added content"
2202    );
2203
2204    // 8. restore_checkpoint: restore to original state
2205    repository
2206        .update(cx, |repository, _| {
2207            repository.restore_checkpoint(checkpoint_1.clone())
2208        })
2209        .await
2210        .unwrap()
2211        .unwrap();
2212    cx.run_until_parked();
2213
2214    // 9. Create a checkpoint after restore
2215    let checkpoint_3 = repository
2216        .update(cx, |repository, _| repository.checkpoint())
2217        .await
2218        .unwrap()
2219        .unwrap();
2220
2221    // 10. compare_checkpoints: restored state matches original
2222    let equal = repository
2223        .update(cx, |repository, _| {
2224            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2225        })
2226        .await
2227        .unwrap()
2228        .unwrap();
2229    assert!(equal, "restored state should match original checkpoint");
2230
2231    // 11. diff_checkpoints: restored state vs original => empty diff
2232    let diff = repository
2233        .update(cx, |repository, _| {
2234            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2235        })
2236        .await
2237        .unwrap()
2238        .unwrap();
2239    assert!(diff.is_empty(), "diff after restore should be empty");
2240}
2241
2242#[gpui::test]
2243async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2244    let fs = FakeFs::new(server_cx.executor());
2245    fs.insert_tree(
2246        path!("/project"),
2247        json!({
2248            "a.txt": "A",
2249            "b.txt": "B",
2250        }),
2251    )
2252    .await;
2253
2254    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2255    project
2256        .update(cx, |project, cx| {
2257            project.find_or_create_worktree(path!("/project"), true, cx)
2258        })
2259        .await
2260        .unwrap();
2261
2262    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
2263
2264    let input = ReadFileToolInput {
2265        path: "project/b.txt".into(),
2266        start_line: None,
2267        end_line: None,
2268    };
2269    let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
2270    let (event_stream, _) = ToolCallEventStream::test();
2271
2272    let exists_result = cx.update(|cx| {
2273        read_tool
2274            .clone()
2275            .run(ToolInput::resolved(input), event_stream.clone(), cx)
2276    });
2277    let output = exists_result.await.unwrap();
2278    assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
2279
2280    let input = ReadFileToolInput {
2281        path: "project/c.txt".into(),
2282        start_line: None,
2283        end_line: None,
2284    };
2285    let does_not_exist_result =
2286        cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
2287    does_not_exist_result.await.unwrap_err();
2288}
2289
2290#[gpui::test]
2291async fn test_remote_external_agent_server(
2292    cx: &mut TestAppContext,
2293    server_cx: &mut TestAppContext,
2294) {
2295    let fs = FakeFs::new(server_cx.executor());
2296    fs.insert_tree(path!("/project"), json!({})).await;
2297
2298    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2299    project
2300        .update(cx, |project, cx| {
2301            project.find_or_create_worktree(path!("/project"), true, cx)
2302        })
2303        .await
2304        .unwrap();
2305    let names = project.update(cx, |project, cx| {
2306        project
2307            .agent_server_store()
2308            .read(cx)
2309            .external_agents()
2310            .map(|name| name.to_string())
2311            .collect::<Vec<_>>()
2312    });
2313    pretty_assertions::assert_eq!(names, Vec::<String>::new());
2314    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
2315        settings_store
2316            .set_server_settings(
2317                &json!({
2318                    "agent_servers": {
2319                        "foo": {
2320                            "type": "custom",
2321                            "command": "foo-cli",
2322                            "args": ["--flag"],
2323                            "env": {
2324                                "VAR": "val"
2325                            }
2326                        }
2327                    }
2328                })
2329                .to_string(),
2330                cx,
2331            )
2332            .unwrap();
2333    });
2334    server_cx.run_until_parked();
2335    cx.run_until_parked();
2336    let names = project.update(cx, |project, cx| {
2337        project
2338            .agent_server_store()
2339            .read(cx)
2340            .external_agents()
2341            .map(|name| name.to_string())
2342            .collect::<Vec<_>>()
2343    });
2344    pretty_assertions::assert_eq!(names, ["foo"]);
2345    let command = project
2346        .update(cx, |project, cx| {
2347            project.agent_server_store().update(cx, |store, cx| {
2348                store
2349                    .get_external_agent(&"foo".into())
2350                    .unwrap()
2351                    .get_command(
2352                        vec![],
2353                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
2354                        &mut cx.to_async(),
2355                    )
2356            })
2357        })
2358        .await
2359        .unwrap();
2360    assert_eq!(
2361        command,
2362        AgentServerCommand {
2363            path: "foo-cli".into(),
2364            args: vec!["--flag".into()],
2365            env: Some(HashMap::from_iter([
2366                ("NO_BROWSER".into(), "1".into()),
2367                ("VAR".into(), "val".into()),
2368                ("OTHER_VAR".into(), "other-val".into())
2369            ]))
2370        }
2371    );
2372}
2373
2374pub async fn init_test(
2375    server_fs: &Arc<FakeFs>,
2376    cx: &mut TestAppContext,
2377    server_cx: &mut TestAppContext,
2378) -> (Entity<Project>, Entity<HeadlessProject>) {
2379    let server_fs = server_fs.clone();
2380    cx.update(|cx| {
2381        release_channel::init(semver::Version::new(0, 0, 0), cx);
2382    });
2383    server_cx.update(|cx| {
2384        release_channel::init(semver::Version::new(0, 0, 0), cx);
2385    });
2386    init_logger();
2387
2388    let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
2389    let http_client = Arc::new(BlockedHttpClient);
2390    let node_runtime = NodeRuntime::unavailable();
2391    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
2392    let proxy = Arc::new(ExtensionHostProxy::new());
2393    server_cx.update(HeadlessProject::init);
2394    let headless = server_cx.new(|cx| {
2395        HeadlessProject::new(
2396            crate::HeadlessAppState {
2397                session: ssh_server_client,
2398                fs: server_fs.clone(),
2399                http_client,
2400                node_runtime,
2401                languages,
2402                extension_host_proxy: proxy,
2403                startup_time: std::time::Instant::now(),
2404            },
2405            false,
2406            cx,
2407        )
2408    });
2409
2410    let ssh = RemoteClient::connect_mock(opts, cx).await;
2411    let project = build_project(ssh, cx);
2412    project
2413        .update(cx, {
2414            let headless = headless.clone();
2415            |_, cx| cx.on_release(|_, _| drop(headless))
2416        })
2417        .detach();
2418    (project, headless)
2419}
2420
2421fn init_logger() {
2422    zlog::init_test();
2423}
2424
2425fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
2426    cx.update(|cx| {
2427        if !cx.has_global::<SettingsStore>() {
2428            let settings_store = SettingsStore::test(cx);
2429            cx.set_global(settings_store);
2430        }
2431    });
2432
2433    let client = cx.update(|cx| {
2434        Client::new(
2435            Arc::new(FakeSystemClock::new()),
2436            FakeHttpClient::with_404_response(),
2437            cx,
2438        )
2439    });
2440
2441    let node = NodeRuntime::unavailable();
2442    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2443    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
2444    let fs = FakeFs::new(cx.executor());
2445
2446    cx.update(|cx| {
2447        Project::init(&client, cx);
2448    });
2449
2450    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
2451}