Add support for using a language server with multiple languages (#10293)

Marshall Bowers and Max created

This PR updates the `extension.toml` to allow specifying multiple
languages for a language server to work with.

The `languages` field takes precedence over `language`. In the future
the `language` field will be removed.

As part of this, the Emmet extension has been extended with support for
PHP and ERB.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>

Change summary

crates/extension/src/extension_manifest.rs | 25 ++++++++++++++++++
crates/extension/src/extension_store.rs    | 32 +++++++++++++----------
extensions/emmet/extension.toml            |  1 
3 files changed, 43 insertions(+), 15 deletions(-)

Detailed changes

crates/extension/src/extension_manifest.rs 🔗

@@ -98,11 +98,34 @@ pub struct GrammarManifestEntry {
 
 #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
 pub struct LanguageServerManifestEntry {
-    pub language: Arc<str>,
+    /// Deprecated in favor of `languages`.
+    #[serde(default)]
+    language: Option<Arc<str>>,
+    /// The list of languages this language server should work with.
+    #[serde(default)]
+    languages: Vec<Arc<str>>,
     #[serde(default)]
     pub language_ids: HashMap<String, String>,
 }
 
+impl LanguageServerManifestEntry {
+    /// Returns the list of languages for the language server.
+    ///
+    /// Prefer this over accessing the `language` or `languages` fields directly,
+    /// as we currently support both.
+    ///
+    /// We can replace this with just field access for the `languages` field once
+    /// we have removed `language`.
+    pub fn languages(&self) -> impl IntoIterator<Item = Arc<str>> + '_ {
+        let language = if self.languages.is_empty() {
+            self.language.clone()
+        } else {
+            None
+        };
+        self.languages.iter().cloned().chain(language)
+    }
+}
+
 impl ExtensionManifest {
     pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
         let extension_name = extension_dir

crates/extension/src/extension_store.rs 🔗

@@ -962,8 +962,10 @@ impl ExtensionStore {
             };
             grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
             for (language_server_name, config) in extension.manifest.language_servers.iter() {
-                self.language_registry
-                    .remove_lsp_adapter(config.language.as_ref(), language_server_name);
+                for language in config.languages() {
+                    self.language_registry
+                        .remove_lsp_adapter(&language, language_server_name);
+                }
             }
         }
 
@@ -1103,18 +1105,20 @@ impl ExtensionStore {
 
                 for (manifest, wasm_extension) in &wasm_extensions {
                     for (language_server_id, language_server_config) in &manifest.language_servers {
-                        this.language_registry.register_lsp_adapter(
-                            language_server_config.language.clone(),
-                            Arc::new(ExtensionLspAdapter {
-                                extension: wasm_extension.clone(),
-                                host: this.wasm_host.clone(),
-                                language_server_id: language_server_id.clone(),
-                                config: wit::LanguageServerConfig {
-                                    name: language_server_id.0.to_string(),
-                                    language_name: language_server_config.language.to_string(),
-                                },
-                            }),
-                        );
+                        for language in language_server_config.languages() {
+                            this.language_registry.register_lsp_adapter(
+                                language.clone(),
+                                Arc::new(ExtensionLspAdapter {
+                                    extension: wasm_extension.clone(),
+                                    host: this.wasm_host.clone(),
+                                    language_server_id: language_server_id.clone(),
+                                    config: wit::LanguageServerConfig {
+                                        name: language_server_id.0.to_string(),
+                                        language_name: language.to_string(),
+                                    },
+                                }),
+                            );
+                        }
                     }
                 }
                 this.wasm_extensions.extend(wasm_extensions);

extensions/emmet/extension.toml 🔗

@@ -9,3 +9,4 @@ repository = "https://github.com/zed-industries/zed"
 [language_servers.emmet-language-server]
 name = "Emmet Language Server"
 language = "HTML"
+languages = ["HTML", "PHP", "ERB"]