remote_editing_tests.rs

   1/// todo(windows)
   2/// The tests in this file assume that server_cx is running on Windows too.
   3/// We neead to find a way to test Windows-Non-Windows interactions.
   4use crate::headless_project::HeadlessProject;
   5use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream};
   6use client::{Client, UserStore};
   7use clock::FakeSystemClock;
   8use collections::{HashMap, HashSet};
   9
  10use encodings::Encoding;
  11use extension::ExtensionHostProxy;
  12use fs::{FakeFs, Fs};
  13use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
  14use http_client::{BlockedHttpClient, FakeHttpClient};
  15use language::{
  16    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Rope,
  17    language_settings::{AllLanguageSettings, language_settings},
  18};
  19use language_model::LanguageModelToolResultContent;
  20use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
  21use node_runtime::NodeRuntime;
  22use project::{
  23    ProgressToken, 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};
  35use unindent::Unindent as _;
  36use util::{path, rel_path::rel_path};
  37
  38#[gpui::test]
  39async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  40    let fs = FakeFs::new(server_cx.executor());
  41    fs.insert_tree(
  42        path!("/code"),
  43        json!({
  44            "project1": {
  45                ".git": {},
  46                "README.md": "# project 1",
  47                "src": {
  48                    "lib.rs": "fn one() -> usize { 1 }"
  49                }
  50            },
  51            "project2": {
  52                "README.md": "# project 2",
  53            },
  54        }),
  55    )
  56    .await;
  57    fs.set_index_for_repo(
  58        Path::new(path!("/code/project1/.git")),
  59        &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
  60    );
  61
  62    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  63    let (worktree, _) = project
  64        .update(cx, |project, cx| {
  65            project.find_or_create_worktree(path!("/code/project1"), true, cx)
  66        })
  67        .await
  68        .unwrap();
  69
  70    // The client sees the worktree's contents.
  71    cx.executor().run_until_parked();
  72    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  73    worktree.update(cx, |worktree, _cx| {
  74        assert_eq!(
  75            worktree.paths().collect::<Vec<_>>(),
  76            vec![
  77                rel_path("README.md"),
  78                rel_path("src"),
  79                rel_path("src/lib.rs"),
  80            ]
  81        );
  82    });
  83
  84    // The user opens a buffer in the remote worktree. The buffer's
  85    // contents are loaded from the remote filesystem.
  86    let buffer = project
  87        .update(cx, |project, cx| {
  88            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
  89        })
  90        .await
  91        .unwrap();
  92    let diff = project
  93        .update(cx, |project, cx| {
  94            project.open_unstaged_diff(buffer.clone(), cx)
  95        })
  96        .await
  97        .unwrap();
  98
  99    diff.update(cx, |diff, _| {
 100        assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
 101    });
 102
 103    buffer.update(cx, |buffer, cx| {
 104        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 105        let ix = buffer.text().find('1').unwrap();
 106        buffer.edit([(ix..ix + 1, "100")], None, cx);
 107    });
 108
 109    // The user saves the buffer. The new contents are written to the
 110    // remote filesystem.
 111    project
 112        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 113        .await
 114        .unwrap();
 115    assert_eq!(
 116        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 117        "fn one() -> usize { 100 }"
 118    );
 119
 120    // A new file is created in the remote filesystem. The user
 121    // sees the new file.
 122    fs.save(
 123        path!("/code/project1/src/main.rs").as_ref(),
 124        &Rope::from_str_small("fn main() {}"),
 125        Default::default(),
 126        Encoding::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(
 716                server_id,
 717                Some(ProgressToken::String(SharedString::from(progress_token))),
 718                cx,
 719            )
 720        });
 721
 722        cx.executor().run_until_parked();
 723
 724        // Verify the cancellation was received on the server side
 725        let cancel_notification = fake_lsp
 726            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 727            .await;
 728        assert_eq!(
 729            cancel_notification.token,
 730            lsp::NumberOrString::String(progress_token.to_owned())
 731        );
 732    }
 733}
 734
 735#[gpui::test]
 736async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 737    let fs = FakeFs::new(server_cx.executor());
 738    fs.insert_tree(
 739        path!("/code"),
 740        json!({
 741            "project1": {
 742                ".git": {},
 743                "README.md": "# project 1",
 744                "src": {
 745                    "lib.rs": "fn one() -> usize { 1 }"
 746                }
 747            },
 748        }),
 749    )
 750    .await;
 751
 752    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 753    let (worktree, _) = project
 754        .update(cx, |project, cx| {
 755            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 756        })
 757        .await
 758        .unwrap();
 759
 760    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 761
 762    let buffer = project
 763        .update(cx, |project, cx| {
 764            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 765        })
 766        .await
 767        .unwrap();
 768
 769    fs.save(
 770        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 771        &Rope::from_str_small("bangles"),
 772        LineEnding::Unix,
 773        Encoding::default(),
 774    )
 775    .await
 776    .unwrap();
 777
 778    cx.run_until_parked();
 779
 780    buffer.update(cx, |buffer, cx| {
 781        assert_eq!(buffer.text(), "bangles");
 782        buffer.edit([(0..0, "a")], None, cx);
 783    });
 784
 785    fs.save(
 786        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 787        &Rope::from_str_small("bloop"),
 788        LineEnding::Unix,
 789        Encoding::default(),
 790    )
 791    .await
 792    .unwrap();
 793
 794    cx.run_until_parked();
 795    cx.update(|cx| {
 796        assert!(buffer.read(cx).has_conflict());
 797    });
 798
 799    project
 800        .update(cx, |project, cx| {
 801            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
 802        })
 803        .await
 804        .unwrap();
 805    cx.run_until_parked();
 806
 807    cx.update(|cx| {
 808        assert!(!buffer.read(cx).has_conflict());
 809    });
 810}
 811
 812#[gpui::test]
 813async fn test_remote_resolve_path_in_buffer(
 814    cx: &mut TestAppContext,
 815    server_cx: &mut TestAppContext,
 816) {
 817    let fs = FakeFs::new(server_cx.executor());
 818    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
 819    fs.insert_tree(
 820        path!("/code"),
 821        json!({
 822            "project1": {
 823                ".git": {},
 824                "README.md": "# project 1",
 825                "src": {
 826                    "lib.rs": "fn one() -> usize { 1 }"
 827                }
 828            },
 829            "project2": {
 830                ".git": {},
 831                "README.md": "# project 2",
 832                "src": {
 833                    "lib.rs": "fn two() -> usize { 2 }"
 834                }
 835            }
 836        }),
 837    )
 838    .await;
 839
 840    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 841
 842    let _ = project
 843        .update(cx, |project, cx| {
 844            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 845        })
 846        .await
 847        .unwrap();
 848
 849    let (worktree2, _) = project
 850        .update(cx, |project, cx| {
 851            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 852        })
 853        .await
 854        .unwrap();
 855
 856    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
 857
 858    cx.run_until_parked();
 859
 860    let buffer2 = project
 861        .update(cx, |project, cx| {
 862            project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
 863        })
 864        .await
 865        .unwrap();
 866
 867    let path = project
 868        .update(cx, |project, cx| {
 869            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
 870        })
 871        .await
 872        .unwrap();
 873    assert!(path.is_file());
 874    assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
 875
 876    let path = project
 877        .update(cx, |project, cx| {
 878            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
 879        })
 880        .await
 881        .unwrap();
 882    assert!(path.is_file());
 883    assert_eq!(
 884        path.project_path().unwrap().clone(),
 885        (worktree2_id, rel_path("README.md")).into()
 886    );
 887
 888    let path = project
 889        .update(cx, |project, cx| {
 890            project.resolve_path_in_buffer("../src", &buffer2, cx)
 891        })
 892        .await
 893        .unwrap();
 894    assert_eq!(
 895        path.project_path().unwrap().clone(),
 896        (worktree2_id, rel_path("src")).into()
 897    );
 898    assert!(path.is_dir());
 899}
 900
 901#[gpui::test]
 902async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 903    let fs = FakeFs::new(server_cx.executor());
 904    fs.insert_tree(
 905        path!("/code"),
 906        json!({
 907            "project1": {
 908                ".git": {},
 909                "README.md": "# project 1",
 910                "src": {
 911                    "lib.rs": "fn one() -> usize { 1 }"
 912                }
 913            },
 914        }),
 915    )
 916    .await;
 917
 918    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 919
 920    let path = project
 921        .update(cx, |project, cx| {
 922            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 923        })
 924        .await
 925        .unwrap();
 926
 927    assert!(path.is_file());
 928    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
 929
 930    let path = project
 931        .update(cx, |project, cx| {
 932            project.resolve_abs_path(path!("/code/project1/src"), cx)
 933        })
 934        .await
 935        .unwrap();
 936
 937    assert!(path.is_dir());
 938    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
 939
 940    let path = project
 941        .update(cx, |project, cx| {
 942            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 943        })
 944        .await;
 945    assert!(path.is_none());
 946}
 947
 948#[gpui::test(iterations = 10)]
 949async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 950    let fs = FakeFs::new(server_cx.executor());
 951    fs.insert_tree(
 952        "/code",
 953        json!({
 954            "project1": {
 955                ".git": {},
 956                "README.md": "# project 1",
 957                "src": {
 958                    "lib.rs": "fn one() -> usize { 1 }"
 959                }
 960            },
 961        }),
 962    )
 963    .await;
 964
 965    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 966    let (worktree, _) = project
 967        .update(cx, |project, cx| {
 968            project.find_or_create_worktree("/code/project1", true, cx)
 969        })
 970        .await
 971        .unwrap();
 972    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 973
 974    // Open a buffer on the client but cancel after a random amount of time.
 975    let buffer = project.update(cx, |p, cx| {
 976        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 977    });
 978    cx.executor().simulate_random_delay().await;
 979    drop(buffer);
 980
 981    // Try opening the same buffer again as the client, and ensure we can
 982    // still do it despite the cancellation above.
 983    let buffer = project
 984        .update(cx, |p, cx| {
 985            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 986        })
 987        .await
 988        .unwrap();
 989
 990    buffer.read_with(cx, |buf, _| {
 991        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 992    });
 993}
 994
 995#[gpui::test]
 996async fn test_adding_then_removing_then_adding_worktrees(
 997    cx: &mut TestAppContext,
 998    server_cx: &mut TestAppContext,
 999) {
1000    let fs = FakeFs::new(server_cx.executor());
1001    fs.insert_tree(
1002        path!("/code"),
1003        json!({
1004            "project1": {
1005                ".git": {},
1006                "README.md": "# project 1",
1007                "src": {
1008                    "lib.rs": "fn one() -> usize { 1 }"
1009                }
1010            },
1011            "project2": {
1012                "README.md": "# project 2",
1013            },
1014        }),
1015    )
1016    .await;
1017
1018    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1019    let (_worktree, _) = project
1020        .update(cx, |project, cx| {
1021            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1022        })
1023        .await
1024        .unwrap();
1025
1026    let (worktree_2, _) = project
1027        .update(cx, |project, cx| {
1028            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1029        })
1030        .await
1031        .unwrap();
1032    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1033
1034    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1035
1036    let (worktree_2, _) = project
1037        .update(cx, |project, cx| {
1038            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1039        })
1040        .await
1041        .unwrap();
1042
1043    cx.run_until_parked();
1044    worktree_2.update(cx, |worktree, _cx| {
1045        assert!(worktree.is_visible());
1046        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1047        assert_eq!(entries.len(), 2);
1048        assert_eq!(entries[1].path.as_unix_str(), "README.md")
1049    })
1050}
1051
1052#[gpui::test]
1053async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1054    let fs = FakeFs::new(server_cx.executor());
1055    fs.insert_tree(
1056        path!("/code"),
1057        json!({
1058            "project1": {
1059                ".git": {},
1060                "README.md": "# project 1",
1061                "src": {
1062                    "lib.rs": "fn one() -> usize { 1 }"
1063                }
1064            },
1065        }),
1066    )
1067    .await;
1068
1069    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1070    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1071    cx.executor().run_until_parked();
1072
1073    let buffer = buffer.await.unwrap();
1074
1075    cx.update(|cx| {
1076        assert_eq!(
1077            buffer.read(cx).text(),
1078            initial_server_settings_content()
1079                .to_string()
1080                .replace("\r\n", "\n")
1081        )
1082    })
1083}
1084
1085#[gpui::test(iterations = 20)]
1086async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1087    let fs = FakeFs::new(server_cx.executor());
1088    fs.insert_tree(
1089        path!("/code"),
1090        json!({
1091            "project1": {
1092                ".git": {},
1093                "README.md": "# project 1",
1094                "src": {
1095                    "lib.rs": "fn one() -> usize { 1 }"
1096                }
1097            },
1098        }),
1099    )
1100    .await;
1101
1102    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1103
1104    let (worktree, _) = project
1105        .update(cx, |project, cx| {
1106            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1107        })
1108        .await
1109        .unwrap();
1110
1111    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1112    let buffer = project
1113        .update(cx, |project, cx| {
1114            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1115        })
1116        .await
1117        .unwrap();
1118
1119    buffer.update(cx, |buffer, cx| {
1120        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1121        let ix = buffer.text().find('1').unwrap();
1122        buffer.edit([(ix..ix + 1, "100")], None, cx);
1123    });
1124
1125    let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1126    client
1127        .update(cx, |client, cx| client.simulate_disconnect(cx))
1128        .detach();
1129
1130    project
1131        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1132        .await
1133        .unwrap();
1134
1135    assert_eq!(
1136        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1137            .await
1138            .unwrap(),
1139        "fn one() -> usize { 100 }"
1140    );
1141}
1142
1143#[gpui::test]
1144async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1145    let fs = FakeFs::new(server_cx.executor());
1146    fs.insert_tree(
1147        "/code",
1148        json!({
1149            "project1": {
1150                ".git": {},
1151                "README.md": "# project 1",
1152            },
1153        }),
1154    )
1155    .await;
1156
1157    let (project, _) = init_test(&fs, cx, server_cx).await;
1158
1159    let (worktree, _) = project
1160        .update(cx, |project, cx| {
1161            project.find_or_create_worktree("/code/project1", true, cx)
1162        })
1163        .await
1164        .unwrap();
1165
1166    cx.run_until_parked();
1167
1168    fs.rename(
1169        &PathBuf::from("/code/project1"),
1170        &PathBuf::from("/code/project2"),
1171        Default::default(),
1172    )
1173    .await
1174    .unwrap();
1175
1176    cx.run_until_parked();
1177    worktree.update(cx, |worktree, _| {
1178        assert_eq!(worktree.root_name(), "project2")
1179    })
1180}
1181
1182#[gpui::test]
1183async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1184    let fs = FakeFs::new(server_cx.executor());
1185    fs.insert_tree(
1186        "/code",
1187        json!({
1188            "project1": {
1189                ".git": {},
1190                "README.md": "# project 1",
1191            },
1192        }),
1193    )
1194    .await;
1195
1196    let (project, _) = init_test(&fs, cx, server_cx).await;
1197    let (worktree, _) = project
1198        .update(cx, |project, cx| {
1199            project.find_or_create_worktree("/code/project1", true, cx)
1200        })
1201        .await
1202        .unwrap();
1203
1204    cx.run_until_parked();
1205
1206    let entry = project
1207        .update(cx, |project, cx| {
1208            let worktree = worktree.read(cx);
1209            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1210            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1211        })
1212        .await
1213        .unwrap()
1214        .into_included()
1215        .unwrap();
1216
1217    cx.run_until_parked();
1218
1219    worktree.update(cx, |worktree, _| {
1220        assert_eq!(
1221            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1222            entry.id
1223        )
1224    });
1225}
1226
1227#[gpui::test]
1228async fn test_copy_file_into_remote_project(
1229    cx: &mut TestAppContext,
1230    server_cx: &mut TestAppContext,
1231) {
1232    let remote_fs = FakeFs::new(server_cx.executor());
1233    remote_fs
1234        .insert_tree(
1235            path!("/code"),
1236            json!({
1237                "project1": {
1238                    ".git": {},
1239                    "README.md": "# project 1",
1240                    "src": {
1241                        "main.rs": ""
1242                    }
1243                },
1244            }),
1245        )
1246        .await;
1247
1248    let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1249    let (worktree, _) = project
1250        .update(cx, |project, cx| {
1251            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1252        })
1253        .await
1254        .unwrap();
1255
1256    cx.run_until_parked();
1257
1258    let local_fs = project
1259        .read_with(cx, |project, _| project.fs().clone())
1260        .as_fake();
1261    local_fs
1262        .insert_tree(
1263            path!("/local-code"),
1264            json!({
1265                "dir1": {
1266                    "file1": "file 1 content",
1267                    "dir2": {
1268                        "file2": "file 2 content",
1269                        "dir3": {
1270                            "file3": ""
1271                        },
1272                        "dir4": {}
1273                    },
1274                    "dir5": {}
1275                },
1276                "file4": "file 4 content"
1277            }),
1278        )
1279        .await;
1280
1281    worktree
1282        .update(cx, |worktree, cx| {
1283            worktree.copy_external_entries(
1284                rel_path("src").into(),
1285                vec![
1286                    Path::new(path!("/local-code/dir1/file1")).into(),
1287                    Path::new(path!("/local-code/dir1/dir2")).into(),
1288                ],
1289                local_fs.clone(),
1290                cx,
1291            )
1292        })
1293        .await
1294        .unwrap();
1295
1296    assert_eq!(
1297        remote_fs.paths(true),
1298        vec![
1299            PathBuf::from(path!("/")),
1300            PathBuf::from(path!("/code")),
1301            PathBuf::from(path!("/code/project1")),
1302            PathBuf::from(path!("/code/project1/.git")),
1303            PathBuf::from(path!("/code/project1/README.md")),
1304            PathBuf::from(path!("/code/project1/src")),
1305            PathBuf::from(path!("/code/project1/src/dir2")),
1306            PathBuf::from(path!("/code/project1/src/file1")),
1307            PathBuf::from(path!("/code/project1/src/main.rs")),
1308            PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1309            PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1310            PathBuf::from(path!("/code/project1/src/dir2/file2")),
1311            PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1312        ]
1313    );
1314    assert_eq!(
1315        remote_fs
1316            .load(path!("/code/project1/src/file1").as_ref())
1317            .await
1318            .unwrap(),
1319        "file 1 content"
1320    );
1321    assert_eq!(
1322        remote_fs
1323            .load(path!("/code/project1/src/dir2/file2").as_ref())
1324            .await
1325            .unwrap(),
1326        "file 2 content"
1327    );
1328    assert_eq!(
1329        remote_fs
1330            .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1331            .await
1332            .unwrap(),
1333        ""
1334    );
1335}
1336
1337#[gpui::test]
1338async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1339    let text_2 = "
1340        fn one() -> usize {
1341            1
1342        }
1343    "
1344    .unindent();
1345    let text_1 = "
1346        fn one() -> usize {
1347            0
1348        }
1349    "
1350    .unindent();
1351
1352    let fs = FakeFs::new(server_cx.executor());
1353    fs.insert_tree(
1354        "/code",
1355        json!({
1356            "project1": {
1357                ".git": {},
1358                "src": {
1359                    "lib.rs": text_2
1360                },
1361                "README.md": "# project 1",
1362            },
1363        }),
1364    )
1365    .await;
1366    fs.set_index_for_repo(
1367        Path::new("/code/project1/.git"),
1368        &[("src/lib.rs", text_1.clone())],
1369    );
1370    fs.set_head_for_repo(
1371        Path::new("/code/project1/.git"),
1372        &[("src/lib.rs", text_1.clone())],
1373        "deadbeef",
1374    );
1375
1376    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1377    let (worktree, _) = project
1378        .update(cx, |project, cx| {
1379            project.find_or_create_worktree("/code/project1", true, cx)
1380        })
1381        .await
1382        .unwrap();
1383    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1384    cx.executor().run_until_parked();
1385
1386    let buffer = project
1387        .update(cx, |project, cx| {
1388            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1389        })
1390        .await
1391        .unwrap();
1392    let diff = project
1393        .update(cx, |project, cx| {
1394            project.open_uncommitted_diff(buffer.clone(), cx)
1395        })
1396        .await
1397        .unwrap();
1398
1399    diff.read_with(cx, |diff, cx| {
1400        assert_eq!(diff.base_text_string().unwrap(), text_1);
1401        assert_eq!(
1402            diff.secondary_diff()
1403                .unwrap()
1404                .read(cx)
1405                .base_text_string()
1406                .unwrap(),
1407            text_1
1408        );
1409    });
1410
1411    // stage the current buffer's contents
1412    fs.set_index_for_repo(
1413        Path::new("/code/project1/.git"),
1414        &[("src/lib.rs", text_2.clone())],
1415    );
1416
1417    cx.executor().run_until_parked();
1418    diff.read_with(cx, |diff, cx| {
1419        assert_eq!(diff.base_text_string().unwrap(), text_1);
1420        assert_eq!(
1421            diff.secondary_diff()
1422                .unwrap()
1423                .read(cx)
1424                .base_text_string()
1425                .unwrap(),
1426            text_2
1427        );
1428    });
1429
1430    // commit the current buffer's contents
1431    fs.set_head_for_repo(
1432        Path::new("/code/project1/.git"),
1433        &[("src/lib.rs", text_2.clone())],
1434        "deadbeef",
1435    );
1436
1437    cx.executor().run_until_parked();
1438    diff.read_with(cx, |diff, cx| {
1439        assert_eq!(diff.base_text_string().unwrap(), text_2);
1440        assert_eq!(
1441            diff.secondary_diff()
1442                .unwrap()
1443                .read(cx)
1444                .base_text_string()
1445                .unwrap(),
1446            text_2
1447        );
1448    });
1449}
1450
1451#[gpui::test]
1452async fn test_remote_git_diffs_when_recv_update_repository_delay(
1453    cx: &mut TestAppContext,
1454    server_cx: &mut TestAppContext,
1455) {
1456    use editor::Editor;
1457    use gpui::VisualContext;
1458    let text_2 = "
1459        fn one() -> usize {
1460            1
1461        }
1462    "
1463    .unindent();
1464    let text_1 = "
1465        fn one() -> usize {
1466            0
1467        }
1468    "
1469    .unindent();
1470
1471    let fs = FakeFs::new(server_cx.executor());
1472    fs.insert_tree(
1473        path!("/code"),
1474        json!({
1475            "project1": {
1476                "src": {
1477                    "lib.rs": text_2
1478                },
1479                "README.md": "# project 1",
1480            },
1481        }),
1482    )
1483    .await;
1484
1485    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1486    let (worktree, _) = project
1487        .update(cx, |project, cx| {
1488            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1489        })
1490        .await
1491        .unwrap();
1492    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1493    let buffer = project
1494        .update(cx, |project, cx| {
1495            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1496        })
1497        .await
1498        .unwrap();
1499    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1500    cx.update(|cx| {
1501        workspace::init_settings(cx);
1502        editor::init_settings(cx);
1503    });
1504    let cx = cx.add_empty_window();
1505    let editor = cx.new_window_entity(|window, cx| {
1506        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1507    });
1508
1509    // Remote server will send proto::UpdateRepository after the instance of Editor create.
1510    fs.insert_tree(
1511        path!("/code"),
1512        json!({
1513            "project1": {
1514                ".git": {},
1515            },
1516        }),
1517    )
1518    .await;
1519
1520    fs.set_index_for_repo(
1521        Path::new(path!("/code/project1/.git")),
1522        &[("src/lib.rs", text_1.clone())],
1523    );
1524    fs.set_head_for_repo(
1525        Path::new(path!("/code/project1/.git")),
1526        &[("src/lib.rs", text_1.clone())],
1527        "sha",
1528    );
1529
1530    cx.executor().run_until_parked();
1531    let diff = editor
1532        .read_with(cx, |editor, cx| {
1533            editor
1534                .buffer()
1535                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1536        })
1537        .unwrap();
1538
1539    diff.read_with(cx, |diff, cx| {
1540        assert_eq!(diff.base_text_string().unwrap(), text_1);
1541        assert_eq!(
1542            diff.secondary_diff()
1543                .unwrap()
1544                .read(cx)
1545                .base_text_string()
1546                .unwrap(),
1547            text_1
1548        );
1549    });
1550
1551    // stage the current buffer's contents
1552    fs.set_index_for_repo(
1553        Path::new(path!("/code/project1/.git")),
1554        &[("src/lib.rs", text_2.clone())],
1555    );
1556
1557    cx.executor().run_until_parked();
1558    diff.read_with(cx, |diff, cx| {
1559        assert_eq!(diff.base_text_string().unwrap(), text_1);
1560        assert_eq!(
1561            diff.secondary_diff()
1562                .unwrap()
1563                .read(cx)
1564                .base_text_string()
1565                .unwrap(),
1566            text_2
1567        );
1568    });
1569
1570    // commit the current buffer's contents
1571    fs.set_head_for_repo(
1572        Path::new(path!("/code/project1/.git")),
1573        &[("src/lib.rs", text_2.clone())],
1574        "sha",
1575    );
1576
1577    cx.executor().run_until_parked();
1578    diff.read_with(cx, |diff, cx| {
1579        assert_eq!(diff.base_text_string().unwrap(), text_2);
1580        assert_eq!(
1581            diff.secondary_diff()
1582                .unwrap()
1583                .read(cx)
1584                .base_text_string()
1585                .unwrap(),
1586            text_2
1587        );
1588    });
1589}
1590
1591#[gpui::test]
1592async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1593    let fs = FakeFs::new(server_cx.executor());
1594    fs.insert_tree(
1595        path!("/code"),
1596        json!({
1597            "project1": {
1598                ".git": {},
1599                "README.md": "# project 1",
1600            },
1601        }),
1602    )
1603    .await;
1604
1605    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1606    let branches = ["main", "dev", "feature-1"];
1607    let branches_set = branches
1608        .iter()
1609        .map(ToString::to_string)
1610        .collect::<HashSet<_>>();
1611    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1612
1613    let (_worktree, _) = project
1614        .update(cx, |project, cx| {
1615            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1616        })
1617        .await
1618        .unwrap();
1619    // Give the worktree a bit of time to index the file system
1620    cx.run_until_parked();
1621
1622    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1623
1624    let remote_branches = repository
1625        .update(cx, |repository, _| repository.branches())
1626        .await
1627        .unwrap()
1628        .unwrap();
1629
1630    let new_branch = branches[2];
1631
1632    let remote_branches = remote_branches
1633        .into_iter()
1634        .map(|branch| branch.name().to_string())
1635        .collect::<HashSet<_>>();
1636
1637    assert_eq!(&remote_branches, &branches_set);
1638
1639    cx.update(|cx| {
1640        repository.update(cx, |repository, _cx| {
1641            repository.change_branch(new_branch.to_string())
1642        })
1643    })
1644    .await
1645    .unwrap()
1646    .unwrap();
1647
1648    cx.run_until_parked();
1649
1650    let server_branch = server_cx.update(|cx| {
1651        headless_project.update(cx, |headless_project, cx| {
1652            headless_project.git_store.update(cx, |git_store, cx| {
1653                git_store
1654                    .repositories()
1655                    .values()
1656                    .next()
1657                    .unwrap()
1658                    .read(cx)
1659                    .branch
1660                    .as_ref()
1661                    .unwrap()
1662                    .clone()
1663            })
1664        })
1665    });
1666
1667    assert_eq!(server_branch.name(), branches[2]);
1668
1669    // Also try creating a new branch
1670    cx.update(|cx| {
1671        repository.update(cx, |repo, _cx| {
1672            repo.create_branch("totally-new-branch".to_string())
1673        })
1674    })
1675    .await
1676    .unwrap()
1677    .unwrap();
1678
1679    cx.update(|cx| {
1680        repository.update(cx, |repo, _cx| {
1681            repo.change_branch("totally-new-branch".to_string())
1682        })
1683    })
1684    .await
1685    .unwrap()
1686    .unwrap();
1687
1688    cx.run_until_parked();
1689
1690    let server_branch = server_cx.update(|cx| {
1691        headless_project.update(cx, |headless_project, cx| {
1692            headless_project.git_store.update(cx, |git_store, cx| {
1693                git_store
1694                    .repositories()
1695                    .values()
1696                    .next()
1697                    .unwrap()
1698                    .read(cx)
1699                    .branch
1700                    .as_ref()
1701                    .unwrap()
1702                    .clone()
1703            })
1704        })
1705    });
1706
1707    assert_eq!(server_branch.name(), "totally-new-branch");
1708}
1709
1710#[gpui::test]
1711async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1712    let fs = FakeFs::new(server_cx.executor());
1713    fs.insert_tree(
1714        path!("/project"),
1715        json!({
1716            "a.txt": "A",
1717            "b.txt": "B",
1718        }),
1719    )
1720    .await;
1721
1722    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1723    project
1724        .update(cx, |project, cx| {
1725            project.find_or_create_worktree(path!("/project"), true, cx)
1726        })
1727        .await
1728        .unwrap();
1729
1730    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
1731
1732    let input = ReadFileToolInput {
1733        path: "project/b.txt".into(),
1734        start_line: None,
1735        end_line: None,
1736    };
1737    let read_tool = Arc::new(ReadFileTool::new(project, action_log));
1738    let (event_stream, _) = ToolCallEventStream::test();
1739
1740    let exists_result = cx.update(|cx| read_tool.clone().run(input, event_stream.clone(), cx));
1741    let output = exists_result.await.unwrap();
1742    assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
1743
1744    let input = ReadFileToolInput {
1745        path: "project/c.txt".into(),
1746        start_line: None,
1747        end_line: None,
1748    };
1749    let does_not_exist_result = cx.update(|cx| read_tool.run(input, event_stream, cx));
1750    does_not_exist_result.await.unwrap_err();
1751}
1752
1753#[gpui::test]
1754async fn test_remote_external_agent_server(
1755    cx: &mut TestAppContext,
1756    server_cx: &mut TestAppContext,
1757) {
1758    let fs = FakeFs::new(server_cx.executor());
1759    fs.insert_tree(path!("/project"), json!({})).await;
1760
1761    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1762    project
1763        .update(cx, |project, cx| {
1764            project.find_or_create_worktree(path!("/project"), true, cx)
1765        })
1766        .await
1767        .unwrap();
1768    let names = project.update(cx, |project, cx| {
1769        project
1770            .agent_server_store()
1771            .read(cx)
1772            .external_agents()
1773            .map(|name| name.to_string())
1774            .collect::<Vec<_>>()
1775    });
1776    pretty_assertions::assert_eq!(names, ["codex", "gemini", "claude"]);
1777    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
1778        settings_store
1779            .set_server_settings(
1780                &json!({
1781                    "agent_servers": {
1782                        "foo": {
1783                            "command": "foo-cli",
1784                            "args": ["--flag"],
1785                            "env": {
1786                                "VAR": "val"
1787                            }
1788                        }
1789                    }
1790                })
1791                .to_string(),
1792                cx,
1793            )
1794            .unwrap();
1795    });
1796    server_cx.run_until_parked();
1797    cx.run_until_parked();
1798    let names = project.update(cx, |project, cx| {
1799        project
1800            .agent_server_store()
1801            .read(cx)
1802            .external_agents()
1803            .map(|name| name.to_string())
1804            .collect::<Vec<_>>()
1805    });
1806    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]);
1807    let (command, root, login) = project
1808        .update(cx, |project, cx| {
1809            project.agent_server_store().update(cx, |store, cx| {
1810                store
1811                    .get_external_agent(&"foo".into())
1812                    .unwrap()
1813                    .get_command(
1814                        None,
1815                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
1816                        None,
1817                        None,
1818                        &mut cx.to_async(),
1819                    )
1820            })
1821        })
1822        .await
1823        .unwrap();
1824    assert_eq!(
1825        command,
1826        AgentServerCommand {
1827            path: "ssh".into(),
1828            args: vec!["foo-cli".into(), "--flag".into()],
1829            env: Some(HashMap::from_iter([
1830                ("VAR".into(), "val".into()),
1831                ("OTHER_VAR".into(), "other-val".into())
1832            ]))
1833        }
1834    );
1835    assert_eq!(&PathBuf::from(root), paths::home_dir());
1836    assert!(login.is_none());
1837}
1838
1839pub async fn init_test(
1840    server_fs: &Arc<FakeFs>,
1841    cx: &mut TestAppContext,
1842    server_cx: &mut TestAppContext,
1843) -> (Entity<Project>, Entity<HeadlessProject>) {
1844    let server_fs = server_fs.clone();
1845    cx.update(|cx| {
1846        release_channel::init(SemanticVersion::default(), cx);
1847    });
1848    server_cx.update(|cx| {
1849        release_channel::init(SemanticVersion::default(), cx);
1850    });
1851    init_logger();
1852
1853    let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
1854    let http_client = Arc::new(BlockedHttpClient);
1855    let node_runtime = NodeRuntime::unavailable();
1856    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1857    let proxy = Arc::new(ExtensionHostProxy::new());
1858    server_cx.update(HeadlessProject::init);
1859    let headless = server_cx.new(|cx| {
1860        client::init_settings(cx);
1861
1862        HeadlessProject::new(
1863            crate::HeadlessAppState {
1864                session: ssh_server_client,
1865                fs: server_fs.clone(),
1866                http_client,
1867                node_runtime,
1868                languages,
1869                extension_host_proxy: proxy,
1870            },
1871            cx,
1872        )
1873    });
1874
1875    let ssh = RemoteClient::fake_client(opts, cx).await;
1876    let project = build_project(ssh, cx);
1877    project
1878        .update(cx, {
1879            let headless = headless.clone();
1880            |_, cx| cx.on_release(|_, _| drop(headless))
1881        })
1882        .detach();
1883    (project, headless)
1884}
1885
1886fn init_logger() {
1887    zlog::init_test();
1888}
1889
1890fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1891    cx.update(|cx| {
1892        if !cx.has_global::<SettingsStore>() {
1893            let settings_store = SettingsStore::test(cx);
1894            cx.set_global(settings_store);
1895        }
1896    });
1897
1898    let client = cx.update(|cx| {
1899        Client::new(
1900            Arc::new(FakeSystemClock::new()),
1901            FakeHttpClient::with_404_response(),
1902            cx,
1903        )
1904    });
1905
1906    let node = NodeRuntime::unavailable();
1907    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1908    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1909    let fs = FakeFs::new(cx.executor());
1910
1911    cx.update(|cx| {
1912        Project::init(&client, cx);
1913        language::init(cx);
1914    });
1915
1916    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
1917}