filter @apply completions

master
Brad Cornes 2020-05-10 13:06:22 +01:00
parent ef1f922c86
commit c9fea34deb
3 changed files with 135 additions and 106 deletions

View File

@ -46,99 +46,102 @@ function getClassNamesFromSelector(selector) {
return classNames return classNames
} }
async function process(ast) { async function process(groups) {
const tree = {} const tree = {}
const commonContext = {} const commonContext = {}
ast.root.walkRules((rule) => { groups.forEach((group) => {
const classNames = getClassNamesFromSelector(rule.selector) group.root.walkRules((rule) => {
const classNames = getClassNamesFromSelector(rule.selector)
const decls = {} const decls = {}
rule.walkDecls((decl) => { rule.walkDecls((decl) => {
if (decls[decl.prop]) { if (decls[decl.prop]) {
decls[decl.prop] = [ decls[decl.prop] = [
...(Array.isArray(decls[decl.prop]) ...(Array.isArray(decls[decl.prop])
? decls[decl.prop] ? decls[decl.prop]
: [decls[decl.prop]]), : [decls[decl.prop]]),
decl.value, decl.value,
] ]
} else { } else {
decls[decl.prop] = decl.value decls[decl.prop] = decl.value
}
})
let p = rule
const keys = []
while (p.parent.type !== 'root') {
p = p.parent
if (p.type === 'atrule') {
keys.push(`@${p.name} ${p.params}`)
}
}
for (let i = 0; i < classNames.length; i++) {
const context = keys.concat([])
const baseKeys = classNames[i].className.split('__TAILWIND_SEPARATOR__')
const contextKeys = baseKeys.slice(0, baseKeys.length - 1)
const index = []
const existing = dlv(tree, baseKeys)
if (typeof existing !== 'undefined') {
if (Array.isArray(existing)) {
const scopeIndex = existing.findIndex(
(x) =>
x.__scope === classNames[i].scope &&
arraysEqual(existing.__context, context)
)
if (scopeIndex > -1) {
keys.unshift(scopeIndex)
index.push(scopeIndex)
} else {
keys.unshift(existing.length)
index.push(existing.length)
}
} else {
if (
existing.__scope !== classNames[i].scope ||
!arraysEqual(existing.__context, context)
) {
dset(tree, baseKeys, [existing])
keys.unshift(1)
index.push(1)
}
}
}
if (classNames[i].__rule) {
dset(tree, [...baseKeys, ...index, '__rule'], true)
dset(tree, [...baseKeys, ...index, '__source'], group.source)
dsetEach(tree, [...baseKeys, ...index], decls)
}
if (classNames[i].__pseudo) {
dset(tree, [...baseKeys, '__pseudo'], classNames[i].__pseudo)
}
dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope)
dset(
tree,
[...baseKeys, ...index, '__context'],
context.concat([]).reverse()
)
// common context
if (classNames[i].__pseudo) {
context.push(...classNames[i].__pseudo)
}
for (let i = 0; i < contextKeys.length; i++) {
if (typeof commonContext[contextKeys[i]] === 'undefined') {
commonContext[contextKeys[i]] = context
} else {
commonContext[contextKeys[i]] = intersection(
commonContext[contextKeys[i]],
context
)
}
}
} }
}) })
let p = rule
const keys = []
while (p.parent.type !== 'root') {
p = p.parent
if (p.type === 'atrule') {
keys.push(`@${p.name} ${p.params}`)
}
}
for (let i = 0; i < classNames.length; i++) {
const context = keys.concat([])
const baseKeys = classNames[i].className.split('__TAILWIND_SEPARATOR__')
const contextKeys = baseKeys.slice(0, baseKeys.length - 1)
const index = []
const existing = dlv(tree, baseKeys)
if (typeof existing !== 'undefined') {
if (Array.isArray(existing)) {
const scopeIndex = existing.findIndex(
(x) =>
x.__scope === classNames[i].scope &&
arraysEqual(existing.__context, context)
)
if (scopeIndex > -1) {
keys.unshift(scopeIndex)
index.push(scopeIndex)
} else {
keys.unshift(existing.length)
index.push(existing.length)
}
} else {
if (
existing.__scope !== classNames[i].scope ||
!arraysEqual(existing.__context, context)
) {
dset(tree, baseKeys, [existing])
keys.unshift(1)
index.push(1)
}
}
}
if (classNames[i].__rule) {
dset(tree, [...baseKeys, ...index, '__rule'], true)
dsetEach(tree, [...baseKeys, ...index], decls)
}
if (classNames[i].__pseudo) {
dset(tree, [...baseKeys, '__pseudo'], classNames[i].__pseudo)
}
dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope)
dset(
tree,
[...baseKeys, ...index, '__context'],
context.concat([]).reverse()
)
// common context
if (classNames[i].__pseudo) {
context.push(...classNames[i].__pseudo)
}
for (let i = 0; i < contextKeys.length; i++) {
if (typeof commonContext[contextKeys[i]] === 'undefined') {
commonContext[contextKeys[i]] = context
} else {
commonContext[contextKeys[i]] = intersection(
commonContext[contextKeys[i]],
context
)
}
}
}
}) })
return { classNames: tree, context: commonContext } return { classNames: tree, context: commonContext }

