Allow controlling Tailwind via the `language_servers` setting (#11012)

Marshall Bowers created

This PR adds the ability for the Tailwind language server
(`tailwindcss-language-server`) to be controlled by the
`language_servers` setting.

Now in your settings you can indicate that the Tailwind language server
should be used for a given language, even if that language does not have
the Tailwind language server registered for it already:

```json
{
  "languages": {
    "My Language": {
      "language_servers": ["tailwindcss-language-server", "..."]
    }
  }
}
```

Release Notes:

- N/A

Change summary

crates/language/src/language_registry.rs | 35 +++++++++++++++++++++
crates/language/src/language_settings.rs | 19 +++++++++++
crates/languages/src/lib.rs              | 43 ++++++++++++++++++-------
crates/project/src/project.rs            | 34 ++++++++++++++++---
4 files changed, 113 insertions(+), 18 deletions(-)

Detailed changes

crates/language/src/language_registry.rs 🔗

@@ -46,6 +46,8 @@ struct LanguageRegistryState {
     available_languages: Vec<AvailableLanguage>,
     grammars: HashMap<Arc<str>, AvailableGrammar>,
     lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
+    available_lsp_adapters:
+        HashMap<LanguageServerName, Arc<dyn Fn() -> Arc<CachedLspAdapter> + 'static + Send + Sync>>,
     loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
     subscription: (watch::Sender<()>, watch::Receiver<()>),
     theme: Option<Arc<Theme>>,
@@ -153,6 +155,7 @@ impl LanguageRegistry {
                 language_settings: Default::default(),
                 loading_languages: Default::default(),
                 lsp_adapters: Default::default(),
+                available_lsp_adapters: HashMap::default(),
                 subscription: watch::channel(),
                 theme: Default::default(),
                 version: 0,
@@ -213,6 +216,38 @@ impl LanguageRegistry {
         )
     }
 
+    /// Registers an available language server adapter.
+    ///
+    /// The language server is registered under the language server name, but
+    /// not bound to a particular language.
+    ///
+    /// When a language wants to load this particular language server, it will
+    /// invoke the `load` function.
+    pub fn register_available_lsp_adapter(
+        &self,
+        name: LanguageServerName,
+        load: impl Fn() -> Arc<dyn LspAdapter> + 'static + Send + Sync,
+    ) {
+        self.state.write().available_lsp_adapters.insert(
+            name,
+            Arc::new(move || {
+                let lsp_adapter = load();
+                CachedLspAdapter::new(lsp_adapter, true)
+            }),
+        );
+    }
+
+    /// Loads the language server adapter for the language server with the given name.
+    pub fn load_available_lsp_adapter(
+        &self,
+        name: &LanguageServerName,
+    ) -> Option<Arc<CachedLspAdapter>> {
+        let state = self.state.read();
+        let load_lsp_adapter = state.available_lsp_adapters.get(name)?;
+
+        Some(load_lsp_adapter())
+    }
+
     pub fn register_lsp_adapter(&self, language_name: Arc<str>, adapter: Arc<dyn LspAdapter>) {
         self.state
             .write()

crates/language/src/language_settings.rs 🔗

@@ -789,5 +789,24 @@ mod tests {
             ),
             language_server_names(&["deno", "eslint", "tailwind"])
         );
+
+        // Adding a language server not in the list of available languages servers adds it to the list.
+        assert_eq!(
+            LanguageSettings::resolve_language_servers(
+                &[
+                    "my-cool-language-server".into(),
+                    LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()
+                ],
+                &available_language_servers
+            ),
+            language_server_names(&[
+                "my-cool-language-server",
+                "typescript-language-server",
+                "biome",
+                "deno",
+                "eslint",
+                "tailwind",
+            ])
+        );
     }
 }

crates/languages/src/lib.rs 🔗

@@ -107,10 +107,7 @@ pub fn init(
     language!("cpp", vec![Arc::new(c::CLspAdapter)]);
     language!(
         "css",
-        vec![
-            Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
-            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-        ]
+        vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),]
     );
     language!("go", vec![Arc::new(go::GoLspAdapter)]);
     language!("gomod");
@@ -160,13 +157,7 @@ pub fn init(
         ))]
     );
     language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]);
-    language!(
-        "erb",
-        vec![
-            Arc::new(ruby::RubyLanguageServer),
-            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
-        ]
-    );
+    language!("erb", vec![Arc::new(ruby::RubyLanguageServer),]);
     language!("regex");
     language!(
         "yaml",
@@ -174,14 +165,42 @@ pub fn init(
     );
     language!("proto");
 
+    // Register Tailwind globally as an available language server.
+    //
+    // This will allow users to add Tailwind support for a given language via
+    // the `language_servers` setting:
+    //
+    // ```json
+    // {
+    //   "languages": {
+    //     "My Language": {
+    //       "language_servers": ["tailwindcss-language-server", "..."]
+    //     }
+    //   }
+    // }
+    // ```
+    languages.register_available_lsp_adapter(
+        LanguageServerName("tailwindcss-language-server".into()),
+        {
+            let node_runtime = node_runtime.clone();
+            move || Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone()))
+        },
+    );
+
+    // Register Tailwind for the existing languages that should have it by default.
+    //
+    // This can be driven by the `language_servers` setting once we have a way for
+    // extensions to provide their own default value for that setting.
     let tailwind_languages = [
         "Astro",
+        "CSS",
+        "ERB",
         "HEEX",
         "HTML",
+        "JavaScript",
         "PHP",
         "Svelte",
         "TSX",
-        "JavaScript",
         "Vue.js",
     ];
 

crates/project/src/project.rs 🔗

@@ -3066,12 +3066,34 @@ impl Project {
             .map(|lsp_adapter| lsp_adapter.name.clone())
             .collect::<Vec<_>>();
 
-        let enabled_language_servers =
+        let desired_language_servers =
             settings.customized_language_servers(&available_language_servers);
-        let enabled_lsp_adapters = available_lsp_adapters
-            .into_iter()
-            .filter(|adapter| enabled_language_servers.contains(&adapter.name))
-            .collect::<Vec<_>>();
+
+        let mut enabled_lsp_adapters: Vec<Arc<CachedLspAdapter>> = Vec::new();
+        for desired_language_server in desired_language_servers {
+            if let Some(adapter) = available_lsp_adapters
+                .iter()
+                .find(|adapter| adapter.name == desired_language_server)
+            {
+                enabled_lsp_adapters.push(adapter.clone());
+                continue;
+            }
+
+            if let Some(adapter) = self
+                .languages
+                .load_available_lsp_adapter(&desired_language_server)
+            {
+                self.languages()
+                    .register_lsp_adapter(language.name(), adapter.adapter.clone());
+                enabled_lsp_adapters.push(adapter);
+                continue;
+            }
+
+            log::warn!(
+                "no language server found matching '{}'",
+                desired_language_server.0
+            );
+        }
 
         log::info!(
             "starting language servers for {language}: {adapters}",
@@ -3083,7 +3105,7 @@ impl Project {
         );
 
         for adapter in enabled_lsp_adapters {
-            self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
+            self.start_language_server(worktree, adapter, language.clone(), cx);
         }
     }