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