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