remote_editing_tests.rs

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