remote_editing_tests.rs

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