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