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