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