Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
Adopt `getVariants` API
Brad Cornes <hello@bradley.dev>
2 years ago
6 changed files, 281 additions(+), 126 deletions(-)
M packages/tailwindcss-language-server/src/server.ts -> packages/tailwindcss-language-server/src/server.ts
diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts
index d98ab34cbba69391205ffaf7f6be1f5b49e83546..008c221a7744fb579ca9a8887c75c87a9916755c 100644
--- a/packages/tailwindcss-language-server/src/server.ts
+++ b/packages/tailwindcss-language-server/src/server.ts
@@ -63,6 +63,7 @@   State,
   FeatureFlags,
   Settings,
   ClassNames,
+  Variant,
 } from 'tailwindcss-language-service/src/util/state'
 import {
   provideDiagnostics,
@@ -1181,105 +1182,117 @@ function isAtRule(node: Node): node is AtRule {
   return node.type === 'atrule'
 }
 
-function getVariants(state: State): Record<string, string> {
-  if (state.jit) {
-    function escape(className: string): string {
-      let node = state.modules.postcssSelectorParser.module.className()
-      node.value = className
-      return dlv(node, 'raws.value', node.value)
-    }
+function getVariants(state: State): Array<Variant> {
+  if (state.jitContext?.getVariants) {
+    return state.jitContext.getVariants()
+  }
 
-    let result = {}
+  if (state.jit) {
+    let result: Array<Variant> = []
     // [name, [sort, fn]]
     // [name, [[sort, fn]]]
     Array.from(state.jitContext.variantMap as Map<string, [any, any]>).forEach(
       ([variantName, variantFnOrFns]) => {
-        let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map(
-          ([_sort, fn]) => fn
-        )
+        result.push({
+          name: variantName,
+          values: [],
+          isArbitrary: false,
+          hasDash: true,
+          selectors: () => {
+            function escape(className: string): string {
+              let node = state.modules.postcssSelectorParser.module.className()
+              node.value = className
+              return dlv(node, 'raws.value', node.value)
+            }
 
-        let placeholder = '__variant_placeholder__'
+            let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map(
+              ([_sort, fn]) => fn
+            )
 
-        let root = state.modules.postcss.module.root({
-          nodes: [
-            state.modules.postcss.module.rule({
-              selector: `.${escape(placeholder)}`,
-              nodes: [],
-            }),
-          ],
-        })
+            let placeholder = '__variant_placeholder__'
 
-        let classNameParser = state.modules.postcssSelectorParser.module((selectors) => {
-          return selectors.first.filter(({ type }) => type === 'class').pop().value
-        })
+            let root = state.modules.postcss.module.root({
+              nodes: [
+                state.modules.postcss.module.rule({
+                  selector: `.${escape(placeholder)}`,
+                  nodes: [],
+                }),
+              ],
+            })
 
-        function getClassNameFromSelector(selector) {
-          return classNameParser.transformSync(selector)
-        }
+            let classNameParser = state.modules.postcssSelectorParser.module((selectors) => {
+              return selectors.first.filter(({ type }) => type === 'class').pop().value
+            })
 
-        function modifySelectors(modifierFunction) {
-          root.each((rule) => {
-            if (rule.type !== 'rule') {
-              return
+            function getClassNameFromSelector(selector) {
+              return classNameParser.transformSync(selector)
             }
 
-            rule.selectors = rule.selectors.map((selector) => {
-              return modifierFunction({
-                get className() {
-                  return getClassNameFromSelector(selector)
-                },
-                selector,
+            function modifySelectors(modifierFunction) {
+              root.each((rule) => {
+                if (rule.type !== 'rule') {
+                  return
+                }
+
+                rule.selectors = rule.selectors.map((selector) => {
+                  return modifierFunction({
+                    get className() {
+                      return getClassNameFromSelector(selector)
+                    },
+                    selector,
+                  })
+                })
               })
-            })
-          })
-          return root
-        }
+              return root
+            }
 
-        let definitions = []
+            let definitions = []
 
-        for (let fn of fns) {
-          let definition: string
-          let container = root.clone()
-          let returnValue = fn({
-            container,
-            separator: state.separator,
-            modifySelectors,
-            format: (def: string) => {
-              definition = def.replace(/:merge\(([^)]+)\)/g, '$1')
-            },
-            wrap: (rule: Container) => {
-              if (isAtRule(rule)) {
-                definition = `@${rule.name} ${rule.params}`
-              }
-            },
-          })
+            for (let fn of fns) {
+              let definition: string
+              let container = root.clone()
+              let returnValue = fn({
+                container,
+                separator: state.separator,
+                modifySelectors,
+                format: (def: string) => {
+                  definition = def.replace(/:merge\(([^)]+)\)/g, '$1')
+                },
+                wrap: (rule: Container) => {
+                  if (isAtRule(rule)) {
+                    definition = `@${rule.name} ${rule.params}`
+                  }
+                },
+              })
 
-          if (!definition) {
-            definition = returnValue
-          }
+              if (!definition) {
+                definition = returnValue
+              }
 
-          if (definition) {
-            definitions.push(definition)
-            continue
-          }
+              if (definition) {
+                definitions.push(definition)
+                continue
+              }
 
-          container.walkDecls((decl) => {
-            decl.remove()
-          })
+              container.walkDecls((decl) => {
+                decl.remove()
+              })
 
-          definition = container
-            .toString()
-            .replace(`.${escape(`${variantName}:${placeholder}`)}`, '&')
-            .replace(/(?<!\\)[{}]/g, '')
-            .replace(/\s*\n\s*/g, ' ')
-            .trim()
+              definition = container
+                .toString()
+                .replace(`.${escape(`${variantName}:${placeholder}`)}`, '&')
+                .replace(/(?<!\\)[{}]/g, '')
+                .replace(/\s*\n\s*/g, ' ')
+                .trim()
 
-          if (!definition.includes(placeholder)) {
-            definitions.push(definition)
-          }
-        }
+              if (!definition.includes(placeholder)) {
+                definitions.push(definition)
+              }
+            }
 
-        result[variantName] = definitions.join(', ') || null
+            return definitions
+          },
+        })
       }
     )
 
@@ -1311,7 +1324,13 @@       },
     })
   })
 
-  return variants.reduce((obj, variant) => ({ ...obj, [variant]: null }), {})
+  return variants.map((variant) => ({
+    name: variant,
+    values: [],
+    isArbitrary: false,
+    hasDash: true,
+    selectors: () => [],
+  }))
 }
 
 async function getPlugins(config: any) {
M packages/tailwindcss-language-service/src/completionProvider.ts -> packages/tailwindcss-language-service/src/completionProvider.ts
diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts
index 941554f165372cf461f94d82d5f1a7f4aa9f6c4c..adacd136a9a752e0df6731784e1a5fbf05345fd8 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -1,4 +1,4 @@
-import { Settings, State } from './util/state'
+import { Settings, State, Variant } from './util/state'
 import type {
   CompletionItem,
   CompletionItemKind,
@@ -110,7 +110,6 @@         }
       }
     }
 
