remote_editing_tests.rs

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