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