Adopt `getVariants` API

master
Brad Cornes 2022-10-17 18:05:04 +01:00
parent bf57dd14bc
commit f59adbe35b
6 changed files with 286 additions and 131 deletions

View File

@ -63,6 +63,7 @@ import {
FeatureFlags, FeatureFlags,
Settings, Settings,
ClassNames, ClassNames,
Variant,
} from 'tailwindcss-language-service/src/util/state' } from 'tailwindcss-language-service/src/util/state'
import { import {
provideDiagnostics, provideDiagnostics,
@ -1181,105 +1182,117 @@ function isAtRule(node: Node): node is AtRule {
return node.type === 'atrule' return node.type === 'atrule'
} }
function getVariants(state: State): Record<string, string> { function getVariants(state: State): Array<Variant> {
if (state.jit) { if (state.jitContext?.getVariants) {
function escape(className: string): string { return state.jitContext.getVariants()
let node = state.modules.postcssSelectorParser.module.className() }
node.value = className
return dlv(node, 'raws.value', node.value)
}
let result = {} if (state.jit) {
let result: Array<Variant> = []
// [name, [sort, fn]] // [name, [sort, fn]]
// [name, [[sort, fn]]] // [name, [[sort, fn]]]
Array.from(state.jitContext.variantMap as Map<string, [any, any]>).forEach( Array.from(state.jitContext.variantMap as Map<string, [any, any]>).forEach(
([variantName, variantFnOrFns]) => { ([variantName, variantFnOrFns]) => {
let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map( result.push({
([_sort, fn]) => fn name: variantName,
) values: [],
isArbitrary: false,
let placeholder = '__variant_placeholder__' hasDash: true,
selectors: () => {
let root = state.modules.postcss.module.root({ function escape(className: string): string {
nodes: [ let node = state.modules.postcssSelectorParser.module.className()
state.modules.postcss.module.rule({ node.value = className
selector: `.${escape(placeholder)}`, return dlv(node, 'raws.value', node.value)
nodes: [],
}),
],
})
let classNameParser = state.modules.postcssSelectorParser.module((selectors) => {
return selectors.first.filter(({ type }) => type === 'class').pop().value
})
function getClassNameFromSelector(selector) {
return classNameParser.transformSync(selector)
}
function modifySelectors(modifierFunction) {
root.each((rule) => {
if (rule.type !== 'rule') {
return
} }
rule.selectors = rule.selectors.map((selector) => { let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map(
return modifierFunction({ ([_sort, fn]) => fn
get className() { )
return getClassNameFromSelector(selector)
}, let placeholder = '__variant_placeholder__'
selector,
}) let root = state.modules.postcss.module.root({
nodes: [
state.modules.postcss.module.rule({
selector: `.${escape(placeholder)}`,
nodes: [],
}),
],
}) })
})
return root
}
let definitions = [] let classNameParser = state.modules.postcssSelectorParser.module((selectors) => {
return selectors.first.filter(({ type }) => type === 'class').pop().value
})
for (let fn of fns) { function getClassNameFromSelector(selector) {
let definition: string return classNameParser.transformSync(selector)
let container = root.clone() }
let returnValue = fn({
container, function modifySelectors(modifierFunction) {
separator: state.separator, root.each((rule) => {
modifySelectors, if (rule.type !== 'rule') {
format: (def: string) => { return
definition = def.replace(/:merge\(([^)]+)\)/g, '$1') }
},
wrap: (rule: Container) => { rule.selectors = rule.selectors.map((selector) => {
if (isAtRule(rule)) { return modifierFunction({
definition = `@${rule.name} ${rule.params}` get className() {
return getClassNameFromSelector(selector)
},
selector,
})
})
})
return root
}
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}`
}
},
})
if (!definition) {
definition = returnValue
} }
},
})
if (!definition) { if (definition) {
definition = returnValue definitions.push(definition)
} continue
}
if (definition) { container.walkDecls((decl) => {
definitions.push(definition) decl.remove()
continue })
}
container.walkDecls((decl) => { definition = container
decl.remove() .toString()
}) .replace(`.${escape(`${variantName}:${placeholder}`)}`, '&')
.replace(/(?<!\\)[{}]/g, '')
.replace(/\s*\n\s*/g, ' ')
.trim()
definition = container if (!definition.includes(placeholder)) {
.toString() definitions.push(definition)
.replace(`.${escape(`${variantName}:${placeholder}`)}`, '&') }
.replace(/(?<!\\)[{}]/g, '') }
.replace(/\s*\n\s*/g, ' ')
.trim()
if (!definition.includes(placeholder)) { return definitions
definitions.push(definition) },
} })
}
result[variantName] = definitions.join(', ') || null
} }
) )
@ -1311,7 +1324,13 @@ function getVariants(state: State): Record<string, string> {
}) })
}) })
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) { async function getPlugins(config: any) {

View File

@ -1,4 +1,4 @@
import { Settings, State } from './util/state' import { Settings, State, Variant } from './util/state'
import type { import type {
CompletionItem, CompletionItem,
CompletionItemKind, CompletionItemKind,
@ -110,7 +110,6 @@ export function completionsFromClassList(
} }
} }
let allVariants = Object.keys(state.variants)
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName) let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
replacementRange.start.character += offset replacementRange.start.character += offset
@ -123,55 +122,109 @@ export function completionsFromClassList(
let items: CompletionItem[] = [] let items: CompletionItem[] = []
if (!important) { 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( items.push(
...Object.entries(state.variants) ...state.variants.flatMap((variant) => {
.filter(([variant]) => !existingVariants.includes(variant)) let items: CompletionItem[] = []
.map(([variant, definition], index) => {
let resultingVariants = [...existingVariants, variant] 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) { if (shouldSortVariants) {
let allVariants = state.variants.map(({ name }) => name)
resultingVariants = resultingVariants.sort( resultingVariants = resultingVariants.sort(
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a) (a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
) )
} }
return { items.push(
label: variant + sep, variantItem({
kind: 9, label: `${variant.name}${sep}`,
detail: definition, detail: variant.selectors().join(', '),
data: 'variant', textEdit: {
command: { newText: resultingVariants[resultingVariants.length - 1] + sep,
title: '', },
command: 'editor.action.triggerSuggest', additionalTextEdits:
}, shouldSortVariants && resultingVariants.length > 1
sortText: '-' + naturalExpand(index), ? [
textEdit: { {
newText: resultingVariants[resultingVariants.length - 1] + sep, newText:
range: replacementRange, resultingVariants.slice(0, resultingVariants.length - 1).join(sep) +
}, sep,
additionalTextEdits: range: {
shouldSortVariants && resultingVariants.length > 1 start: {
? [ ...classListRange.start,
{ character: classListRange.end.character - partialClassName.length,
newText: },
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep, end: {
range: { ...replacementRange.start,
start: { character: replacementRange.start.character,
...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 @@ function provideVariantsDirectiveCompletions(
if (/\s+/.test(parts[parts.length - 1])) return null 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) const existingVariants = parts.slice(0, parts.length - 1)
if (state.jit) { if (state.jit) {

View File

@ -32,7 +32,12 @@ export function getInvalidVariantDiagnostics(
ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range)) 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) { if (state.jit) {
possibleVariants.unshift('responsive') possibleVariants.unshift('responsive')
possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v)) possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v))

View File

@ -5,17 +5,25 @@ export function getVariantsFromClassName(
state: State, state: State,
className: string className: string
): { variants: string[]; offset: number } { ): { variants: string[]; offset: number } {
let allVariants = Object.keys(state.variants) let allVariants = state.variants.flatMap((variant) => {
let parts = splitAtTopLevelOnly(className, state.separator).filter(Boolean) if (variant.values.length) {
return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
}
return [variant.name]
})
let variants = new Set<string>() let variants = new Set<string>()
let offset = 0 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) { for (let part of parts) {
if ( if (
allVariants.includes(part) || allVariants.includes(part) ||
(state.jit && (state.jit &&
((part.includes('[') && part.endsWith(']')) || ((part.includes('[') && part.endsWith(']')) || part.includes('/')) &&
(part.includes('<') && part.includes('>'))) &&
jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0) jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0)
) { ) {
variants.add(part) variants.add(part)

View File

@ -80,6 +80,14 @@ export interface FeatureFlags {
experimental: string[] experimental: string[]
} }
export interface Variant {
name: string
values: string[]
isArbitrary: boolean
hasDash: boolean
selectors: (params?: { value?: string; label?: string }) => string[]
}
export interface State { export interface State {
enabled: boolean enabled: boolean
configPath?: string configPath?: string
@ -90,7 +98,7 @@ export interface State {
dependencies?: string[] dependencies?: string[]
plugins?: any plugins?: any
screens?: string[] screens?: string[]
variants?: Record<string, string | null> variants?: Variant[]
corePlugins?: string[] corePlugins?: string[]
modules?: { modules?: {
tailwindcss?: { version: string; module: any } tailwindcss?: { version: string; module: any }

View File

@ -26,6 +26,7 @@ import {
ProviderResult, ProviderResult,
SnippetString, SnippetString,
TextEdit, TextEdit,
TextEditorSelectionChangeKind,
} from 'vscode' } from 'vscode'
import { import {
LanguageClient, LanguageClient,
@ -149,6 +150,62 @@ export async function activate(context: ExtensionContext) {
}) })
) )
// 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) let watcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true)
watcher.onDidCreate((uri) => { watcher.onDidCreate((uri) => {