Scope Tailwind in JS/TS to within string

Julia and Kirill Bulatov created

In some situations outside JSX elements Tailwind will never
respond to a completion request, holding up the tsserver completions.

Only submit the request to Tailwind when we wouldn't get tsserver
completions anyway and don't submit to Tailwind when we know we won't
get Tailwind completions

Co-Authored-By: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/language/src/language.rs                 | 31 ++++++++++++++++++
crates/project/src/project.rs                   | 13 +++++++
crates/zed/src/languages/javascript/config.toml |  2 +
crates/zed/src/languages/tsx/config.toml        |  2 +
4 files changed, 46 insertions(+), 2 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -345,6 +345,8 @@ pub struct LanguageConfig {
     #[serde(default)]
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
     #[serde(default)]
+    pub scope_opt_in_language_servers: Vec<String>,
+    #[serde(default)]
     pub overrides: HashMap<String, LanguageConfigOverride>,
     #[serde(default)]
     pub word_characters: HashSet<char>,
@@ -377,6 +379,8 @@ pub struct LanguageConfigOverride {
     pub disabled_bracket_ixs: Vec<u16>,
     #[serde(default)]
     pub word_characters: Override<HashSet<char>>,
+    #[serde(default)]
+    pub opt_into_language_servers: Vec<String>,
 }
 
 #[derive(Clone, Deserialize, Debug)]
@@ -415,6 +419,7 @@ impl Default for LanguageConfig {
             autoclose_before: Default::default(),
             line_comment: Default::default(),
             block_comment: Default::default(),
+            scope_opt_in_language_servers: Default::default(),
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
             word_characters: Default::default(),
@@ -1352,13 +1357,23 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_override_query(mut self, source: &str) -> Result<Self> {
+    pub fn with_override_query(mut self, source: &str) -> anyhow::Result<Self> {
         let query = Query::new(self.grammar_mut().ts_language, source)?;
 
         let mut override_configs_by_id = HashMap::default();
         for (ix, name) in query.capture_names().iter().enumerate() {
             if !name.starts_with('_') {
                 let value = self.config.overrides.remove(name).unwrap_or_default();
+                for server_name in &value.opt_into_language_servers {
+                    if !self
+                        .config
+                        .scope_opt_in_language_servers
+                        .contains(server_name)
+                    {
+                        util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
+                    }
+                }
+
                 override_configs_by_id.insert(ix as u32, (name.clone(), value));
             }
         }
@@ -1597,6 +1612,20 @@ impl LanguageScope {
         c.is_whitespace() || self.language.config.autoclose_before.contains(c)
     }
 
+    pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
+        let config = &self.language.config;
+        let opt_in_servers = &config.scope_opt_in_language_servers;
+        if opt_in_servers.iter().any(|o| *o == *name.0) {
+            if let Some(over) = self.config_override() {
+                over.opt_into_language_servers.iter().any(|o| *o == *name.0)
+            } else {
+                false
+            }
+        } else {
+            true
+        }
+    }
+
     fn config_override(&self) -> Option<&LanguageConfigOverride> {
         let id = self.override_id?;
         let grammar = self.language.grammar.as_ref()?;

crates/project/src/project.rs 🔗

@@ -4429,16 +4429,27 @@ impl Project {
         self.request_primary_lsp(buffer.clone(), GetHover { position }, cx)
     }
 
-    pub fn completions<T: ToPointUtf16>(
+    pub fn completions<T: ToOffset + ToPointUtf16>(
         &self,
         buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
+        let snapshot = buffer.read(cx).snapshot();
+        let offset = position.to_offset(&snapshot);
         let position = position.to_point_utf16(buffer.read(cx));
+
+        let scope = snapshot.language_scope_at(offset);
+
         let server_ids: Vec<_> = self
             .language_servers_for_buffer(buffer.read(cx), cx)
             .filter(|(_, server)| server.capabilities().completion_provider.is_some())
+            .filter(|(adapter, _)| {
+                scope
+                    .as_ref()
+                    .map(|scope| scope.language_allowed(&adapter.name))
+                    .unwrap_or(true)
+            })
             .map(|(_, server)| server.server_id())
             .collect();
 

crates/zed/src/languages/javascript/config.toml 🔗

@@ -13,6 +13,7 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
 
 [overrides.element]
 line_comment = { remove = true }
@@ -20,3 +21,4 @@ block_comment = ["{/* ", " */}"]
 
 [overrides.string]
 word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed/src/languages/tsx/config.toml 🔗

@@ -12,6 +12,7 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
 
 [overrides.element]
 line_comment = { remove = true }
@@ -19,3 +20,4 @@ block_comment = ["{/* ", " */}"]
 
 [overrides.string]
 word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]