-    let allVariants = Object.keys(state.variants)
     let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
 
     replacementRange.start.character += offset
@@ -123,55 +122,109 @@
     let items: CompletionItem[] = []
 
     if (!important) {
-      let shouldSortVariants = !semver.gte(state.version, '2.99.0')
+      let variantOrder = 0
+
+      function variantItem(
+        item: Omit<CompletionItem, 'kind' | 'data' | 'sortText' | 'textEdit'> & {
+          textEdit?: { newText: string; range?: Range }
+        }
+      ): CompletionItem {
+        return {
+          kind: 9,
+          data: 'variant',
+          command:
+            item.insertTextFormat === 2 // Snippet
+              ? undefined
+              : {
+                  title: '',
+                  command: 'editor.action.triggerSuggest',
+                },
+          sortText: '-' + naturalExpand(variantOrder++),
+          ...item,
+          textEdit: {
+            newText: item.label,
+            range: replacementRange,
+            ...item.textEdit,
+          },
+        }
+      }
 
       items.push(
-        ...Object.entries(state.variants)
-          .filter(([variant]) => !existingVariants.includes(variant))
-          .map(([variant, definition], index) => {
-            let resultingVariants = [...existingVariants, variant]
+        ...state.variants.flatMap((variant) => {
+          let items: CompletionItem[] = []
+
+          if (variant.isArbitrary) {
+            items.push(
+              variantItem({
+                label: `${variant.name}${variant.hasDash ? '-' : ''}[]${sep}`,
+                insertTextFormat: 2,
+                textEdit: {
+                  newText: `${variant.name}-[\${1:&}]${sep}\${0}`,
+                },
+                // command: {
+                //   title: '',
+                //   command: 'tailwindCSS.onInsertArbitraryVariantSnippet',
+                //   arguments: [variant.name, replacementRange],
+                // },
+              })
+            )
+          } else if (!existingVariants.includes(variant.name)) {
+            let shouldSortVariants = !semver.gte(state.version, '2.99.0')
+            let resultingVariants = [...existingVariants, variant.name]
 
             if (shouldSortVariants) {
+              let allVariants = state.variants.map(({ name }) => name)
               resultingVariants = resultingVariants.sort(
                 (a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
               )
             }
 
-            return {
-              label: variant + sep,
-              kind: 9,
-              detail: definition,
-              data: 'variant',
-              command: {
-                title: '',
-                command: 'editor.action.triggerSuggest',
-              },
-              sortText: '-' + naturalExpand(index),
-              textEdit: {
-                newText: resultingVariants[resultingVariants.length - 1] + sep,
-                range: replacementRange,
-              },
-              additionalTextEdits:
-                shouldSortVariants && resultingVariants.length > 1
-                  ? [
-                      {
-                        newText:
-                          resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
-                        range: {
-                          start: {
-                            ...classListRange.start,
-                            character: classListRange.end.character - partialClassName.length,
-                          },
-                          end: {
-                            ...replacementRange.start,
-                            character: replacementRange.start.character,
+            items.push(
+              variantItem({
+                label: `${variant.name}${sep}`,
+                detail: variant.selectors().join(', '),
+                textEdit: {
+                  newText: resultingVariants[resultingVariants.length - 1] + sep,
+                },
+                additionalTextEdits:
+                  shouldSortVariants && resultingVariants.length > 1
+                    ? [
+                        {
+                          newText:
+                            resultingVariants.slice(0, resultingVariants.length - 1).join(sep) +
+                            sep,
+                          range: {
+                            start: {
+                              ...classListRange.start,
+                              character: classListRange.end.character - partialClassName.length,
+                            },
+                            end: {
+                              ...replacementRange.start,
+                              character: replacementRange.start.character,
+                            },
                           },
                         },
-                      },
-                    ]
-                  : [],
-            } as CompletionItem
-          })
+                      ]
+                    : [],
+              })
+            )
+          }
+
+          if (variant.values.length) {
+            items.push(
+              ...variant.values
+                .filter((value) => !existingVariants.includes(`${variant.name}-${value}`))
+                .map((value) =>
+                  variantItem({
+                    label: `${variant.name}${variant.hasDash ? '-' : ''}${value}${sep}`,
+                    detail: variant.selectors({ value }).join(', '),
+                  })
+                )
+            )
+          }
+
+          return items
+        })
       )
     }
 
@@ -790,7 +843,12 @@   const parts = match.groups.partial.split(/\s*,\s*/)
 
   if (/\s+/.test(parts[parts.length - 1])) return null
 
-  let possibleVariants = Object.keys(state.variants)
+  let possibleVariants = state.variants.flatMap((variant) => {
+    if (variant.values.length) {
+      return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
+    }
+    return [variant.name]
+  })
   const existingVariants = parts.slice(0, parts.length - 1)
 
   if (state.jit) {
M packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts -> packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts
diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts
index 1b6c997abe4119d48fd73b428837f673c1fd08e1..454e0f59cafae735251e5b97a10af6329a9f8d8c 100644
--- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts
+++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts
@@ -32,7 +32,12 @@     if (!boundaries) return []
     ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range))
   }
 
-  let possibleVariants = Object.keys(state.variants)
+  let possibleVariants = state.variants.flatMap((variant) => {
+    if (variant.values.length) {
+      return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
+    }
+    return [variant.name]
+  })
   if (state.jit) {
     possibleVariants.unshift('responsive')
     possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v))
M packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts -> packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts
diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts
index 10dfe90c7cf4d353d820a43f32ed880f5fc490c8..b1709e12c482168bcb00cd38cc5a581eb24ec0eb 100644
--- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts
+++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts
@@ -5,17 +5,25 @@ export function getVariantsFromClassName(
   state: State,
   className: string
 ): { variants: string[]; offset: number } {
-  let allVariants = Object.keys(state.variants)
-  let parts = splitAtTopLevelOnly(className, state.separator).filter(Boolean)
+  let allVariants = state.variants.flatMap((variant) => {
+    if (variant.values.length) {
+      return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
+    }
+    return [variant.name]
+  })
   let variants = new Set<string>()
   let offset = 0
+  let parts = splitAtTopLevelOnly(className, state.separator)
+  if (parts.length < 2) {
+    return { variants: Array.from(variants), offset }
+  }
+  parts = parts.filter(Boolean)
 
   for (let part of parts) {
     if (
       allVariants.includes(part) ||
       (state.jit &&
-        ((part.includes('[') && part.endsWith(']')) ||
-          (part.includes('<') && part.includes('>'))) &&
+        ((part.includes('[') && part.endsWith(']')) || part.includes('/')) &&
         jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0)
     ) {
       variants.add(part)
M packages/tailwindcss-language-service/src/util/state.ts -> packages/tailwindcss-language-service/src/util/state.ts
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index cb863773db039a83b1afeae41807701a07a88219..4c82bb52f4d20aa0a7ff551700be178b14318179 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -80,6 +80,14 @@   future: string[]
   experimental: string[]
 }
 
+export interface Variant {
+  name: string
+  values: string[]
+  isArbitrary: boolean
+  hasDash: boolean
+  selectors: (params?: { value?: string; label?: string }) => string[]
+}
+
 export interface State {
   enabled: boolean
   configPath?: string
@@ -90,7 +98,7 @@   separator?: string
   dependencies?: string[]
   plugins?: any
   screens?: string[]
-  variants?: Record<string, string | null>
+  variants?: Variant[]
   corePlugins?: string[]
   modules?: {
     tailwindcss?: { version: string; module: any }
M packages/vscode-tailwindcss/src/extension.ts -> packages/vscode-tailwindcss/src/extension.ts
diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts
index 4b2bdfb03c7c526ba31f67193e9c573fdec095d8..d1cb232a15727263b29110a09d58a70b75da31fe 100755
--- a/packages/vscode-tailwindcss/src/extension.ts
+++ b/packages/vscode-tailwindcss/src/extension.ts
@@ -26,6 +26,7 @@   CompletionList,
   ProviderResult,
   SnippetString,
   TextEdit,
+  TextEditorSelectionChangeKind,
 } from 'vscode'
 import {
   LanguageClient,
@@ -148,6 +149,62 @@         outputChannel.show()
       }
     })
   )
