From c9fea34deb4d022fe18774b6b99badc9981aa230 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sun, 10 May 2020 13:06:22 +0100 Subject: [PATCH] filter @apply completions --- src/class-names/extractClassNames.js | 177 ++++++++++++------------ src/class-names/index.js | 17 ++- src/lsp/providers/completionProvider.ts | 47 +++++-- 3 files changed, 135 insertions(+), 106 deletions(-) diff --git a/src/class-names/extractClassNames.js b/src/class-names/extractClassNames.js index df45599..2aa426b 100644 --- a/src/class-names/extractClassNames.js +++ b/src/class-names/extractClassNames.js @@ -46,99 +46,102 @@ function getClassNamesFromSelector(selector) { return classNames } -async function process(ast) { +async function process(groups) { const tree = {} const commonContext = {} - ast.root.walkRules((rule) => { - const classNames = getClassNamesFromSelector(rule.selector) + groups.forEach((group) => { + group.root.walkRules((rule) => { + const classNames = getClassNamesFromSelector(rule.selector) - const decls = {} - rule.walkDecls((decl) => { - if (decls[decl.prop]) { - decls[decl.prop] = [ - ...(Array.isArray(decls[decl.prop]) - ? decls[decl.prop] - : [decls[decl.prop]]), - decl.value, - ] - } else { - decls[decl.prop] = decl.value + const decls = {} + rule.walkDecls((decl) => { + if (decls[decl.prop]) { + decls[decl.prop] = [ + ...(Array.isArray(decls[decl.prop]) + ? decls[decl.prop] + : [decls[decl.prop]]), + decl.value, + ] + } else { + 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 } diff --git a/src/class-names/index.js b/src/class-names/index.js index 1e2d3e5..8aef0b0 100644 --- a/src/class-names/index.js +++ b/src/class-names/index.js @@ -83,12 +83,12 @@ export default async function getClassNames( } hook.unwatch() - const ast = await postcss([tailwindcss(configPath)]).process( - ` - @tailwind components; - @tailwind utilities; - `, - { from: undefined } + const [components, utilities] = await Promise.all( + ['components', 'utilities'].map((group) => + postcss([tailwindcss(configPath)]).process(`@tailwind ${group};`, { + from: undefined, + }) + ) ) hook.unhook() @@ -111,7 +111,10 @@ export default async function getClassNames( configPath, config: resolvedConfig, 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, plugins: getPlugins(config), variants: getVariants({ config, version, postcss, browserslist }), diff --git a/src/lsp/providers/completionProvider.ts b/src/lsp/providers/completionProvider.ts index d7cbbef..8f2c43d 100644 --- a/src/lsp/providers/completionProvider.ts +++ b/src/lsp/providers/completionProvider.ts @@ -28,7 +28,8 @@ import { ensureArray } from '../../util/array' function completionsFromClassList( state: State, classList: string, - classListRange: Range + classListRange: Range, + filter?: (item: CompletionItem) => boolean ): CompletionList { let classNames = classList.split(/[\s+]/) const partialClassName = classNames[classNames.length - 1] @@ -68,8 +69,8 @@ function completionsFromClassList( return { isIncomplete: false, - items: Object.keys(isSubset ? subset : state.classNames.classNames).map( - (className, index) => { + items: Object.keys(isSubset ? subset : state.classNames.classNames) + .map((className, index) => { let label = className let kind: CompletionItemKind = CompletionItemKind.Constant let documentation: string = null @@ -88,7 +89,7 @@ function completionsFromClassList( } } - return { + const item = { label, kind, documentation, @@ -100,8 +101,14 @@ function completionsFromClassList( 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 - return completionsFromClassList(state, classList, { - start: { - line: position.line, - character: position.character - classList.length, + return completionsFromClassList( + state, + classList, + { + 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(