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,
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
)
let placeholder = '__variant_placeholder__'
let root = state.modules.postcss.module.root({
nodes: [
state.modules.postcss.module.rule({
selector: `.${escape(placeholder)}`,
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
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)
}
rule.selectors = rule.selectors.map((selector) => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
})
let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map(
([_sort, fn]) => fn
)
let placeholder = '__variant_placeholder__'
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) {
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}`
function getClassNameFromSelector(selector) {
return classNameParser.transformSync(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
}
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) {
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 @@ 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) {

View File

@ -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 @@ export function completionsFromClassList(
}
}
let allVariants = Object.keys(state.variants)
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
replacementRange.start.character += offset
@ -123,55 +122,109 @@ export function completionsFromClassList(
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 @@ function provideVariantsDirectiveCompletions(
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) {

View File

@ -32,7 +32,12 @@ export function getInvalidVariantDiagnostics(
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))

View File

@ -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)

View File

@ -80,6 +80,14 @@ export interface FeatureFlags {
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 @@ export interface State {
dependencies?: string[]
plugins?: any
screens?: string[]
variants?: Record<string, string | null>
variants?: Variant[]
corePlugins?: string[]
modules?: {
tailwindcss?: { version: string; module: any }

View File

@ -26,6 +26,7 @@ import {
ProviderResult,
SnippetString,
TextEdit,
TextEditorSelectionChangeKind,
} from 'vscode'
import {
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)
watcher.onDidCreate((uri) => {