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