remote_editing_tests.rs

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