remote_editing_tests.rs

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