View File

@ -83,12 +83,12 @@ export default async function getClassNames(
} }
hook.unwatch() hook.unwatch()
const ast = await postcss([tailwindcss(configPath)]).process( const [components, utilities] = await Promise.all(
` ['components', 'utilities'].map((group) =>
@tailwind components; postcss([tailwindcss(configPath)]).process(`@tailwind ${group};`, {
@tailwind utilities; from: undefined,
`, })
{ from: undefined } )
) )
hook.unhook() hook.unhook()
@ -111,7 +111,10 @@ export default async function getClassNames(
configPath, configPath,
config: resolvedConfig, config: resolvedConfig,
separator: typeof userSeperator === 'undefined' ? ':' : userSeperator, separator: typeof userSeperator === 'undefined' ? ':' : userSeperator,
classNames: await extractClassNames(ast), classNames: await extractClassNames([
{ root: components.root, source: 'components' },
{ root: utilities.root, source: 'utilities' },
]),
dependencies: hook.deps, dependencies: hook.deps,
plugins: getPlugins(config), plugins: getPlugins(config),
variants: getVariants({ config, version, postcss, browserslist }), variants: getVariants({ config, version, postcss, browserslist }),

View File

@ -28,7 +28,8 @@ import { ensureArray } from '../../util/array'
function completionsFromClassList( function completionsFromClassList(
state: State, state: State,
classList: string, classList: string,
classListRange: Range classListRange: Range,
filter?: (item: CompletionItem) => boolean
): CompletionList { ): CompletionList {
let classNames = classList.split(/[\s+]/) let classNames = classList.split(/[\s+]/)
const partialClassName = classNames[classNames.length - 1] const partialClassName = classNames[classNames.length - 1]
@ -68,8 +69,8 @@ function completionsFromClassList(
return { return {
isIncomplete: false, isIncomplete: false,
items: Object.keys(isSubset ? subset : state.classNames.classNames).map( items: Object.keys(isSubset ? subset : state.classNames.classNames)
(className, index) => { .map((className, index) => {
let label = className let label = className
let kind: CompletionItemKind = CompletionItemKind.Constant let kind: CompletionItemKind = CompletionItemKind.Constant
let documentation: string = null let documentation: string = null
@ -88,7 +89,7 @@ function completionsFromClassList(
} }
} }
return { const item = {
label, label,
kind, kind,
documentation, documentation,
@ -100,8 +101,14 @@ function completionsFromClassList(
range: replacementRange, range: replacementRange,
}, },
} }
}
), if (filter && !filter(item)) {
return null
}
return item
})
.filter((item) => item !== null),
} }
} }
@ -175,13 +182,29 @@ function provideAtApplyCompletions(
const classList = match.groups.classList const classList = match.groups.classList
return completionsFromClassList(state, classList, { return completionsFromClassList(
start: { state,
line: position.line, classList,
character: position.character - classList.length, {
start: {
line: position.line,
character: position.character - classList.length,
},
end: position,
}, },
end: position, (item) => {
}) // TODO: first line excludes all subtrees but there could _technically_ be
// valid apply-able class names in there. Will be correct in 99% of cases
if (item.kind === CompletionItemKind.Module) return false
let info = dlv(state.classNames.classNames, item.data)
return (
!Array.isArray(info) &&
info.__source === 'utilities' &&
(info.__context || []).length === 0 &&
(info.__pseudo || []).length === 0
)
}
)
} }
function provideClassNameCompletions( function provideClassNameCompletions(