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