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,
  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, rel_path::rel_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", "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().collect::<Vec<_>>(),
  77            vec![
  78                rel_path("README.md"),
  79                rel_path("src"),
  80                rel_path("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, rel_path("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().collect::<Vec<_>>(),
 134            vec![
 135                rel_path("README.md"),
 136                rel_path("src"),
 137                rel_path("src/lib.rs"),
 138                rel_path("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(), rel_path("src/lib2.rs"));
 154    });
 155
 156    fs.set_index_for_repo(
 157        Path::new(path!("/code/project1/.git")),
 158        &[("src/lib2.rs", "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, rel_path("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: rel_path("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, rel_path("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, rel_path("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, rel_path("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, rel_path("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!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
 867
 868    let path = project
 869        .update(cx, |project, cx| {
 870            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
 871        })
 872        .await
 873        .unwrap();
 874    assert!(path.is_file());
 875    assert_eq!(
 876        path.project_path().unwrap().clone(),
 877        (worktree2_id, rel_path("README.md")).into()
 878    );
 879
 880    let path = project
 881        .update(cx, |project, cx| {
 882            project.resolve_path_in_buffer("../src", &buffer2, cx)
 883        })
 884        .await
 885        .unwrap();
 886    assert_eq!(
 887        path.project_path().unwrap().clone(),
 888        (worktree2_id, rel_path("src")).into()
 889    );
 890    assert!(path.is_dir());
 891}
 892
 893#[gpui::test]
 894async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 895    let fs = FakeFs::new(server_cx.executor());
 896    fs.insert_tree(
 897        path!("/code"),
 898        json!({
 899            "project1": {
 900                ".git": {},
 901                "README.md": "# project 1",
 902                "src": {
 903                    "lib.rs": "fn one() -> usize { 1 }"
 904                }
 905            },
 906        }),
 907    )
 908    .await;
 909
 910    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 911
 912    let path = project
 913        .update(cx, |project, cx| {
 914            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 915        })
 916        .await
 917        .unwrap();
 918
 919    assert!(path.is_file());
 920    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
 921
 922    let path = project
 923        .update(cx, |project, cx| {
 924            project.resolve_abs_path(path!("/code/project1/src"), cx)
 925        })
 926        .await
 927        .unwrap();
 928
 929    assert!(path.is_dir());
 930    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
 931
 932    let path = project
 933        .update(cx, |project, cx| {
 934            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 935        })
 936        .await;
 937    assert!(path.is_none());
 938}
 939
 940#[gpui::test(iterations = 10)]
 941async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 942    let fs = FakeFs::new(server_cx.executor());
 943    fs.insert_tree(
 944        "/code",
 945        json!({
 946            "project1": {
 947                ".git": {},
 948                "README.md": "# project 1",
 949                "src": {
 950                    "lib.rs": "fn one() -> usize { 1 }"
 951                }
 952            },
 953        }),
 954    )
 955    .await;
 956
 957    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 958    let (worktree, _) = project
 959        .update(cx, |project, cx| {
 960            project.find_or_create_worktree("/code/project1", true, cx)
 961        })
 962        .await
 963        .unwrap();
 964    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 965
 966    // Open a buffer on the client but cancel after a random amount of time.
 967    let buffer = project.update(cx, |p, cx| {
 968        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 969    });
 970    cx.executor().simulate_random_delay().await;
 971    drop(buffer);
 972
 973    // Try opening the same buffer again as the client, and ensure we can
 974    // still do it despite the cancellation above.
 975    let buffer = project
 976        .update(cx, |p, cx| {
 977            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 978        })
 979        .await
 980        .unwrap();
 981
 982    buffer.read_with(cx, |buf, _| {
 983        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 984    });
 985}
 986
 987#[gpui::test]
 988async fn test_adding_then_removing_then_adding_worktrees(
 989    cx: &mut TestAppContext,
 990    server_cx: &mut TestAppContext,
 991) {
 992    let fs = FakeFs::new(server_cx.executor());
 993    fs.insert_tree(
 994        path!("/code"),
 995        json!({
 996            "project1": {
 997                ".git": {},
 998                "README.md": "# project 1",
 999                "src": {
1000                    "lib.rs": "fn one() -> usize { 1 }"
1001                }
1002            },
1003            "project2": {
1004                "README.md": "# project 2",
1005            },
1006        }),
1007    )
1008    .await;
1009
1010    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1011    let (_worktree, _) = project
1012        .update(cx, |project, cx| {
1013            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1014        })
1015        .await
1016        .unwrap();
1017
1018    let (worktree_2, _) = project
1019        .update(cx, |project, cx| {
1020            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1021        })
1022        .await
1023        .unwrap();
1024    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1025
1026    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1027
1028    let (worktree_2, _) = project
1029        .update(cx, |project, cx| {
1030            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1031        })
1032        .await
1033        .unwrap();
1034
1035    cx.run_until_parked();
1036    worktree_2.update(cx, |worktree, _cx| {
1037        assert!(worktree.is_visible());
1038        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1039        assert_eq!(entries.len(), 2);
1040        assert_eq!(entries[1].path.as_str(), "README.md")
1041    })
1042}
1043
1044#[gpui::test]
1045async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1046    let fs = FakeFs::new(server_cx.executor());
1047    fs.insert_tree(
1048        path!("/code"),
1049        json!({
1050            "project1": {
1051                ".git": {},
1052                "README.md": "# project 1",
1053                "src": {
1054                    "lib.rs": "fn one() -> usize { 1 }"
1055                }
1056            },
1057        }),
1058    )
1059    .await;
1060
1061    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1062    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1063    cx.executor().run_until_parked();
1064
1065    let buffer = buffer.await.unwrap();
1066
1067    cx.update(|cx| {
1068        assert_eq!(
1069            buffer.read(cx).text(),
1070            initial_server_settings_content()
1071                .to_string()
1072                .replace("\r\n", "\n")
1073        )
1074    })
1075}
1076
1077#[gpui::test(iterations = 20)]
1078async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1079    let fs = FakeFs::new(server_cx.executor());
1080    fs.insert_tree(
1081        path!("/code"),
1082        json!({
1083            "project1": {
1084                ".git": {},
1085                "README.md": "# project 1",
1086                "src": {
1087                    "lib.rs": "fn one() -> usize { 1 }"
1088                }
1089            },
1090        }),
1091    )
1092    .await;
1093
1094    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1095
1096    let (worktree, _) = project
1097        .update(cx, |project, cx| {
1098            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1099        })
1100        .await
1101        .unwrap();
1102
1103    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1104    let buffer = project
1105        .update(cx, |project, cx| {
1106            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1107        })
1108        .await
1109        .unwrap();
1110
1111    buffer.update(cx, |buffer, cx| {
1112        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1113        let ix = buffer.text().find('1').unwrap();
1114        buffer.edit([(ix..ix + 1, "100")], None, cx);
1115    });
1116
1117    let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1118    client
1119        .update(cx, |client, cx| client.simulate_disconnect(cx))
1120        .detach();
1121
1122    project
1123        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1124        .await
1125        .unwrap();
1126
1127    assert_eq!(
1128        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1129            .await
1130            .unwrap(),
1131        "fn one() -> usize { 100 }"
1132    );
1133}
1134
1135#[gpui::test]
1136async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1137    let fs = FakeFs::new(server_cx.executor());
1138    fs.insert_tree(
1139        "/code",
1140        json!({
1141            "project1": {
1142                ".git": {},
1143                "README.md": "# project 1",
1144            },
1145        }),
1146    )
1147    .await;
1148
1149    let (project, _) = init_test(&fs, cx, server_cx).await;
1150
1151    let (worktree, _) = project
1152        .update(cx, |project, cx| {
1153            project.find_or_create_worktree("/code/project1", true, cx)
1154        })
1155        .await
1156        .unwrap();
1157
1158    cx.run_until_parked();
1159
1160    fs.rename(
1161        &PathBuf::from("/code/project1"),
1162        &PathBuf::from("/code/project2"),
1163        Default::default(),
1164    )
1165    .await
1166    .unwrap();
1167
1168    cx.run_until_parked();
1169    worktree.update(cx, |worktree, _| {
1170        assert_eq!(worktree.root_name(), "project2")
1171    })
1172}
1173
1174#[gpui::test]
1175async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1176    let fs = FakeFs::new(server_cx.executor());
1177    fs.insert_tree(
1178        "/code",
1179        json!({
1180            "project1": {
1181                ".git": {},
1182                "README.md": "# project 1",
1183            },
1184        }),
1185    )
1186    .await;
1187
1188    let (project, _) = init_test(&fs, cx, server_cx).await;
1189    let (worktree, _) = project
1190        .update(cx, |project, cx| {
1191            project.find_or_create_worktree("/code/project1", true, cx)
1192        })
1193        .await
1194        .unwrap();
1195
1196    cx.run_until_parked();
1197
1198    let entry = project
1199        .update(cx, |project, cx| {
1200            let worktree = worktree.read(cx);
1201            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1202            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1203        })
1204        .await
1205        .unwrap()
1206        .into_included()
1207        .unwrap();
1208
1209    cx.run_until_parked();
1210
1211    worktree.update(cx, |worktree, _| {
1212        assert_eq!(
1213            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1214            entry.id
1215        )
1216    });
1217}
1218
1219#[gpui::test]
1220async fn test_copy_file_into_remote_project(
1221    cx: &mut TestAppContext,
1222    server_cx: &mut TestAppContext,
1223) {
1224    let remote_fs = FakeFs::new(server_cx.executor());
1225    remote_fs
1226        .insert_tree(
1227            path!("/code"),
1228            json!({
1229                "project1": {
1230                    ".git": {},
1231                    "README.md": "# project 1",
1232                    "src": {
1233                        "main.rs": ""
1234                    }
1235                },
1236            }),
1237        )
1238        .await;
1239
1240    let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1241    let (worktree, _) = project
1242        .update(cx, |project, cx| {
1243            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1244        })
1245        .await
1246        .unwrap();
1247
1248    cx.run_until_parked();
1249
1250    let local_fs = project
1251        .read_with(cx, |project, _| project.fs().clone())
1252        .as_fake();
1253    local_fs
1254        .insert_tree(
1255            path!("/local-code"),
1256            json!({
1257                "dir1": {
1258                    "file1": "file 1 content",
1259                    "dir2": {
1260                        "file2": "file 2 content",
1261                        "dir3": {
1262                            "file3": ""
1263                        },
1264                        "dir4": {}
1265                    },
1266                    "dir5": {}
1267                },
1268                "file4": "file 4 content"
1269            }),
1270        )
1271        .await;
1272
1273    worktree
1274        .update(cx, |worktree, cx| {
1275            worktree.copy_external_entries(
1276                rel_path("src").into(),
1277                vec![
1278                    Path::new(path!("/local-code/dir1/file1")).into(),
1279                    Path::new(path!("/local-code/dir1/dir2")).into(),
1280                ],
1281                local_fs.clone(),
1282                cx,
1283            )
1284        })
1285        .await
1286        .unwrap();
1287
1288    assert_eq!(
1289        remote_fs.paths(true),
1290        vec![
1291            PathBuf::from(path!("/")),
1292            PathBuf::from(path!("/code")),
1293            PathBuf::from(path!("/code/project1")),
1294            PathBuf::from(path!("/code/project1/.git")),
1295            PathBuf::from(path!("/code/project1/README.md")),
1296            PathBuf::from(path!("/code/project1/src")),
1297            PathBuf::from(path!("/code/project1/src/dir2")),
1298            PathBuf::from(path!("/code/project1/src/file1")),
1299            PathBuf::from(path!("/code/project1/src/main.rs")),
1300            PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1301            PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1302            PathBuf::from(path!("/code/project1/src/dir2/file2")),
1303            PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1304        ]
1305    );
1306    assert_eq!(
1307        remote_fs
1308            .load(path!("/code/project1/src/file1").as_ref())
1309            .await
1310            .unwrap(),
1311        "file 1 content"
1312    );
1313    assert_eq!(
1314        remote_fs
1315            .load(path!("/code/project1/src/dir2/file2").as_ref())
1316            .await
1317            .unwrap(),
1318        "file 2 content"
1319    );
1320    assert_eq!(
1321        remote_fs
1322            .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1323            .await
1324            .unwrap(),
1325        ""
1326    );
1327}
1328
1329// TODO: this test fails on Windows.
1330#[cfg(not(windows))]
1331#[gpui::test]
1332async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1333    let text_2 = "
1334        fn one() -> usize {
1335            1
1336        }
1337    "
1338    .unindent();
1339    let text_1 = "
1340        fn one() -> usize {
1341            0
1342        }
1343    "
1344    .unindent();
1345
1346    let fs = FakeFs::new(server_cx.executor());
1347    fs.insert_tree(
1348        "/code",
1349        json!({
1350            "project1": {
1351                ".git": {},
1352                "src": {
1353                    "lib.rs": text_2
1354                },
1355                "README.md": "# project 1",
1356            },
1357        }),
1358    )
1359    .await;
1360    fs.set_index_for_repo(
1361        Path::new("/code/project1/.git"),
1362        &[("src/lib.rs", text_1.clone())],
1363    );
1364    fs.set_head_for_repo(
1365        Path::new("/code/project1/.git"),
1366        &[("src/lib.rs", text_1.clone())],
1367        "deadbeef",
1368    );
1369
1370    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1371    let (worktree, _) = project
1372        .update(cx, |project, cx| {
1373            project.find_or_create_worktree("/code/project1", true, cx)
1374        })
1375        .await
1376        .unwrap();
1377    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1378    cx.executor().run_until_parked();
1379
1380    let buffer = project
1381        .update(cx, |project, cx| {
1382            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1383        })
1384        .await
1385        .unwrap();
1386    let diff = project
1387        .update(cx, |project, cx| {
1388            project.open_uncommitted_diff(buffer.clone(), cx)
1389        })
1390        .await
1391        .unwrap();
1392
1393    diff.read_with(cx, |diff, cx| {
1394        assert_eq!(diff.base_text_string().unwrap(), text_1);
1395        assert_eq!(
1396            diff.secondary_diff()
1397                .unwrap()
1398                .read(cx)
1399                .base_text_string()
1400                .unwrap(),
1401            text_1
1402        );
1403    });
1404
1405    // stage the current buffer's contents
1406    fs.set_index_for_repo(
1407        Path::new("/code/project1/.git"),
1408        &[("src/lib.rs", text_2.clone())],
1409    );
1410
1411    cx.executor().run_until_parked();
1412    diff.read_with(cx, |diff, cx| {
1413        assert_eq!(diff.base_text_string().unwrap(), text_1);
1414        assert_eq!(
1415            diff.secondary_diff()
1416                .unwrap()
1417                .read(cx)
1418                .base_text_string()
1419                .unwrap(),
1420            text_2
1421        );
1422    });
1423
1424    // commit the current buffer's contents
1425    fs.set_head_for_repo(
1426        Path::new("/code/project1/.git"),
1427        &[("src/lib.rs", text_2.clone())],
1428        "deadbeef",
1429    );
1430
1431    cx.executor().run_until_parked();
1432    diff.read_with(cx, |diff, cx| {
1433        assert_eq!(diff.base_text_string().unwrap(), text_2);
1434        assert_eq!(
1435            diff.secondary_diff()
1436                .unwrap()
1437                .read(cx)
1438                .base_text_string()
1439                .unwrap(),
1440            text_2
1441        );
1442    });
1443}
1444
1445// TODO: this test fails on Windows.
1446#[cfg(not(windows))]
1447#[gpui::test]
1448async fn test_remote_git_diffs_when_recv_update_repository_delay(
1449    cx: &mut TestAppContext,
1450    server_cx: &mut TestAppContext,
1451) {
1452    use editor::Editor;
1453    use gpui::VisualContext;
1454    let text_2 = "
1455        fn one() -> usize {
1456            1
1457        }
1458    "
1459    .unindent();
1460    let text_1 = "
1461        fn one() -> usize {
1462            0
1463        }
1464    "
1465    .unindent();
1466
1467    let fs = FakeFs::new(server_cx.executor());
1468    fs.insert_tree(
1469        "/code",
1470        json!({
1471            "project1": {
1472                "src": {
1473                    "lib.rs": text_2
1474                },
1475                "README.md": "# project 1",
1476            },
1477        }),
1478    )
1479    .await;
1480
1481    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1482    let (worktree, _) = project
1483        .update(cx, |project, cx| {
1484            project.find_or_create_worktree("/code/project1", true, cx)
1485        })
1486        .await
1487        .unwrap();
1488    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1489    let buffer = project
1490        .update(cx, |project, cx| {
1491            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1492        })
1493        .await
1494        .unwrap();
1495    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1496    cx.update(|cx| {
1497        workspace::init_settings(cx);
1498        editor::init_settings(cx);
1499    });
1500    let cx = cx.add_empty_window();
1501    let editor = cx.new_window_entity(|window, cx| {
1502        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1503    });
1504
1505    // Remote server will send proto::UpdateRepository after the instance of Editor create.
1506    fs.insert_tree(
1507        "/code",
1508        json!({
1509            "project1": {
1510                ".git": {},
1511            },
1512        }),
1513    )
1514    .await;
1515
1516    fs.set_index_for_repo(
1517        Path::new("/code/project1/.git"),
1518        &[("src/lib.rs", text_1.clone())],
1519    );
1520    fs.set_head_for_repo(
1521        Path::new("/code/project1/.git"),
1522        &[("src/lib.rs", text_1.clone())],
1523        "sha",
1524    );
1525
1526    cx.executor().run_until_parked();
1527    let diff = editor
1528        .read_with(cx, |editor, cx| {
1529            editor
1530                .buffer()
1531                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1532        })
1533        .unwrap();
1534
1535    diff.read_with(cx, |diff, cx| {
1536        assert_eq!(diff.base_text_string().unwrap(), text_1);
1537        assert_eq!(
1538            diff.secondary_diff()
1539                .unwrap()
1540                .read(cx)
1541                .base_text_string()
1542                .unwrap(),
1543            text_1
1544        );
1545    });
1546
1547    // stage the current buffer's contents
1548    fs.set_index_for_repo(
1549        Path::new("/code/project1/.git"),
1550        &[("src/lib.rs", text_2.clone())],
1551    );
1552
1553    cx.executor().run_until_parked();
1554    diff.read_with(cx, |diff, cx| {
1555        assert_eq!(diff.base_text_string().unwrap(), text_1);
1556        assert_eq!(
1557            diff.secondary_diff()
1558                .unwrap()
1559                .read(cx)
1560                .base_text_string()
1561                .unwrap(),
1562            text_2
1563        );
1564    });
1565
1566    // commit the current buffer's contents
1567    fs.set_head_for_repo(
1568        Path::new("/code/project1/.git"),
1569        &[("src/lib.rs", text_2.clone())],
1570        "sha",
1571    );
1572
1573    cx.executor().run_until_parked();
1574    diff.read_with(cx, |diff, cx| {
1575        assert_eq!(diff.base_text_string().unwrap(), text_2);
1576        assert_eq!(
1577            diff.secondary_diff()
1578                .unwrap()
1579                .read(cx)
1580                .base_text_string()
1581                .unwrap(),
1582            text_2
1583        );
1584    });
1585}
1586
1587#[gpui::test]
1588async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1589    let fs = FakeFs::new(server_cx.executor());
1590    fs.insert_tree(
1591        path!("/code"),
1592        json!({
1593            "project1": {
1594                ".git": {},
1595                "README.md": "# project 1",
1596            },
1597        }),
1598    )
1599    .await;
1600
1601    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1602    let branches = ["main", "dev", "feature-1"];
1603    let branches_set = branches
1604        .iter()
1605        .map(ToString::to_string)
1606        .collect::<HashSet<_>>();
1607    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1608
1609    let (_worktree, _) = project
1610        .update(cx, |project, cx| {
1611            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1612        })
1613        .await
1614        .unwrap();
1615    // Give the worktree a bit of time to index the file system
1616    cx.run_until_parked();
1617
1618    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1619
1620    let remote_branches = repository
1621        .update(cx, |repository, _| repository.branches())
1622        .await
1623        .unwrap()
1624        .unwrap();
1625
1626    let new_branch = branches[2];
1627
1628    let remote_branches = remote_branches
1629        .into_iter()
1630        .map(|branch| branch.name().to_string())
1631        .collect::<HashSet<_>>();
1632
1633    assert_eq!(&remote_branches, &branches_set);
1634
1635    cx.update(|cx| {
1636        repository.update(cx, |repository, _cx| {
1637            repository.change_branch(new_branch.to_string())
1638        })
1639    })
1640    .await
1641    .unwrap()
1642    .unwrap();
1643
1644    cx.run_until_parked();
1645
1646    let server_branch = server_cx.update(|cx| {
1647        headless_project.update(cx, |headless_project, cx| {
1648            headless_project.git_store.update(cx, |git_store, cx| {
1649                git_store
1650                    .repositories()
1651                    .values()
1652                    .next()
1653                    .unwrap()
1654                    .read(cx)
1655                    .branch
1656                    .as_ref()
1657                    .unwrap()
1658                    .clone()
1659            })
1660        })
1661    });
1662
1663    assert_eq!(server_branch.name(), branches[2]);
1664
1665    // Also try creating a new branch
1666    cx.update(|cx| {
1667        repository.update(cx, |repo, _cx| {
1668            repo.create_branch("totally-new-branch".to_string())
1669        })
1670    })
1671    .await
1672    .unwrap()
1673    .unwrap();
1674
1675    cx.update(|cx| {
1676        repository.update(cx, |repo, _cx| {
1677            repo.change_branch("totally-new-branch".to_string())
1678        })
1679    })
1680    .await
1681    .unwrap()
1682    .unwrap();
1683
1684    cx.run_until_parked();
1685
1686    let server_branch = server_cx.update(|cx| {
1687        headless_project.update(cx, |headless_project, cx| {
1688            headless_project.git_store.update(cx, |git_store, cx| {
1689                git_store
1690                    .repositories()
1691                    .values()
1692                    .next()
1693                    .unwrap()
1694                    .read(cx)
1695                    .branch
1696                    .as_ref()
1697                    .unwrap()
1698                    .clone()
1699            })
1700        })
1701    });
1702
1703    assert_eq!(server_branch.name(), "totally-new-branch");
1704}
1705
1706#[gpui::test]
1707async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1708    let fs = FakeFs::new(server_cx.executor());
1709    fs.insert_tree(
1710        path!("/project"),
1711        json!({
1712            "a.txt": "A",
1713            "b.txt": "B",
1714        }),
1715    )
1716    .await;
1717
1718    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1719    project
1720        .update(cx, |project, cx| {
1721            project.find_or_create_worktree(path!("/project"), true, cx)
1722        })
1723        .await
1724        .unwrap();
1725
1726    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
1727    let model = Arc::new(FakeLanguageModel::default());
1728    let request = Arc::new(LanguageModelRequest::default());
1729
1730    let input = ReadFileToolInput {
1731        path: "project/b.txt".into(),
1732        start_line: None,
1733        end_line: None,
1734    };
1735    let exists_result = cx.update(|cx| {
1736        ReadFileTool::run(
1737            Arc::new(ReadFileTool),
1738            serde_json::to_value(input).unwrap(),
1739            request.clone(),
1740            project.clone(),
1741            action_log.clone(),
1742            model.clone(),
1743            None,
1744            cx,
1745        )
1746    });
1747    let output = exists_result.output.await.unwrap().content;
1748    assert_eq!(output, ToolResultContent::Text("B".to_string()));
1749
1750    let input = ReadFileToolInput {
1751        path: "project/c.txt".into(),
1752        start_line: None,
1753        end_line: None,
1754    };
1755    let does_not_exist_result = cx.update(|cx| {
1756        ReadFileTool::run(
1757            Arc::new(ReadFileTool),
1758            serde_json::to_value(input).unwrap(),
1759            request.clone(),
1760            project.clone(),
1761            action_log.clone(),
1762            model.clone(),
1763            None,
1764            cx,
1765        )
1766    });
1767    does_not_exist_result.output.await.unwrap_err();
1768}
1769
1770#[gpui::test]
1771async fn test_remote_external_agent_server(
1772    cx: &mut TestAppContext,
1773    server_cx: &mut TestAppContext,
1774) {
1775    let fs = FakeFs::new(server_cx.executor());
1776    fs.insert_tree(path!("/project"), json!({})).await;
1777
1778    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1779    project
1780        .update(cx, |project, cx| {
1781            project.find_or_create_worktree(path!("/project"), true, cx)
1782        })
1783        .await
1784        .unwrap();
1785    let names = project.update(cx, |project, cx| {
1786        project
1787            .agent_server_store()
1788            .read(cx)
1789            .external_agents()
1790            .map(|name| name.to_string())
1791            .collect::<Vec<_>>()
1792    });
1793    pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
1794    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
1795        settings_store
1796            .set_server_settings(
1797                &json!({
1798                    "agent_servers": {
1799                        "foo": {
1800                            "command": "foo-cli",
1801                            "args": ["--flag"],
1802                            "env": {
1803                                "VAR": "val"
1804                            }
1805                        }
1806                    }
1807                })
1808                .to_string(),
1809                cx,
1810            )
1811            .unwrap();
1812    });
1813    server_cx.run_until_parked();
1814    cx.run_until_parked();
1815    let names = project.update(cx, |project, cx| {
1816        project
1817            .agent_server_store()
1818            .read(cx)
1819            .external_agents()
1820            .map(|name| name.to_string())
1821            .collect::<Vec<_>>()
1822    });
1823    pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
1824    let (command, root, login) = project
1825        .update(cx, |project, cx| {
1826            project.agent_server_store().update(cx, |store, cx| {
1827                store
1828                    .get_external_agent(&"foo".into())
1829                    .unwrap()
1830                    .get_command(
1831                        None,
1832                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
1833                        None,
1834                        None,
1835                        &mut cx.to_async(),
1836                    )
1837            })
1838        })
1839        .await
1840        .unwrap();
1841    assert_eq!(
1842        command,
1843        AgentServerCommand {
1844            path: "ssh".into(),
1845            args: vec!["foo-cli".into(), "--flag".into()],
1846            env: Some(HashMap::from_iter([
1847                ("VAR".into(), "val".into()),
1848                ("OTHER_VAR".into(), "other-val".into())
1849            ]))
1850        }
1851    );
1852    assert_eq!(&PathBuf::from(root), paths::home_dir());
1853    assert!(login.is_none());
1854}
1855
1856pub async fn init_test(
1857    server_fs: &Arc<FakeFs>,
1858    cx: &mut TestAppContext,
1859    server_cx: &mut TestAppContext,
1860) -> (Entity<Project>, Entity<HeadlessProject>) {
1861    let server_fs = server_fs.clone();
1862    cx.update(|cx| {
1863        release_channel::init(SemanticVersion::default(), cx);
1864    });
1865    server_cx.update(|cx| {
1866        release_channel::init(SemanticVersion::default(), cx);
1867    });
1868    init_logger();
1869
1870    let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
1871    let http_client = Arc::new(BlockedHttpClient);
1872    let node_runtime = NodeRuntime::unavailable();
1873    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1874    let proxy = Arc::new(ExtensionHostProxy::new());
1875    server_cx.update(HeadlessProject::init);
1876    let headless = server_cx.new(|cx| {
1877        client::init_settings(cx);
1878
1879        HeadlessProject::new(
1880            crate::HeadlessAppState {
1881                session: ssh_server_client,
1882                fs: server_fs.clone(),
1883                http_client,
1884                node_runtime,
1885                languages,
1886                extension_host_proxy: proxy,
1887            },
1888            cx,
1889        )
1890    });
1891
1892    let ssh = RemoteClient::fake_client(opts, cx).await;
1893    let project = build_project(ssh, cx);
1894    project
1895        .update(cx, {
1896            let headless = headless.clone();
1897            |_, cx| cx.on_release(|_, _| drop(headless))
1898        })
1899        .detach();
1900    (project, headless)
1901}
1902
1903fn init_logger() {
1904    zlog::init_test();
1905}
1906
1907fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1908    cx.update(|cx| {
1909        if !cx.has_global::<SettingsStore>() {
1910            let settings_store = SettingsStore::test(cx);
1911            cx.set_global(settings_store);
1912        }
1913    });
1914
1915    let client = cx.update(|cx| {
1916        Client::new(
1917            Arc::new(FakeSystemClock::new()),
1918            FakeHttpClient::with_404_response(),
1919            cx,
1920        )
1921    });
1922
1923    let node = NodeRuntime::unavailable();
1924    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1925    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1926    let fs = FakeFs::new(cx.executor());
1927
1928    cx.update(|cx| {
1929        Project::init(&client, cx);
1930        language::init(cx);
1931    });
1932
1933    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
1934}