extension_store_test.rs

   1use crate::{
   2    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
   3    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
   4    RELOAD_DEBOUNCE_DURATION, SchemaVersion,
   5};
   6use async_compression::futures::bufread::GzipEncoder;
   7use collections::{BTreeMap, HashSet};
   8use extension::ExtensionHostProxy;
   9use fs::{FakeFs, Fs, RealFs};
  10use futures::{AsyncReadExt, FutureExt, StreamExt, io::BufReader};
  11use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
  12use http_client::{FakeHttpClient, Response};
  13use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry};
  14use language_extension::LspAccess;
  15use lsp::LanguageServerName;
  16use node_runtime::NodeRuntime;
  17use parking_lot::Mutex;
  18use project::{DEFAULT_COMPLETION_CONTEXT, Project};
  19use release_channel::AppVersion;
  20use reqwest_client::ReqwestClient;
  21use serde_json::json;
  22use settings::SettingsStore;
  23use std::{
  24    ffi::OsString,
  25    path::{Path, PathBuf},
  26    sync::Arc,
  27};
  28use theme::ThemeRegistry;
  29use util::{rel_path::rel_path_buf, test::TempTree};
  30
  31#[cfg(test)]
  32#[ctor::ctor]
  33fn init_logger() {
  34    zlog::init_test();
  35}
  36
  37#[gpui::test]
  38async fn test_extension_store(cx: &mut TestAppContext) {
  39    init_test(cx);
  40
  41    let fs = FakeFs::new(cx.executor());
  42    let http_client = FakeHttpClient::with_200_response();
  43
  44    fs.insert_tree(
  45        "/the-extension-dir",
  46        json!({
  47            "installed": {
  48                "zed-monokai": {
  49                    "extension.json": r#"{
  50                        "id": "zed-monokai",
  51                        "name": "Zed Monokai",
  52                        "version": "2.0.0",
  53                        "themes": {
  54                            "Monokai Dark": "themes/monokai.json",
  55                            "Monokai Light": "themes/monokai.json",
  56                            "Monokai Pro Dark": "themes/monokai-pro.json",
  57                            "Monokai Pro Light": "themes/monokai-pro.json"
  58                        }
  59                    }"#,
  60                    "themes": {
  61                        "monokai.json": r#"{
  62                            "name": "Monokai",
  63                            "author": "Someone",
  64                            "themes": [
  65                                {
  66                                    "name": "Monokai Dark",
  67                                    "appearance": "dark",
  68                                    "style": {}
  69                                },
  70                                {
  71                                    "name": "Monokai Light",
  72                                    "appearance": "light",
  73                                    "style": {}
  74                                }
  75                            ]
  76                        }"#,
  77                        "monokai-pro.json": r#"{
  78                            "name": "Monokai Pro",
  79                            "author": "Someone",
  80                            "themes": [
  81                                {
  82                                    "name": "Monokai Pro Dark",
  83                                    "appearance": "dark",
  84                                    "style": {}
  85                                },
  86                                {
  87                                    "name": "Monokai Pro Light",
  88                                    "appearance": "light",
  89                                    "style": {}
  90                                }
  91                            ]
  92                        }"#,
  93                    }
  94                },
  95                "zed-ruby": {
  96                    "extension.json": r#"{
  97                        "id": "zed-ruby",
  98                        "name": "Zed Ruby",
  99                        "version": "1.0.0",
 100                        "grammars": {
 101                            "ruby": "grammars/ruby.wasm",
 102                            "embedded_template": "grammars/embedded_template.wasm"
 103                        },
 104                        "languages": {
 105                            "ruby": "languages/ruby",
 106                            "erb": "languages/erb"
 107                        }
 108                    }"#,
 109                    "grammars": {
 110                        "ruby.wasm": "",
 111                        "embedded_template.wasm": "",
 112                    },
 113                    "languages": {
 114                        "ruby": {
 115                            "config.toml": r#"
 116                                name = "Ruby"
 117                                grammar = "ruby"
 118                                path_suffixes = ["rb"]
 119                            "#,
 120                            "highlights.scm": "",
 121                        },
 122                        "erb": {
 123                            "config.toml": r#"
 124                                name = "ERB"
 125                                grammar = "embedded_template"
 126                                path_suffixes = ["erb"]
 127                            "#,
 128                            "highlights.scm": "",
 129                        }
 130                    },
 131                }
 132            }
 133        }),
 134    )
 135    .await;
 136
 137    let mut expected_index = ExtensionIndex {
 138        extensions: [
 139            (
 140                "zed-ruby".into(),
 141                ExtensionIndexEntry {
 142                    manifest: Arc::new(ExtensionManifest {
 143                        id: "zed-ruby".into(),
 144                        name: "Zed Ruby".into(),
 145                        version: "1.0.0".into(),
 146                        schema_version: SchemaVersion::ZERO,
 147                        description: None,
 148                        authors: Vec::new(),
 149                        repository: None,
 150                        themes: Default::default(),
 151                        icon_themes: Vec::new(),
 152                        lib: Default::default(),
 153                        languages: vec![
 154                            rel_path_buf("languages/erb"),
 155                            rel_path_buf("languages/ruby"),
 156                        ],
 157                        grammars: [
 158                            ("embedded_template".into(), GrammarManifestEntry::default()),
 159                            ("ruby".into(), GrammarManifestEntry::default()),
 160                        ]
 161                        .into_iter()
 162                        .collect(),
 163                        language_servers: BTreeMap::default(),
 164                        context_servers: BTreeMap::default(),
 165                        agent_servers: BTreeMap::default(),
 166                        slash_commands: BTreeMap::default(),
 167                        snippets: None,
 168                        capabilities: Vec::new(),
 169                        debug_adapters: Default::default(),
 170                        debug_locators: Default::default(),
 171                        language_model_providers: BTreeMap::default(),
 172                    }),
 173                    dev: false,
 174                },
 175            ),
 176            (
 177                "zed-monokai".into(),
 178                ExtensionIndexEntry {
 179                    manifest: Arc::new(ExtensionManifest {
 180                        id: "zed-monokai".into(),
 181                        name: "Zed Monokai".into(),
 182                        version: "2.0.0".into(),
 183                        schema_version: SchemaVersion::ZERO,
 184                        description: None,
 185                        authors: vec![],
 186                        repository: None,
 187                        themes: vec![
 188                            rel_path_buf("themes/monokai-pro.json"),
 189                            rel_path_buf("themes/monokai.json"),
 190                        ],
 191                        icon_themes: Vec::new(),
 192                        lib: Default::default(),
 193                        languages: Default::default(),
 194                        grammars: BTreeMap::default(),
 195                        language_servers: BTreeMap::default(),
 196                        context_servers: BTreeMap::default(),
 197                        agent_servers: BTreeMap::default(),
 198                        slash_commands: BTreeMap::default(),
 199                        snippets: None,
 200                        capabilities: Vec::new(),
 201                        debug_adapters: Default::default(),
 202                        debug_locators: Default::default(),
 203                        language_model_providers: BTreeMap::default(),
 204                    }),
 205                    dev: false,
 206                },
 207            ),
 208        ]
 209        .into_iter()
 210        .collect(),
 211        languages: [
 212            (
 213                "ERB".into(),
 214                ExtensionIndexLanguageEntry {
 215                    extension: "zed-ruby".into(),
 216                    path: "languages/erb".into(),
 217                    grammar: Some("embedded_template".into()),
 218                    hidden: false,
 219                    matcher: LanguageMatcher {
 220                        path_suffixes: vec!["erb".into()],
 221                        first_line_pattern: None,
 222                        ..LanguageMatcher::default()
 223                    },
 224                },
 225            ),
 226            (
 227                "Ruby".into(),
 228                ExtensionIndexLanguageEntry {
 229                    extension: "zed-ruby".into(),
 230                    path: "languages/ruby".into(),
 231                    grammar: Some("ruby".into()),
 232                    hidden: false,
 233                    matcher: LanguageMatcher {
 234                        path_suffixes: vec!["rb".into()],
 235                        first_line_pattern: None,
 236                        ..LanguageMatcher::default()
 237                    },
 238                },
 239            ),
 240        ]
 241        .into_iter()
 242        .collect(),
 243        themes: [
 244            (
 245                "Monokai Dark".into(),
 246                ExtensionIndexThemeEntry {
 247                    extension: "zed-monokai".into(),
 248                    path: "themes/monokai.json".into(),
 249                },
 250            ),
 251            (
 252                "Monokai Light".into(),
 253                ExtensionIndexThemeEntry {
 254                    extension: "zed-monokai".into(),
 255                    path: "themes/monokai.json".into(),
 256                },
 257            ),
 258            (
 259                "Monokai Pro Dark".into(),
 260                ExtensionIndexThemeEntry {
 261                    extension: "zed-monokai".into(),
 262                    path: "themes/monokai-pro.json".into(),
 263                },
 264            ),
 265            (
 266                "Monokai Pro Light".into(),
 267                ExtensionIndexThemeEntry {
 268                    extension: "zed-monokai".into(),
 269                    path: "themes/monokai-pro.json".into(),
 270                },
 271            ),
 272        ]
 273        .into_iter()
 274        .collect(),
 275        icon_themes: BTreeMap::default(),
 276    };
 277
 278    let proxy = Arc::new(ExtensionHostProxy::new());
 279    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
 280    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
 281    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
 282    language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
 283    let node_runtime = NodeRuntime::unavailable();
 284
 285    let store = cx.new(|cx| {
 286        ExtensionStore::new(
 287            PathBuf::from("/the-extension-dir"),
 288            None,
 289            proxy.clone(),
 290            fs.clone(),
 291            http_client.clone(),
 292            http_client.clone(),
 293            None,
 294            node_runtime.clone(),
 295            cx,
 296        )
 297    });
 298
 299    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
 300    store.read_with(cx, |store, _| {
 301        let index = &store.extension_index;
 302        assert_eq!(index.extensions, expected_index.extensions);
 303
 304        for ((actual_key, actual_language), (expected_key, expected_language)) in
 305            index.languages.iter().zip(expected_index.languages.iter())
 306        {
 307            assert_eq!(actual_key, expected_key);
 308            assert_eq!(actual_language.grammar, expected_language.grammar);
 309            assert_eq!(actual_language.matcher, expected_language.matcher);
 310            assert_eq!(actual_language.hidden, expected_language.hidden);
 311        }
 312        assert_eq!(index.themes, expected_index.themes);
 313
 314        assert_eq!(
 315            language_registry.language_names(),
 316            [
 317                LanguageName::new_static("ERB"),
 318                LanguageName::new_static("Plain Text"),
 319                LanguageName::new_static("Ruby"),
 320            ]
 321        );
 322        assert_eq!(
 323            theme_registry.list_names(),
 324            [
 325                "Monokai Dark",
 326                "Monokai Light",
 327                "Monokai Pro Dark",
 328                "Monokai Pro Light",
 329                "One Dark",
 330            ]
 331        );
 332    });
 333
 334    fs.insert_tree(
 335        "/the-extension-dir/installed/zed-gruvbox",
 336        json!({
 337            "extension.json": r#"{
 338                "id": "zed-gruvbox",
 339                "name": "Zed Gruvbox",
 340                "version": "1.0.0",
 341                "themes": {
 342                    "Gruvbox": "themes/gruvbox.json"
 343                }
 344            }"#,
 345            "themes": {
 346                "gruvbox.json": r#"{
 347                    "name": "Gruvbox",
 348                    "author": "Someone Else",
 349                    "themes": [
 350                        {
 351                            "name": "Gruvbox",
 352                            "appearance": "dark",
 353                            "style": {}
 354                        }
 355                    ]
 356                }"#,
 357            }
 358        }),
 359    )
 360    .await;
 361
 362    expected_index.extensions.insert(
 363        "zed-gruvbox".into(),
 364        ExtensionIndexEntry {
 365            manifest: Arc::new(ExtensionManifest {
 366                id: "zed-gruvbox".into(),
 367                name: "Zed Gruvbox".into(),
 368                version: "1.0.0".into(),
 369                schema_version: SchemaVersion::ZERO,
 370                description: None,
 371                authors: vec![],
 372                repository: None,
 373                themes: vec![rel_path_buf("themes/gruvbox.json")],
 374                icon_themes: Vec::new(),
 375                lib: Default::default(),
 376                languages: Default::default(),
 377                grammars: BTreeMap::default(),
 378                language_servers: BTreeMap::default(),
 379                context_servers: BTreeMap::default(),
 380                agent_servers: BTreeMap::default(),
 381                slash_commands: BTreeMap::default(),
 382                snippets: None,
 383                capabilities: Vec::new(),
 384                debug_adapters: Default::default(),
 385                debug_locators: Default::default(),
 386                language_model_providers: BTreeMap::default(),
 387            }),
 388            dev: false,
 389        },
 390    );
 391    expected_index.themes.insert(
 392        "Gruvbox".into(),
 393        ExtensionIndexThemeEntry {
 394            extension: "zed-gruvbox".into(),
 395            path: "themes/gruvbox.json".into(),
 396        },
 397    );
 398
 399    #[allow(clippy::let_underscore_future)]
 400    let _ = store.update(cx, |store, cx| store.reload(None, cx));
 401
 402    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
 403    store.read_with(cx, |store, _| {
 404        let index = &store.extension_index;
 405
 406        for ((actual_key, actual_language), (expected_key, expected_language)) in
 407            index.languages.iter().zip(expected_index.languages.iter())
 408        {
 409            assert_eq!(actual_key, expected_key);
 410            assert_eq!(actual_language.grammar, expected_language.grammar);
 411            assert_eq!(actual_language.matcher, expected_language.matcher);
 412            assert_eq!(actual_language.hidden, expected_language.hidden);
 413        }
 414
 415        assert_eq!(index.extensions, expected_index.extensions);
 416        assert_eq!(index.themes, expected_index.themes);
 417
 418        assert_eq!(
 419            theme_registry.list_names(),
 420            [
 421                "Gruvbox",
 422                "Monokai Dark",
 423                "Monokai Light",
 424                "Monokai Pro Dark",
 425                "Monokai Pro Light",
 426                "One Dark",
 427            ]
 428        );
 429    });
 430
 431    let prev_fs_metadata_call_count = fs.metadata_call_count();
 432    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
 433
 434    // Create new extension store, as if Zed were restarting.
 435    drop(store);
 436    let store = cx.new(|cx| {
 437        ExtensionStore::new(
 438            PathBuf::from("/the-extension-dir"),
 439            None,
 440            proxy,
 441            fs.clone(),
 442            http_client.clone(),
 443            http_client.clone(),
 444            None,
 445            node_runtime.clone(),
 446            cx,
 447        )
 448    });
 449
 450    cx.executor().run_until_parked();
 451    store.read_with(cx, |store, _| {
 452        assert_eq!(store.extension_index.extensions, expected_index.extensions);
 453        assert_eq!(store.extension_index.themes, expected_index.themes);
 454        assert_eq!(
 455            store.extension_index.icon_themes,
 456            expected_index.icon_themes
 457        );
 458
 459        for ((actual_key, actual_language), (expected_key, expected_language)) in store
 460            .extension_index
 461            .languages
 462            .iter()
 463            .zip(expected_index.languages.iter())
 464        {
 465            assert_eq!(actual_key, expected_key);
 466            assert_eq!(actual_language.grammar, expected_language.grammar);
 467            assert_eq!(actual_language.matcher, expected_language.matcher);
 468            assert_eq!(actual_language.hidden, expected_language.hidden);
 469        }
 470
 471        assert_eq!(
 472            language_registry.language_names(),
 473            [
 474                LanguageName::new_static("ERB"),
 475                LanguageName::new_static("Plain Text"),
 476                LanguageName::new_static("Ruby"),
 477            ]
 478        );
 479        assert_eq!(
 480            language_registry.grammar_names(),
 481            ["embedded_template".into(), "ruby".into()]
 482        );
 483        assert_eq!(
 484            theme_registry.list_names(),
 485            [
 486                "Gruvbox",
 487                "Monokai Dark",
 488                "Monokai Light",
 489                "Monokai Pro Dark",
 490                "Monokai Pro Light",
 491                "One Dark",
 492            ]
 493        );
 494
 495        // The on-disk manifest limits the number of FS calls that need to be made
 496        // on startup.
 497        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
 498        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
 499    });
 500
 501    store.update(cx, |store, cx| {
 502        store
 503            .uninstall_extension("zed-ruby".into(), cx)
 504            .detach_and_log_err(cx);
 505    });
 506
 507    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
 508    expected_index.extensions.remove("zed-ruby");
 509    expected_index.languages.remove("Ruby");
 510    expected_index.languages.remove("ERB");
 511
 512    store.read_with(cx, |store, _| {
 513        assert_eq!(store.extension_index.extensions, expected_index.extensions);
 514        assert_eq!(store.extension_index.themes, expected_index.themes);
 515        assert_eq!(
 516            store.extension_index.icon_themes,
 517            expected_index.icon_themes
 518        );
 519
 520        for ((actual_key, actual_language), (expected_key, expected_language)) in store
 521            .extension_index
 522            .languages
 523            .iter()
 524            .zip(expected_index.languages.iter())
 525        {
 526            assert_eq!(actual_key, expected_key);
 527            assert_eq!(actual_language.grammar, expected_language.grammar);
 528            assert_eq!(actual_language.matcher, expected_language.matcher);
 529            assert_eq!(actual_language.hidden, expected_language.hidden);
 530        }
 531
 532        assert_eq!(
 533            language_registry.language_names(),
 534            [LanguageName::new_static("Plain Text")]
 535        );
 536        assert_eq!(language_registry.grammar_names(), []);
 537    });
 538}
 539
 540#[gpui::test]
 541async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
 542    init_test(cx);
 543    cx.executor().allow_parking();
 544
 545    let executor = cx.executor();
 546    async fn await_or_timeout<T>(
 547        executor: &BackgroundExecutor,
 548        what: &'static str,
 549        seconds: u64,
 550        future: impl std::future::Future<Output = T>,
 551    ) -> T {
 552        let timeout = executor.timer(std::time::Duration::from_secs(seconds));
 553
 554        futures::select! {
 555            output = future.fuse() => output,
 556            _ = futures::FutureExt::fuse(timeout) => panic!(
 557            "[test_extension_store_with_test_extension] timed out after {seconds}s while {what}"
 558        )
 559        }
 560    }
 561
 562    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
 563        .parent()
 564        .unwrap()
 565        .parent()
 566        .unwrap();
 567    let cache_dir = root_dir.join("target");
 568    let test_extension_id = "test-extension";
 569    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
 570
 571    let fs = Arc::new(RealFs::new(None, cx.executor()));
 572    let extensions_tree = TempTree::new(json!({
 573        "installed": {},
 574        "work": {}
 575    }));
 576    let project_dir = TempTree::new(json!({
 577        "test.gleam": ""
 578    }));
 579
 580    let extensions_dir = extensions_tree.path().canonicalize().unwrap();
 581    let project_dir = project_dir.path().canonicalize().unwrap();
 582
 583    let project = await_or_timeout(
 584        &executor,
 585        "awaiting Project::test",
 586        5,
 587        Project::test(fs.clone(), [project_dir.as_path()], cx),
 588    )
 589    .await;
 590
 591    let proxy = Arc::new(ExtensionHostProxy::new());
 592    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
 593    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
 594    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
 595    language_extension::init(
 596        LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())),
 597        proxy.clone(),
 598        language_registry.clone(),
 599    );
 600    let node_runtime = NodeRuntime::unavailable();
 601
 602    let mut status_updates = language_registry.language_server_binary_statuses();
 603
 604    struct FakeLanguageServerVersion {
 605        version: String,
 606        binary_contents: String,
 607        http_request_count: usize,
 608    }
 609
 610    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
 611        version: "v1.2.3".into(),
 612        binary_contents: "the-binary-contents".into(),
 613        http_request_count: 0,
 614    }));
 615
 616    let extension_client = FakeHttpClient::create({
 617        let language_server_version = language_server_version.clone();
 618        move |request| {
 619            let language_server_version = language_server_version.clone();
 620            async move {
 621                let version = language_server_version.lock().version.clone();
 622                let binary_contents = language_server_version.lock().binary_contents.clone();
 623
 624                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
 625                let asset_download_uri =
 626                    format!("https://fake-download.example.com/gleam-{version}");
 627
 628                let uri = request.uri().to_string();
 629                if uri == github_releases_uri {
 630                    language_server_version.lock().http_request_count += 1;
 631                    Ok(Response::new(
 632                        json!([
 633                            {
 634                                "tag_name": version,
 635                                "prerelease": false,
 636                                "tarball_url": "",
 637                                "zipball_url": "",
 638                                "assets": [
 639                                    {
 640                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
 641                                        "browser_download_url": asset_download_uri
 642                                    },
 643                                    {
 644                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
 645                                        "browser_download_url": asset_download_uri
 646                                    },
 647                                    {
 648                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
 649                                        "browser_download_url": asset_download_uri
 650                                    },
 651                                    {
 652                                        "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"),
 653                                        "browser_download_url": asset_download_uri
 654                                    }
 655                                ]
 656                            }
 657                        ])
 658                        .to_string()
 659                        .into(),
 660                    ))
 661                } else if uri == asset_download_uri {
 662                    language_server_version.lock().http_request_count += 1;
 663                    let mut bytes = Vec::<u8>::new();
 664                    let mut archive = async_tar::Builder::new(&mut bytes);
 665                    let mut header = async_tar::Header::new_gnu();
 666                    header.set_size(binary_contents.len() as u64);
 667                    archive
 668                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
 669                        .await
 670                        .unwrap();
 671                    archive.into_inner().await.unwrap();
 672                    let mut gzipped_bytes = Vec::new();
 673                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
 674                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
 675                    Ok(Response::new(gzipped_bytes.into()))
 676                } else {
 677                    Ok(Response::builder().status(404).body("not found".into())?)
 678                }
 679            }
 680        }
 681    });
 682    let user_agent = cx.update(|cx| {
 683        format!(
 684            "Zed/{} ({}; {})",
 685            AppVersion::global(cx),
 686            std::env::consts::OS,
 687            std::env::consts::ARCH
 688        )
 689    });
 690    let builder_client =
 691        Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
 692
 693    let extension_store = cx.new(|cx| {
 694        ExtensionStore::new(
 695            extensions_dir.clone(),
 696            Some(cache_dir),
 697            proxy,
 698            fs.clone(),
 699            extension_client.clone(),
 700            builder_client,
 701            None,
 702            node_runtime,
 703            cx,
 704        )
 705    });
 706
 707    // Ensure that debounces fire.
 708    let mut events = cx.events(&extension_store);
 709    let executor = cx.executor();
 710    let _task = cx.executor().spawn(async move {
 711        while let Some(event) = events.next().await {
 712            if let Event::StartedReloading = event {
 713                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
 714            }
 715        }
 716    });
 717
 718    extension_store.update(cx, |_, cx| {
 719        cx.subscribe(&extension_store, |_, _, event, _| {
 720            if matches!(event, Event::ExtensionFailedToLoad(_)) {
 721                panic!("extension failed to load");
 722            }
 723        })
 724        .detach();
 725    });
 726
 727    let mut extension_events = cx.events(&cx.update(|cx| {
 728        extension::ExtensionEvents::try_global(cx)
 729            .expect("ExtensionEvents should be initialized in tests")
 730    }));
 731
 732    let executor = cx.executor();
 733    await_or_timeout(
 734        &executor,
 735        "awaiting install_dev_extension",
 736        60,
 737        extension_store.update(cx, |store, cx| {
 738            store.install_dev_extension(test_extension_dir.clone(), cx)
 739        }),
 740    )
 741    .await
 742    .unwrap();
 743
 744    await_or_timeout(
 745        &executor,
 746        "awaiting ExtensionsInstalledChanged",
 747        10,
 748        async {
 749            while let Some(event) = extension_events.next().await {
 750                if matches!(event, extension::Event::ExtensionsInstalledChanged) {
 751                    return;
 752                }
 753            }
 754
 755            panic!(
 756                "[test_extension_store_with_test_extension] extension event stream ended before ExtensionsInstalledChanged"
 757            );
 758        },
 759    )
 760    .await;
 761
 762    let mut fake_servers = language_registry.register_fake_lsp_server(
 763        LanguageServerName("gleam".into()),
 764        lsp::ServerCapabilities {
 765            completion_provider: Some(Default::default()),
 766            ..Default::default()
 767        },
 768        None,
 769    );
 770    cx.executor().run_until_parked();
 771
 772    let mut project_events = cx.events(&project);
 773    let buffer_path = project_dir.join("test.gleam");
 774    let (buffer, _handle) = await_or_timeout(
 775        &executor,
 776        "awaiting open_local_buffer_with_lsp",
 777        5,
 778        project.update(cx, |project, cx| {
 779            project.open_local_buffer_with_lsp(buffer_path.clone(), cx)
 780        }),
 781    )
 782    .await
 783    .unwrap();
 784    cx.executor().run_until_parked();
 785
 786    let buffer_remote_id = buffer.read_with(cx, |buffer, _cx| buffer.remote_id());
 787
 788    let fake_server = await_or_timeout(
 789        &executor,
 790        "awaiting first fake server spawn",
 791        10,
 792        fake_servers.next(),
 793    )
 794    .await
 795    .unwrap();
 796
 797    let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
 798    let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
 799    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
 800
 801    // check that IO operations in extension work correctly
 802    assert!(work_dir.join("dir-created-with-rel-path").exists());
 803    assert!(work_dir.join("dir-created-with-abs-path").exists());
 804    assert!(work_dir.join("file-created-with-abs-path").exists());
 805    assert!(work_dir.join("file-created-with-rel-path").exists());
 806
 807    assert_eq!(fake_server.binary.path, expected_server_path);
 808    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
 809    assert_eq!(
 810        await_or_timeout(
 811            &executor,
 812            "awaiting fs.load(expected_server_path)",
 813            5,
 814            fs.load(&expected_server_path)
 815        )
 816        .await
 817        .unwrap(),
 818        expected_binary_contents
 819    );
 820    assert_eq!(language_server_version.lock().http_request_count, 2);
 821    assert_eq!(
 822        [
 823            await_or_timeout(
 824                &executor,
 825                "awaiting status_updates #1",
 826                5,
 827                status_updates.next()
 828            )
 829            .await
 830            .unwrap(),
 831            await_or_timeout(
 832                &executor,
 833                "awaiting status_updates #2",
 834                5,
 835                status_updates.next()
 836            )
 837            .await
 838            .unwrap(),
 839            await_or_timeout(
 840                &executor,
 841                "awaiting status_updates #3",
 842                5,
 843                status_updates.next()
 844            )
 845            .await
 846            .unwrap(),
 847            await_or_timeout(
 848                &executor,
 849                "awaiting status_updates #4",
 850                5,
 851                status_updates.next()
 852            )
 853            .await
 854            .unwrap(),
 855        ],
 856        [
 857            (
 858                LanguageServerName::new_static("gleam"),
 859                BinaryStatus::Starting
 860            ),
 861            (
 862                LanguageServerName::new_static("gleam"),
 863                BinaryStatus::CheckingForUpdate
 864            ),
 865            (
 866                LanguageServerName::new_static("gleam"),
 867                BinaryStatus::Downloading
 868            ),
 869            (LanguageServerName::new_static("gleam"), BinaryStatus::None)
 870        ]
 871    );
 872
 873    // The extension creates custom labels for completion items.
 874    fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 875        Ok(Some(lsp::CompletionResponse::Array(vec![
 876            lsp::CompletionItem {
 877                label: "foo".into(),
 878                kind: Some(lsp::CompletionItemKind::FUNCTION),
 879                detail: Some("fn() -> Result(Nil, Error)".into()),
 880                ..Default::default()
 881            },
 882            lsp::CompletionItem {
 883                label: "bar.baz".into(),
 884                kind: Some(lsp::CompletionItemKind::FUNCTION),
 885                detail: Some("fn(List(a)) -> a".into()),
 886                ..Default::default()
 887            },
 888            lsp::CompletionItem {
 889                label: "Quux".into(),
 890                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
 891                detail: Some("fn(String) -> T".into()),
 892                ..Default::default()
 893            },
 894            lsp::CompletionItem {
 895                label: "my_string".into(),
 896                kind: Some(lsp::CompletionItemKind::CONSTANT),
 897                detail: Some("String".into()),
 898                ..Default::default()
 899            },
 900        ])))
 901    });
 902
 903    // `register_fake_lsp_server` can yield a server instance before the client has fully registered
 904    // the buffer with the project LSP plumbing. Wait for the project to observe that registration
 905    // before issuing requests like completion.
 906    await_or_timeout(
 907        &executor,
 908        "awaiting LanguageServerBufferRegistered",
 909        5,
 910        async {
 911            while let Some(event) = project_events.next().await {
 912                if let project::Event::LanguageServerBufferRegistered { buffer_id, .. } = event {
 913                    if buffer_id == buffer_remote_id {
 914                        return;
 915                    }
 916                }
 917            }
 918
 919            panic!(
 920                "[test_extension_store_with_test_extension] project event stream ended before buffer registration for {}",
 921                buffer_path.display()
 922            );
 923        },
 924    )
 925    .await;
 926
 927    let completion_labels = await_or_timeout(
 928        &executor,
 929        "awaiting completions",
 930        5,
 931        project.update(cx, |project, cx| {
 932            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
 933        }),
 934    )
 935    .await
 936    .unwrap()
 937    .into_iter()
 938    .flat_map(|response| response.completions)
 939    .map(|c| c.label.text)
 940    .collect::<Vec<_>>();
 941    assert_eq!(
 942        completion_labels,
 943        [
 944            "foo: fn() -> Result(Nil, Error)".to_string(),
 945            "bar.baz: fn(List(a)) -> a".to_string(),
 946            "Quux: fn(String) -> T".to_string(),
 947            "my_string: String".to_string(),
 948        ]
 949    );
 950
 951    // Simulate a new version of the language server being released
 952    language_server_version.lock().version = "v2.0.0".into();
 953    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
 954    language_server_version.lock().http_request_count = 0;
 955
 956    // Start a new instance of the language server.
 957    project.update(cx, |project, cx| {
 958        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
 959    });
 960    cx.executor().run_until_parked();
 961
 962    // The extension has cached the binary path, and does not attempt
 963    // to reinstall it.
 964    let fake_server = await_or_timeout(
 965        &executor,
 966        "awaiting second fake server spawn",
 967        5,
 968        fake_servers.next(),
 969    )
 970    .await
 971    .unwrap();
 972    assert_eq!(fake_server.binary.path, expected_server_path);
 973    assert_eq!(
 974        await_or_timeout(
 975            &executor,
 976            "awaiting fs.load(expected_server_path) after restart",
 977            5,
 978            fs.load(&expected_server_path)
 979        )
 980        .await
 981        .unwrap(),
 982        expected_binary_contents
 983    );
 984    assert_eq!(language_server_version.lock().http_request_count, 0);
 985
 986    // Reload the extension, clearing its cache.
 987    // Start a new instance of the language server.
 988    await_or_timeout(
 989        &executor,
 990        "awaiting extension_store.reload(test-extension)",
 991        5,
 992        extension_store.update(cx, |store, cx| {
 993            store.reload(Some("test-extension".into()), cx)
 994        }),
 995    )
 996    .await;
 997    cx.executor().run_until_parked();
 998    project.update(cx, |project, cx| {
 999        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
1000    });
1001
1002    // The extension re-fetches the latest version of the language server.
1003    let fake_server = await_or_timeout(
1004        &executor,
1005        "awaiting third fake server spawn",
1006        5,
1007        fake_servers.next(),
1008    )
1009    .await
1010    .unwrap();
1011    let new_expected_server_path =
1012        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
1013    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
1014    assert_eq!(fake_server.binary.path, new_expected_server_path);
1015    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
1016    assert_eq!(
1017        await_or_timeout(
1018            &executor,
1019            "awaiting fs.load(new_expected_server_path)",
1020            5,
1021            fs.load(&new_expected_server_path)
1022        )
1023        .await
1024        .unwrap(),
1025        expected_binary_contents
1026    );
1027
1028    // The old language server directory has been cleaned up.
1029    assert!(
1030        await_or_timeout(
1031            &executor,
1032            "awaiting fs.metadata(expected_server_path)",
1033            5,
1034            fs.metadata(&expected_server_path)
1035        )
1036        .await
1037        .unwrap()
1038        .is_none()
1039    );
1040}
1041
1042fn init_test(cx: &mut TestAppContext) {
1043    cx.update(|cx| {
1044        let store = SettingsStore::test(cx);
1045        cx.set_global(store);
1046        release_channel::init(semver::Version::new(0, 0, 0), cx);
1047        extension::init(cx);
1048        theme_settings::init(theme::LoadThemes::JustBase, cx);
1049        gpui_tokio::init(cx);
1050    });
1051}