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