remote_editing_tests.rs

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