Adopt `getVariants` API
parent
bf57dd14bc
commit
f59adbe35b
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in New Issue