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 client::{Client, UserStore};
   6use clock::FakeSystemClock;
   7use dap::DapRegistry;
   8use extension::ExtensionHostProxy;
   9use fs::{FakeFs, Fs};
  10use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
  11use http_client::{BlockedHttpClient, FakeHttpClient};
  12use language::{
  13    language_settings::{language_settings, AllLanguageSettings},
  14    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
  15};
  16use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
  17use node_runtime::NodeRuntime;
  18use project::{
  19    search::{SearchQuery, SearchResult},
  20    Project, ProjectPath,
  21};
  22use remote::SshRemoteClient;
  23use serde_json::json;
  24use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
  25use smol::stream::StreamExt;
  26use std::{
  27    collections::HashSet,
  28    path::{Path, PathBuf},
  29    sync::Arc,
  30};
  31use unindent::Unindent as _;
  32use util::{path, separator};
  33
  34#[gpui::test]
  35async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
  36    let fs = FakeFs::new(server_cx.executor());
  37    fs.insert_tree(
  38        path!("/code"),
  39        json!({
  40            "project1": {
  41                ".git": {},
  42                "README.md": "# project 1",
  43                "src": {
  44                    "lib.rs": "fn one() -> usize { 1 }"
  45                }
  46            },
  47            "project2": {
  48                "README.md": "# project 2",
  49            },
  50        }),
  51    )
  52    .await;
  53    fs.set_index_for_repo(
  54        Path::new(path!("/code/project1/.git")),
  55        &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
  56    );
  57
  58    let (project, _headless) = init_test(&fs, cx, server_cx).await;
  59    let (worktree, _) = project
  60        .update(cx, |project, cx| {
  61            project.find_or_create_worktree(path!("/code/project1"), true, cx)
  62        })
  63        .await
  64        .unwrap();
  65
  66    // The client sees the worktree's contents.
  67    cx.executor().run_until_parked();
  68    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
  69    worktree.update(cx, |worktree, _cx| {
  70        assert_eq!(
  71            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
  72            vec![
  73                Path::new("README.md"),
  74                Path::new("src"),
  75                Path::new("src/lib.rs"),
  76            ]
  77        );
  78    });
  79
  80    // The user opens a buffer in the remote worktree. The buffer's
  81    // contents are loaded from the remote filesystem.
  82    let buffer = project
  83        .update(cx, |project, cx| {
  84            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
  85        })
  86        .await
  87        .unwrap();
  88    let diff = project
  89        .update(cx, |project, cx| {
  90            project.open_unstaged_diff(buffer.clone(), cx)
  91        })
  92        .await
  93        .unwrap();
  94
  95    diff.update(cx, |diff, _| {
  96        assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
  97    });
  98
  99    buffer.update(cx, |buffer, cx| {
 100        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 101        let ix = buffer.text().find('1').unwrap();
 102        buffer.edit([(ix..ix + 1, "100")], None, cx);
 103    });
 104
 105    // The user saves the buffer. The new contents are written to the
 106    // remote filesystem.
 107    project
 108        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 109        .await
 110        .unwrap();
 111    assert_eq!(
 112        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 113        "fn one() -> usize { 100 }"
 114    );
 115
 116    // A new file is created in the remote filesystem. The user
 117    // sees the new file.
 118    fs.save(
 119        path!("/code/project1/src/main.rs").as_ref(),
 120        &"fn main() {}".into(),
 121        Default::default(),
 122    )
 123    .await
 124    .unwrap();
 125    cx.executor().run_until_parked();
 126    worktree.update(cx, |worktree, _cx| {
 127        assert_eq!(
 128            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 129            vec![
 130                Path::new("README.md"),
 131                Path::new("src"),
 132                Path::new("src/lib.rs"),
 133                Path::new("src/main.rs"),
 134            ]
 135        );
 136    });
 137
 138    // A file that is currently open in a buffer is renamed.
 139    fs.rename(
 140        path!("/code/project1/src/lib.rs").as_ref(),
 141        path!("/code/project1/src/lib2.rs").as_ref(),
 142        Default::default(),
 143    )
 144    .await
 145    .unwrap();
 146    cx.executor().run_until_parked();
 147    buffer.update(cx, |buffer, _| {
 148        assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
 149    });
 150
 151    fs.set_index_for_repo(
 152        Path::new(path!("/code/project1/.git")),
 153        &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
 154    );
 155    cx.executor().run_until_parked();
 156    diff.update(cx, |diff, _| {
 157        assert_eq!(
 158            diff.base_text_string().unwrap(),
 159            "fn one() -> usize { 100 }"
 160        );
 161    });
 162}
 163
 164#[gpui::test]
 165async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 166    let fs = FakeFs::new(server_cx.executor());
 167    fs.insert_tree(
 168        path!("/code"),
 169        json!({
 170            "project1": {
 171                ".git": {},
 172                "README.md": "# project 1",
 173                "src": {
 174                    "lib.rs": "fn one() -> usize { 1 }"
 175                }
 176            },
 177        }),
 178    )
 179    .await;
 180
 181    let (project, headless) = init_test(&fs, cx, server_cx).await;
 182
 183    project
 184        .update(cx, |project, cx| {
 185            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 186        })
 187        .await
 188        .unwrap();
 189
 190    cx.run_until_parked();
 191
 192    async fn do_search(project: &Entity<Project>, mut cx: TestAppContext) -> Entity<Buffer> {
 193        let receiver = project.update(&mut cx, |project, cx| {
 194            project.search(
 195                SearchQuery::text(
 196                    "project",
 197                    false,
 198                    true,
 199                    false,
 200                    Default::default(),
 201                    Default::default(),
 202                    None,
 203                )
 204                .unwrap(),
 205                cx,
 206            )
 207        });
 208
 209        let first_response = receiver.recv().await.unwrap();
 210        let SearchResult::Buffer { buffer, .. } = first_response else {
 211            panic!("incorrect result");
 212        };
 213        buffer.update(&mut cx, |buffer, cx| {
 214            assert_eq!(
 215                buffer.file().unwrap().full_path(cx).to_string_lossy(),
 216                separator!("project1/README.md")
 217            )
 218        });
 219
 220        assert!(receiver.recv().await.is_err());
 221        buffer
 222    }
 223
 224    let buffer = do_search(&project, cx.clone()).await;
 225
 226    // test that the headless server is tracking which buffers we have open correctly.
 227    cx.run_until_parked();
 228    headless.update(server_cx, |headless, cx| {
 229        assert!(headless.buffer_store.read(cx).has_shared_buffers())
 230    });
 231    do_search(&project, cx.clone()).await;
 232
 233    cx.update(|_| {
 234        drop(buffer);
 235    });
 236    cx.run_until_parked();
 237    headless.update(server_cx, |headless, cx| {
 238        assert!(!headless.buffer_store.read(cx).has_shared_buffers())
 239    });
 240
 241    do_search(&project, cx.clone()).await;
 242}
 243
 244#[gpui::test]
 245async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 246    let fs = FakeFs::new(server_cx.executor());
 247    fs.insert_tree(
 248        "/code",
 249        json!({
 250            "project1": {
 251                ".git": {},
 252                "README.md": "# project 1",
 253                "src": {
 254                    "lib.rs": "fn one() -> usize { 1 }"
 255                }
 256            },
 257        }),
 258    )
 259    .await;
 260
 261    let (project, headless) = init_test(&fs, cx, server_cx).await;
 262
 263    cx.update_global(|settings_store: &mut SettingsStore, cx| {
 264        settings_store.set_user_settings(
 265            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
 266            cx,
 267        )
 268    })
 269    .unwrap();
 270
 271    cx.run_until_parked();
 272
 273    server_cx.read(|cx| {
 274        assert_eq!(
 275            AllLanguageSettings::get_global(cx)
 276                .language(None, Some(&"Rust".into()), cx)
 277                .language_servers,
 278            ["..."] // local settings are ignored
 279        )
 280    });
 281
 282    server_cx
 283        .update_global(|settings_store: &mut SettingsStore, cx| {
 284            settings_store.set_server_settings(
 285                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
 286                cx,
 287            )
 288        })
 289        .unwrap();
 290
 291    cx.run_until_parked();
 292
 293    server_cx.read(|cx| {
 294        assert_eq!(
 295            AllLanguageSettings::get_global(cx)
 296                .language(None, Some(&"Rust".into()), cx)
 297                .language_servers,
 298            ["from-server-settings".to_string()]
 299        )
 300    });
 301
 302    fs.insert_tree(
 303        "/code/project1/.zed",
 304        json!({
 305            "settings.json": r#"
 306                  {
 307                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
 308                    "lsp": {
 309                      "override-rust-analyzer": {
 310                        "binary": {
 311                          "path": "~/.cargo/bin/rust-analyzer"
 312                        }
 313                      }
 314                    }
 315                  }"#
 316        }),
 317    )
 318    .await;
 319
 320    let worktree_id = project
 321        .update(cx, |project, cx| {
 322            project.find_or_create_worktree("/code/project1", true, cx)
 323        })
 324        .await
 325        .unwrap()
 326        .0
 327        .read_with(cx, |worktree, _| worktree.id());
 328
 329    let buffer = project
 330        .update(cx, |project, cx| {
 331            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 332        })
 333        .await
 334        .unwrap();
 335    cx.run_until_parked();
 336
 337    server_cx.read(|cx| {
 338        let worktree_id = headless
 339            .read(cx)
 340            .worktree_store
 341            .read(cx)
 342            .worktrees()
 343            .next()
 344            .unwrap()
 345            .read(cx)
 346            .id();
 347        assert_eq!(
 348            AllLanguageSettings::get(
 349                Some(SettingsLocation {
 350                    worktree_id,
 351                    path: Path::new("src/lib.rs")
 352                }),
 353                cx
 354            )
 355            .language(None, Some(&"Rust".into()), cx)
 356            .language_servers,
 357            ["override-rust-analyzer".to_string()]
 358        )
 359    });
 360
 361    cx.read(|cx| {
 362        let file = buffer.read(cx).file();
 363        assert_eq!(
 364            language_settings(Some("Rust".into()), file, cx).language_servers,
 365            ["override-rust-analyzer".to_string()]
 366        )
 367    });
 368}
 369
 370#[gpui::test]
 371async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 372    let fs = FakeFs::new(server_cx.executor());
 373    fs.insert_tree(
 374        path!("/code"),
 375        json!({
 376            "project1": {
 377                ".git": {},
 378                "README.md": "# project 1",
 379                "src": {
 380                    "lib.rs": "fn one() -> usize { 1 }"
 381                }
 382            },
 383        }),
 384    )
 385    .await;
 386
 387    let (project, headless) = init_test(&fs, cx, server_cx).await;
 388
 389    fs.insert_tree(
 390        path!("/code/project1/.zed"),
 391        json!({
 392            "settings.json": r#"
 393          {
 394            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 395            "lsp": {
 396              "rust-analyzer": {
 397                "binary": {
 398                  "path": "~/.cargo/bin/rust-analyzer"
 399                }
 400              }
 401            }
 402          }"#
 403        }),
 404    )
 405    .await;
 406
 407    cx.update_entity(&project, |project, _| {
 408        project.languages().register_test_language(LanguageConfig {
 409            name: "Rust".into(),
 410            matcher: LanguageMatcher {
 411                path_suffixes: vec!["rs".into()],
 412                ..Default::default()
 413            },
 414            ..Default::default()
 415        });
 416        project.languages().register_fake_lsp_adapter(
 417            "Rust",
 418            FakeLspAdapter {
 419                name: "rust-analyzer",
 420                ..Default::default()
 421            },
 422        )
 423    });
 424
 425    let mut fake_lsp = server_cx.update(|cx| {
 426        headless.read(cx).languages.register_fake_language_server(
 427            LanguageServerName("rust-analyzer".into()),
 428            Default::default(),
 429            None,
 430        )
 431    });
 432
 433    cx.run_until_parked();
 434
 435    let worktree_id = project
 436        .update(cx, |project, cx| {
 437            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 438        })
 439        .await
 440        .unwrap()
 441        .0
 442        .read_with(cx, |worktree, _| worktree.id());
 443
 444    // Wait for the settings to synchronize
 445    cx.run_until_parked();
 446
 447    let (buffer, _handle) = project
 448        .update(cx, |project, cx| {
 449            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
 450        })
 451        .await
 452        .unwrap();
 453    cx.run_until_parked();
 454
 455    let fake_lsp = fake_lsp.next().await.unwrap();
 456
 457    cx.read(|cx| {
 458        let file = buffer.read(cx).file();
 459        assert_eq!(
 460            language_settings(Some("Rust".into()), file, cx).language_servers,
 461            ["rust-analyzer".to_string()]
 462        )
 463    });
 464
 465    let buffer_id = cx.read(|cx| {
 466        let buffer = buffer.read(cx);
 467        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
 468        buffer.remote_id()
 469    });
 470
 471    server_cx.read(|cx| {
 472        let buffer = headless
 473            .read(cx)
 474            .buffer_store
 475            .read(cx)
 476            .get(buffer_id)
 477            .unwrap();
 478
 479        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
 480    });
 481
 482    server_cx.read(|cx| {
 483        let lsp_store = headless.read(cx).lsp_store.read(cx);
 484        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
 485    });
 486
 487    fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 488        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
 489            label: "boop".to_string(),
 490            ..Default::default()
 491        }])))
 492    });
 493
 494    let result = project
 495        .update(cx, |project, cx| {
 496            project.completions(
 497                &buffer,
 498                0,
 499                CompletionContext {
 500                    trigger_kind: CompletionTriggerKind::INVOKED,
 501                    trigger_character: None,
 502                },
 503                cx,
 504            )
 505        })
 506        .await
 507        .unwrap();
 508
 509    assert_eq!(
 510        result
 511            .unwrap()
 512            .into_iter()
 513            .map(|c| c.label.text)
 514            .collect::<Vec<_>>(),
 515        vec!["boop".to_string()]
 516    );
 517
 518    fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
 519        Ok(Some(lsp::WorkspaceEdit {
 520            changes: Some(
 521                [(
 522                    lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
 523                    vec![lsp::TextEdit::new(
 524                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
 525                        "two".to_string(),
 526                    )],
 527                )]
 528                .into_iter()
 529                .collect(),
 530            ),
 531            ..Default::default()
 532        }))
 533    });
 534
 535    project
 536        .update(cx, |project, cx| {
 537            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
 538        })
 539        .await
 540        .unwrap();
 541
 542    cx.run_until_parked();
 543    buffer.update(cx, |buffer, _| {
 544        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
 545    })
 546}
 547
 548#[gpui::test]
 549async fn test_remote_cancel_language_server_work(
 550    cx: &mut TestAppContext,
 551    server_cx: &mut TestAppContext,
 552) {
 553    let fs = FakeFs::new(server_cx.executor());
 554    fs.insert_tree(
 555        path!("/code"),
 556        json!({
 557            "project1": {
 558                ".git": {},
 559                "README.md": "# project 1",
 560                "src": {
 561                    "lib.rs": "fn one() -> usize { 1 }"
 562                }
 563            },
 564        }),
 565    )
 566    .await;
 567
 568    let (project, headless) = init_test(&fs, cx, server_cx).await;
 569
 570    fs.insert_tree(
 571        path!("/code/project1/.zed"),
 572        json!({
 573            "settings.json": r#"
 574          {
 575            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
 576            "lsp": {
 577              "rust-analyzer": {
 578                "binary": {
 579                  "path": "~/.cargo/bin/rust-analyzer"
 580                }
 581              }
 582            }
 583          }"#
 584        }),
 585    )
 586    .await;
 587
 588    cx.update_entity(&project, |project, _| {
 589        project.languages().register_test_language(LanguageConfig {
 590            name: "Rust".into(),
 591            matcher: LanguageMatcher {
 592                path_suffixes: vec!["rs".into()],
 593                ..Default::default()
 594            },
 595            ..Default::default()
 596        });
 597        project.languages().register_fake_lsp_adapter(
 598            "Rust",
 599            FakeLspAdapter {
 600                name: "rust-analyzer",
 601                ..Default::default()
 602            },
 603        )
 604    });
 605
 606    let mut fake_lsp = server_cx.update(|cx| {
 607        headless.read(cx).languages.register_fake_language_server(
 608            LanguageServerName("rust-analyzer".into()),
 609            Default::default(),
 610            None,
 611        )
 612    });
 613
 614    cx.run_until_parked();
 615
 616    let worktree_id = project
 617        .update(cx, |project, cx| {
 618            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 619        })
 620        .await
 621        .unwrap()
 622        .0
 623        .read_with(cx, |worktree, _| worktree.id());
 624
 625    cx.run_until_parked();
 626
 627    let (buffer, _handle) = project
 628        .update(cx, |project, cx| {
 629            project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
 630        })
 631        .await
 632        .unwrap();
 633
 634    cx.run_until_parked();
 635
 636    let mut fake_lsp = fake_lsp.next().await.unwrap();
 637
 638    // Cancelling all language server work for a given buffer
 639    {
 640        // Two operations, one cancellable and one not.
 641        fake_lsp
 642            .start_progress_with(
 643                "another-token",
 644                lsp::WorkDoneProgressBegin {
 645                    cancellable: Some(false),
 646                    ..Default::default()
 647                },
 648            )
 649            .await;
 650
 651        let progress_token = "the-progress-token";
 652        fake_lsp
 653            .start_progress_with(
 654                progress_token,
 655                lsp::WorkDoneProgressBegin {
 656                    cancellable: Some(true),
 657                    ..Default::default()
 658                },
 659            )
 660            .await;
 661
 662        cx.executor().run_until_parked();
 663
 664        project.update(cx, |project, cx| {
 665            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 666        });
 667
 668        cx.executor().run_until_parked();
 669
 670        // Verify the cancellation was received on the server side
 671        let cancel_notification = fake_lsp
 672            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 673            .await;
 674        assert_eq!(
 675            cancel_notification.token,
 676            lsp::NumberOrString::String(progress_token.into())
 677        );
 678    }
 679
 680    // Cancelling work by server_id and token
 681    {
 682        let server_id = fake_lsp.server.server_id();
 683        let progress_token = "the-progress-token";
 684
 685        fake_lsp
 686            .start_progress_with(
 687                progress_token,
 688                lsp::WorkDoneProgressBegin {
 689                    cancellable: Some(true),
 690                    ..Default::default()
 691                },
 692            )
 693            .await;
 694
 695        cx.executor().run_until_parked();
 696
 697        project.update(cx, |project, cx| {
 698            project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
 699        });
 700
 701        cx.executor().run_until_parked();
 702
 703        // Verify the cancellation was received on the server side
 704        let cancel_notification = fake_lsp
 705            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 706            .await;
 707        assert_eq!(
 708            cancel_notification.token,
 709            lsp::NumberOrString::String(progress_token.into())
 710        );
 711    }
 712}
 713
 714#[gpui::test]
 715async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 716    let fs = FakeFs::new(server_cx.executor());
 717    fs.insert_tree(
 718        path!("/code"),
 719        json!({
 720            "project1": {
 721                ".git": {},
 722                "README.md": "# project 1",
 723                "src": {
 724                    "lib.rs": "fn one() -> usize { 1 }"
 725                }
 726            },
 727        }),
 728    )
 729    .await;
 730
 731    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 732    let (worktree, _) = project
 733        .update(cx, |project, cx| {
 734            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 735        })
 736        .await
 737        .unwrap();
 738
 739    let worktree_id = cx.update(|cx| worktree.read(cx).id());
 740
 741    let buffer = project
 742        .update(cx, |project, cx| {
 743            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 744        })
 745        .await
 746        .unwrap();
 747
 748    fs.save(
 749        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 750        &("bangles".to_string().into()),
 751        LineEnding::Unix,
 752    )
 753    .await
 754    .unwrap();
 755
 756    cx.run_until_parked();
 757
 758    buffer.update(cx, |buffer, cx| {
 759        assert_eq!(buffer.text(), "bangles");
 760        buffer.edit([(0..0, "a")], None, cx);
 761    });
 762
 763    fs.save(
 764        &PathBuf::from(path!("/code/project1/src/lib.rs")),
 765        &("bloop".to_string().into()),
 766        LineEnding::Unix,
 767    )
 768    .await
 769    .unwrap();
 770
 771    cx.run_until_parked();
 772    cx.update(|cx| {
 773        assert!(buffer.read(cx).has_conflict());
 774    });
 775
 776    project
 777        .update(cx, |project, cx| {
 778            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
 779        })
 780        .await
 781        .unwrap();
 782    cx.run_until_parked();
 783
 784    cx.update(|cx| {
 785        assert!(!buffer.read(cx).has_conflict());
 786    });
 787}
 788
 789#[gpui::test]
 790async fn test_remote_resolve_path_in_buffer(
 791    cx: &mut TestAppContext,
 792    server_cx: &mut TestAppContext,
 793) {
 794    let fs = FakeFs::new(server_cx.executor());
 795    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
 796    fs.insert_tree(
 797        path!("/code"),
 798        json!({
 799            "project1": {
 800                ".git": {},
 801                "README.md": "# project 1",
 802                "src": {
 803                    "lib.rs": "fn one() -> usize { 1 }"
 804                }
 805            },
 806            "project2": {
 807                ".git": {},
 808                "README.md": "# project 2",
 809                "src": {
 810                    "lib.rs": "fn two() -> usize { 2 }"
 811                }
 812            }
 813        }),
 814    )
 815    .await;
 816
 817    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 818
 819    let _ = project
 820        .update(cx, |project, cx| {
 821            project.find_or_create_worktree(path!("/code/project1"), true, cx)
 822        })
 823        .await
 824        .unwrap();
 825
 826    let (worktree2, _) = project
 827        .update(cx, |project, cx| {
 828            project.find_or_create_worktree(path!("/code/project2"), true, cx)
 829        })
 830        .await
 831        .unwrap();
 832
 833    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
 834
 835    let buffer2 = project
 836        .update(cx, |project, cx| {
 837            project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
 838        })
 839        .await
 840        .unwrap();
 841
 842    let path = project
 843        .update(cx, |project, cx| {
 844            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
 845        })
 846        .await
 847        .unwrap();
 848    assert!(path.is_file());
 849    assert_eq!(
 850        path.abs_path().unwrap().to_string_lossy(),
 851        path!("/code/project2/README.md")
 852    );
 853
 854    let path = project
 855        .update(cx, |project, cx| {
 856            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
 857        })
 858        .await
 859        .unwrap();
 860    assert!(path.is_file());
 861    assert_eq!(
 862        path.project_path().unwrap().clone(),
 863        ProjectPath::from((worktree2_id, "README.md"))
 864    );
 865
 866    let path = project
 867        .update(cx, |project, cx| {
 868            project.resolve_path_in_buffer("../src", &buffer2, cx)
 869        })
 870        .await
 871        .unwrap();
 872    assert_eq!(
 873        path.project_path().unwrap().clone(),
 874        ProjectPath::from((worktree2_id, "src"))
 875    );
 876    assert!(path.is_dir());
 877}
 878
 879#[gpui::test]
 880async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 881    let fs = FakeFs::new(server_cx.executor());
 882    fs.insert_tree(
 883        path!("/code"),
 884        json!({
 885            "project1": {
 886                ".git": {},
 887                "README.md": "# project 1",
 888                "src": {
 889                    "lib.rs": "fn one() -> usize { 1 }"
 890                }
 891            },
 892        }),
 893    )
 894    .await;
 895
 896    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 897
 898    let path = project
 899        .update(cx, |project, cx| {
 900            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
 901        })
 902        .await
 903        .unwrap();
 904
 905    assert!(path.is_file());
 906    assert_eq!(
 907        path.abs_path().unwrap().to_string_lossy(),
 908        path!("/code/project1/README.md")
 909    );
 910
 911    let path = project
 912        .update(cx, |project, cx| {
 913            project.resolve_abs_path(path!("/code/project1/src"), cx)
 914        })
 915        .await
 916        .unwrap();
 917
 918    assert!(path.is_dir());
 919    assert_eq!(
 920        path.abs_path().unwrap().to_string_lossy(),
 921        path!("/code/project1/src")
 922    );
 923
 924    let path = project
 925        .update(cx, |project, cx| {
 926            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
 927        })
 928        .await;
 929    assert!(path.is_none());
 930}
 931
 932#[gpui::test(iterations = 10)]
 933async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 934    let fs = FakeFs::new(server_cx.executor());
 935    fs.insert_tree(
 936        "/code",
 937        json!({
 938            "project1": {
 939                ".git": {},
 940                "README.md": "# project 1",
 941                "src": {
 942                    "lib.rs": "fn one() -> usize { 1 }"
 943                }
 944            },
 945        }),
 946    )
 947    .await;
 948
 949    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 950    let (worktree, _) = project
 951        .update(cx, |project, cx| {
 952            project.find_or_create_worktree("/code/project1", true, cx)
 953        })
 954        .await
 955        .unwrap();
 956    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 957
 958    // Open a buffer on the client but cancel after a random amount of time.
 959    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
 960    cx.executor().simulate_random_delay().await;
 961    drop(buffer);
 962
 963    // Try opening the same buffer again as the client, and ensure we can
 964    // still do it despite the cancellation above.
 965    let buffer = project
 966        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
 967        .await
 968        .unwrap();
 969
 970    buffer.read_with(cx, |buf, _| {
 971        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
 972    });
 973}
 974
 975#[gpui::test]
 976async fn test_adding_then_removing_then_adding_worktrees(
 977    cx: &mut TestAppContext,
 978    server_cx: &mut TestAppContext,
 979) {
 980    let fs = FakeFs::new(server_cx.executor());
 981    fs.insert_tree(
 982        path!("/code"),
 983        json!({
 984            "project1": {
 985                ".git": {},
 986                "README.md": "# project 1",
 987                "src": {
 988                    "lib.rs": "fn one() -> usize { 1 }"
 989                }
 990            },
 991            "project2": {
 992                "README.md": "# project 2",
 993            },
 994        }),
 995    )
 996    .await;
 997
 998    let (project, _headless) = init_test(&fs, cx, server_cx).await;
 999    let (_worktree, _) = project
1000        .update(cx, |project, cx| {
1001            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1002        })
1003        .await
1004        .unwrap();
1005
1006    let (worktree_2, _) = project
1007        .update(cx, |project, cx| {
1008            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1009        })
1010        .await
1011        .unwrap();
1012    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1013
1014    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1015
1016    let (worktree_2, _) = project
1017        .update(cx, |project, cx| {
1018            project.find_or_create_worktree(path!("/code/project2"), true, cx)
1019        })
1020        .await
1021        .unwrap();
1022
1023    cx.run_until_parked();
1024    worktree_2.update(cx, |worktree, _cx| {
1025        assert!(worktree.is_visible());
1026        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1027        assert_eq!(entries.len(), 2);
1028        assert_eq!(
1029            entries[1].path.to_string_lossy().to_string(),
1030            "README.md".to_string()
1031        )
1032    })
1033}
1034
1035#[gpui::test]
1036async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1037    let fs = FakeFs::new(server_cx.executor());
1038    fs.insert_tree(
1039        path!("/code"),
1040        json!({
1041            "project1": {
1042                ".git": {},
1043                "README.md": "# project 1",
1044                "src": {
1045                    "lib.rs": "fn one() -> usize { 1 }"
1046                }
1047            },
1048        }),
1049    )
1050    .await;
1051
1052    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1053    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1054    cx.executor().run_until_parked();
1055
1056    let buffer = buffer.await.unwrap();
1057
1058    cx.update(|cx| {
1059        assert_eq!(
1060            buffer.read(cx).text(),
1061            initial_server_settings_content()
1062                .to_string()
1063                .replace("\r\n", "\n")
1064        )
1065    })
1066}
1067
1068#[gpui::test(iterations = 20)]
1069async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1070    let fs = FakeFs::new(server_cx.executor());
1071    fs.insert_tree(
1072        path!("/code"),
1073        json!({
1074            "project1": {
1075                ".git": {},
1076                "README.md": "# project 1",
1077                "src": {
1078                    "lib.rs": "fn one() -> usize { 1 }"
1079                }
1080            },
1081        }),
1082    )
1083    .await;
1084
1085    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1086
1087    let (worktree, _) = project
1088        .update(cx, |project, cx| {
1089            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1090        })
1091        .await
1092        .unwrap();
1093
1094    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1095    let buffer = project
1096        .update(cx, |project, cx| {
1097            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1098        })
1099        .await
1100        .unwrap();
1101
1102    buffer.update(cx, |buffer, cx| {
1103        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1104        let ix = buffer.text().find('1').unwrap();
1105        buffer.edit([(ix..ix + 1, "100")], None, cx);
1106    });
1107
1108    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1109    client
1110        .update(cx, |client, cx| client.simulate_disconnect(cx))
1111        .detach();
1112
1113    project
1114        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1115        .await
1116        .unwrap();
1117
1118    assert_eq!(
1119        fs.load(path!("/code/project1/src/lib.rs").as_ref())
1120            .await
1121            .unwrap(),
1122        "fn one() -> usize { 100 }"
1123    );
1124}
1125
1126#[gpui::test]
1127async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1128    let fs = FakeFs::new(server_cx.executor());
1129    fs.insert_tree(
1130        "/code",
1131        json!({
1132            "project1": {
1133                ".git": {},
1134                "README.md": "# project 1",
1135            },
1136        }),
1137    )
1138    .await;
1139
1140    let (project, _) = init_test(&fs, cx, server_cx).await;
1141
1142    let (worktree, _) = project
1143        .update(cx, |project, cx| {
1144            project.find_or_create_worktree("/code/project1", true, cx)
1145        })
1146        .await
1147        .unwrap();
1148
1149    cx.run_until_parked();
1150
1151    fs.rename(
1152        &PathBuf::from("/code/project1"),
1153        &PathBuf::from("/code/project2"),
1154        Default::default(),
1155    )
1156    .await
1157    .unwrap();
1158
1159    cx.run_until_parked();
1160    worktree.update(cx, |worktree, _| {
1161        assert_eq!(worktree.root_name(), "project2")
1162    })
1163}
1164
1165#[gpui::test]
1166async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1167    let fs = FakeFs::new(server_cx.executor());
1168    fs.insert_tree(
1169        "/code",
1170        json!({
1171            "project1": {
1172                ".git": {},
1173                "README.md": "# project 1",
1174            },
1175        }),
1176    )
1177    .await;
1178
1179    let (project, _) = init_test(&fs, cx, server_cx).await;
1180    let (worktree, _) = project
1181        .update(cx, |project, cx| {
1182            project.find_or_create_worktree("/code/project1", true, cx)
1183        })
1184        .await
1185        .unwrap();
1186
1187    cx.run_until_parked();
1188
1189    let entry = worktree
1190        .update(cx, |worktree, cx| {
1191            let entry = worktree.entry_for_path("README.md").unwrap();
1192            worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1193        })
1194        .await
1195        .unwrap()
1196        .to_included()
1197        .unwrap();
1198
1199    cx.run_until_parked();
1200
1201    worktree.update(cx, |worktree, _| {
1202        assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1203    });
1204}
1205
1206#[gpui::test]
1207async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1208    let text_2 = "
1209        fn one() -> usize {
1210            1
1211        }
1212    "
1213    .unindent();
1214    let text_1 = "
1215        fn one() -> usize {
1216            0
1217        }
1218    "
1219    .unindent();
1220
1221    let fs = FakeFs::new(server_cx.executor());
1222    fs.insert_tree(
1223        "/code",
1224        json!({
1225            "project1": {
1226                ".git": {},
1227                "src": {
1228                    "lib.rs": text_2
1229                },
1230                "README.md": "# project 1",
1231            },
1232        }),
1233    )
1234    .await;
1235    fs.set_index_for_repo(
1236        Path::new("/code/project1/.git"),
1237        &[("src/lib.rs".into(), text_1.clone())],
1238    );
1239    fs.set_head_for_repo(
1240        Path::new("/code/project1/.git"),
1241        &[("src/lib.rs".into(), text_1.clone())],
1242    );
1243
1244    let (project, _headless) = init_test(&fs, cx, server_cx).await;
1245    let (worktree, _) = project
1246        .update(cx, |project, cx| {
1247            project.find_or_create_worktree("/code/project1", true, cx)
1248        })
1249        .await
1250        .unwrap();
1251    let worktree_id = cx.update(|cx| worktree.read(cx).id());
1252    cx.executor().run_until_parked();
1253
1254    let buffer = project
1255        .update(cx, |project, cx| {
1256            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1257        })
1258        .await
1259        .unwrap();
1260    let diff = project
1261        .update(cx, |project, cx| {
1262            project.open_uncommitted_diff(buffer.clone(), cx)
1263        })
1264        .await
1265        .unwrap();
1266
1267    diff.read_with(cx, |diff, cx| {
1268        assert_eq!(diff.base_text_string().unwrap(), text_1);
1269        assert_eq!(
1270            diff.secondary_diff()
1271                .unwrap()
1272                .read(cx)
1273                .base_text_string()
1274                .unwrap(),
1275            text_1
1276        );
1277    });
1278
1279    // stage the current buffer's contents
1280    fs.set_index_for_repo(
1281        Path::new("/code/project1/.git"),
1282        &[("src/lib.rs".into(), text_2.clone())],
1283    );
1284
1285    cx.executor().run_until_parked();
1286    diff.read_with(cx, |diff, cx| {
1287        assert_eq!(diff.base_text_string().unwrap(), text_1);
1288        assert_eq!(
1289            diff.secondary_diff()
1290                .unwrap()
1291                .read(cx)
1292                .base_text_string()
1293                .unwrap(),
1294            text_2
1295        );
1296    });
1297
1298    // commit the current buffer's contents
1299    fs.set_head_for_repo(
1300        Path::new("/code/project1/.git"),
1301        &[("src/lib.rs".into(), text_2.clone())],
1302    );
1303
1304    cx.executor().run_until_parked();
1305    diff.read_with(cx, |diff, cx| {
1306        assert_eq!(diff.base_text_string().unwrap(), text_2);
1307        assert_eq!(
1308            diff.secondary_diff()
1309                .unwrap()
1310                .read(cx)
1311                .base_text_string()
1312                .unwrap(),
1313            text_2
1314        );
1315    });
1316}
1317
1318#[gpui::test]
1319async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1320    let fs = FakeFs::new(server_cx.executor());
1321    fs.insert_tree(
1322        path!("/code"),
1323        json!({
1324            "project1": {
1325                ".git": {},
1326                "README.md": "# project 1",
1327            },
1328        }),
1329    )
1330    .await;
1331
1332    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1333    let branches = ["main", "dev", "feature-1"];
1334    let branches_set = branches
1335        .iter()
1336        .map(ToString::to_string)
1337        .collect::<HashSet<_>>();
1338    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1339
1340    let (_worktree, _) = project
1341        .update(cx, |project, cx| {
1342            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1343        })
1344        .await
1345        .unwrap();
1346    // Give the worktree a bit of time to index the file system
1347    cx.run_until_parked();
1348
1349    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1350
1351    let remote_branches = repository
1352        .update(cx, |repository, _| repository.branches())
1353        .await
1354        .unwrap()
1355        .unwrap();
1356
1357    let new_branch = branches[2];
1358
1359    let remote_branches = remote_branches
1360        .into_iter()
1361        .map(|branch| branch.name.to_string())
1362        .collect::<HashSet<_>>();
1363
1364    assert_eq!(&remote_branches, &branches_set);
1365
1366    cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
1367        .await
1368        .unwrap()
1369        .unwrap();
1370
1371    cx.run_until_parked();
1372
1373    let server_branch = server_cx.update(|cx| {
1374        headless_project.update(cx, |headless_project, cx| {
1375            headless_project.git_store.update(cx, |git_store, cx| {
1376                git_store
1377                    .repositories()
1378                    .values()
1379                    .next()
1380                    .unwrap()
1381                    .read(cx)
1382                    .current_branch()
1383                    .unwrap()
1384                    .clone()
1385            })
1386        })
1387    });
1388
1389    assert_eq!(server_branch.name, branches[2]);
1390
1391    // Also try creating a new branch
1392    cx.update(|cx| {
1393        repository
1394            .read(cx)
1395            .create_branch("totally-new-branch".to_string())
1396    })
1397    .await
1398    .unwrap()
1399    .unwrap();
1400
1401    cx.update(|cx| {
1402        repository
1403            .read(cx)
1404            .change_branch("totally-new-branch".to_string())
1405    })
1406    .await
1407    .unwrap()
1408    .unwrap();
1409
1410    cx.run_until_parked();
1411
1412    let server_branch = server_cx.update(|cx| {
1413        headless_project.update(cx, |headless_project, cx| {
1414            headless_project.git_store.update(cx, |git_store, cx| {
1415                git_store
1416                    .repositories()
1417                    .values()
1418                    .next()
1419                    .unwrap()
1420                    .read(cx)
1421                    .current_branch()
1422                    .unwrap()
1423                    .clone()
1424            })
1425        })
1426    });
1427
1428    assert_eq!(server_branch.name, "totally-new-branch");
1429}
1430
1431pub async fn init_test(
1432    server_fs: &Arc<FakeFs>,
1433    cx: &mut TestAppContext,
1434    server_cx: &mut TestAppContext,
1435) -> (Entity<Project>, Entity<HeadlessProject>) {
1436    let server_fs = server_fs.clone();
1437    cx.update(|cx| {
1438        release_channel::init(SemanticVersion::default(), cx);
1439    });
1440    server_cx.update(|cx| {
1441        release_channel::init(SemanticVersion::default(), cx);
1442    });
1443    init_logger();
1444
1445    let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1446    let http_client = Arc::new(BlockedHttpClient);
1447    let node_runtime = NodeRuntime::unavailable();
1448    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1449    let debug_adapters = DapRegistry::default().into();
1450    let proxy = Arc::new(ExtensionHostProxy::new());
1451    server_cx.update(HeadlessProject::init);
1452    let headless = server_cx.new(|cx| {
1453        client::init_settings(cx);
1454
1455        HeadlessProject::new(
1456            crate::HeadlessAppState {
1457                session: ssh_server_client,
1458                fs: server_fs.clone(),
1459                http_client,
1460                node_runtime,
1461                languages,
1462                debug_adapters,
1463                extension_host_proxy: proxy,
1464            },
1465            cx,
1466        )
1467    });
1468
1469    let ssh = SshRemoteClient::fake_client(opts, cx).await;
1470    let project = build_project(ssh, cx);
1471    project
1472        .update(cx, {
1473            let headless = headless.clone();
1474            |_, cx| cx.on_release(|_, _| drop(headless))
1475        })
1476        .detach();
1477    (project, headless)
1478}
1479
1480fn init_logger() {
1481    if std::env::var("RUST_LOG").is_ok() {
1482        env_logger::try_init().ok();
1483    }
1484}
1485
1486fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1487    cx.update(|cx| {
1488        if !cx.has_global::<SettingsStore>() {
1489            let settings_store = SettingsStore::test(cx);
1490            cx.set_global(settings_store);
1491        }
1492    });
1493
1494    let client = cx.update(|cx| {
1495        Client::new(
1496            Arc::new(FakeSystemClock::new()),
1497            FakeHttpClient::with_404_response(),
1498            cx,
1499        )
1500    });
1501
1502    let node = NodeRuntime::unavailable();
1503    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1504    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1505    let fs = FakeFs::new(cx.executor());
1506
1507    cx.update(|cx| {
1508        Project::init(&client, cx);
1509        language::init(cx);
1510    });
1511
1512    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1513}