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