tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Adopt `getVariants` API
6 changed files, 281 additions(+), 126 deletions(-)
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) {
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) {
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))
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)
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 }
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)