+
+  // context.subscriptions.push(
+  //   commands.registerCommand(
+  //     'tailwindCSS.onInsertArbitraryVariantSnippet',
+  //     (
+  //       variantName: string,
+  //       range: {
+  //         start: { line: number; character: number }
+  //         end: { line: number; character: number }
+  //       }
+  //     ) => {
+  //       let listener = Window.onDidChangeTextEditorSelection((event) => {
+  //         if (event.selections.length !== 1) {
+  //           listener.dispose()
+  //           return
+  //         }
+
+  //         let document = event.textEditor.document
+  //         let selection = event.selections[0]
+
+  //         let line = document.lineAt(range.start.line)
+  //         let lineRangeFromCompletion = new Range(
+  //           range.start.line,
+  //           range.start.character,
+  //           line.range.end.line,
+  //           line.range.end.character
+  //         )
+  //         let lineText = document.getText(lineRangeFromCompletion)
+  //         let match = lineText.match(/^(\S+)]:/)
+
+  //         if (!match) {
+  //           listener.dispose()
+  //           return
+  //         }
+
+  //         let arbitraryValueRange = new Range(
+  //           lineRangeFromCompletion.start.translate(0, variantName.length + 2),
+  //           lineRangeFromCompletion.start.translate(0, match[1].length)
+  //         )
+
+  //         if (!arbitraryValueRange.contains(selection)) {
+  //           listener.dispose()
+  //         }
+
+  //         if (
+  //           event.kind === TextEditorSelectionChangeKind.Command &&
+  //           selection.isEmpty &&
+  //           selection.start.isEqual(arbitraryValueRange.end.translate(0, 2))
+  //         ) {
+  //           commands.executeCommand('editor.action.triggerSuggest')
+  //         }
+  //       })
+  //       context.subscriptions.push(listener)
+  //     }
+  //   )
+  // )
 
   let watcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true)