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