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