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 executor = cx.executor();
 725    await_or_timeout(
 726        &executor,
 727        "awaiting install_dev_extension",
 728        60,
 729        extension_store.update(cx, |store, cx| {
 730            store.install_dev_extension(test_extension_dir.clone(), cx)
 731        }),
 732    )
 733    .await
 734    .unwrap();
 735
 736    let mut fake_servers = language_registry.register_fake_lsp_server(
 737        LanguageServerName("gleam".into()),
 738        lsp::ServerCapabilities {
 739            completion_provider: Some(Default::default()),
 740            ..Default::default()
 741        },
 742        None,
 743    );
 744    cx.executor().run_until_parked();
 745
 746    let (buffer, _handle) = await_or_timeout(
 747        &executor,
 748        "awaiting open_local_buffer_with_lsp",
 749        5,
 750        project.update(cx, |project, cx| {
 751            project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
 752        }),
 753    )
 754    .await
 755    .unwrap();
 756    cx.executor().run_until_parked();
 757
 758    let fake_server = await_or_timeout(
 759        &executor,
 760        "awaiting first fake server spawn",
 761        10,
 762        fake_servers.next(),
 763    )
 764    .await
 765    .unwrap();
 766
 767    let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
 768    let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
 769    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
 770
 771    // check that IO operations in extension work correctly
 772    assert!(work_dir.join("dir-created-with-rel-path").exists());
 773    assert!(work_dir.join("dir-created-with-abs-path").exists());
 774    assert!(work_dir.join("file-created-with-abs-path").exists());
 775    assert!(work_dir.join("file-created-with-rel-path").exists());
 776
 777    assert_eq!(fake_server.binary.path, expected_server_path);
 778    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
 779    assert_eq!(
 780        await_or_timeout(
 781            &executor,
 782            "awaiting fs.load(expected_server_path)",
 783            5,
 784            fs.load(&expected_server_path)
 785        )
 786        .await
 787        .unwrap(),
 788        expected_binary_contents
 789    );
 790    assert_eq!(language_server_version.lock().http_request_count, 2);
 791    assert_eq!(
 792        [
 793            await_or_timeout(
 794                &executor,
 795                "awaiting status_updates #1",
 796                5,
 797                status_updates.next()
 798            )
 799            .await
 800            .unwrap(),
 801            await_or_timeout(
 802                &executor,
 803                "awaiting status_updates #2",
 804                5,
 805                status_updates.next()
 806            )
 807            .await
 808            .unwrap(),
 809            await_or_timeout(
 810                &executor,
 811                "awaiting status_updates #3",
 812                5,
 813                status_updates.next()
 814            )
 815            .await
 816            .unwrap(),
 817            await_or_timeout(
 818                &executor,
 819                "awaiting status_updates #4",
 820                5,
 821                status_updates.next()
 822            )
 823            .await
 824            .unwrap(),
 825        ],
 826        [
 827            (
 828                LanguageServerName::new_static("gleam"),
 829                BinaryStatus::Starting
 830            ),
 831            (
 832                LanguageServerName::new_static("gleam"),
 833                BinaryStatus::CheckingForUpdate
 834            ),
 835            (
 836                LanguageServerName::new_static("gleam"),
 837                BinaryStatus::Downloading
 838            ),
 839            (LanguageServerName::new_static("gleam"), BinaryStatus::None)
 840        ]
 841    );
 842
 843    // The extension creates custom labels for completion items.
 844    fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 845        Ok(Some(lsp::CompletionResponse::Array(vec![
 846            lsp::CompletionItem {
 847                label: "foo".into(),
 848                kind: Some(lsp::CompletionItemKind::FUNCTION),
 849                detail: Some("fn() -> Result(Nil, Error)".into()),
 850                ..Default::default()
 851            },
 852            lsp::CompletionItem {
 853                label: "bar.baz".into(),
 854                kind: Some(lsp::CompletionItemKind::FUNCTION),
 855                detail: Some("fn(List(a)) -> a".into()),
 856                ..Default::default()
 857            },
 858            lsp::CompletionItem {
 859                label: "Quux".into(),
 860                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
 861                detail: Some("fn(String) -> T".into()),
 862                ..Default::default()
 863            },
 864            lsp::CompletionItem {
 865                label: "my_string".into(),
 866                kind: Some(lsp::CompletionItemKind::CONSTANT),
 867                detail: Some("String".into()),
 868                ..Default::default()
 869            },
 870        ])))
 871    });
 872
 873    // `register_fake_lsp_server` can yield a server instance before the client has finished the LSP
 874    // initialization handshake. Wait until we observe the client's `initialized` notification before
 875    // issuing requests like completion.
 876    await_or_timeout(
 877        &executor,
 878        "awaiting LSP Initialized notification",
 879        5,
 880        async {
 881            fake_server
 882                .clone()
 883                .try_receive_notification::<lsp::notification::Initialized>()
 884                .await;
 885        },
 886    )
 887    .await;
 888
 889    let completion_labels = await_or_timeout(
 890        &executor,
 891        "awaiting completions",
 892        5,
 893        project.update(cx, |project, cx| {
 894            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
 895        }),
 896    )
 897    .await
 898    .unwrap()
 899    .into_iter()
 900    .flat_map(|response| response.completions)
 901    .map(|c| c.label.text)
 902    .collect::<Vec<_>>();
 903    assert_eq!(
 904        completion_labels,
 905        [
 906            "foo: fn() -> Result(Nil, Error)".to_string(),
 907            "bar.baz: fn(List(a)) -> a".to_string(),
 908            "Quux: fn(String) -> T".to_string(),
 909            "my_string: String".to_string(),
 910        ]
 911    );
 912
 913    // Simulate a new version of the language server being released
 914    language_server_version.lock().version = "v2.0.0".into();
 915    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
 916    language_server_version.lock().http_request_count = 0;
 917
 918    // Start a new instance of the language server.
 919    project.update(cx, |project, cx| {
 920        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
 921    });
 922    cx.executor().run_until_parked();
 923
 924    // The extension has cached the binary path, and does not attempt
 925    // to reinstall it.
 926    let fake_server = await_or_timeout(
 927        &executor,
 928        "awaiting second fake server spawn",
 929        5,
 930        fake_servers.next(),
 931    )
 932    .await
 933    .unwrap();
 934    assert_eq!(fake_server.binary.path, expected_server_path);
 935    assert_eq!(
 936        await_or_timeout(
 937            &executor,
 938            "awaiting fs.load(expected_server_path) after restart",
 939            5,
 940            fs.load(&expected_server_path)
 941        )
 942        .await
 943        .unwrap(),
 944        expected_binary_contents
 945    );
 946    assert_eq!(language_server_version.lock().http_request_count, 0);
 947
 948    // Reload the extension, clearing its cache.
 949    // Start a new instance of the language server.
 950    await_or_timeout(
 951        &executor,
 952        "awaiting extension_store.reload(test-extension)",
 953        5,
 954        extension_store.update(cx, |store, cx| {
 955            store.reload(Some("test-extension".into()), cx)
 956        }),
 957    )
 958    .await;
 959    cx.executor().run_until_parked();
 960    project.update(cx, |project, cx| {
 961        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
 962    });
 963
 964    // The extension re-fetches the latest version of the language server.
 965    let fake_server = await_or_timeout(
 966        &executor,
 967        "awaiting third fake server spawn",
 968        5,
 969        fake_servers.next(),
 970    )
 971    .await
 972    .unwrap();
 973    let new_expected_server_path =
 974        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
 975    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
 976    assert_eq!(fake_server.binary.path, new_expected_server_path);
 977    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
 978    assert_eq!(
 979        await_or_timeout(
 980            &executor,
 981            "awaiting fs.load(new_expected_server_path)",
 982            5,
 983            fs.load(&new_expected_server_path)
 984        )
 985        .await
 986        .unwrap(),
 987        expected_binary_contents
 988    );
 989
 990    // The old language server directory has been cleaned up.
 991    assert!(
 992        await_or_timeout(
 993            &executor,
 994            "awaiting fs.metadata(expected_server_path)",
 995            5,
 996            fs.metadata(&expected_server_path)
 997        )
 998        .await
 999        .unwrap()
1000        .is_none()
1001    );
1002}
1003
1004fn init_test(cx: &mut TestAppContext) {
1005    cx.update(|cx| {
1006        let store = SettingsStore::test(cx);
1007        cx.set_global(store);
1008        release_channel::init(semver::Version::new(0, 0, 0), cx);
1009        extension::init(cx);
1010        theme_settings::init(theme::LoadThemes::JustBase, cx);
1011        gpui_tokio::init(cx);
1012    });
1013}