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