From f59adbe35b2100a3ef86e51c51184c901599b3e8 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 18:05:04 +0100 Subject: [PATCH] Adopt `getVariants` API --- .../tailwindcss-language-server/src/server.ts | 187 ++++++++++-------- .../src/completionProvider.ts | 140 +++++++++---- .../getInvalidVariantDiagnostics.ts | 7 +- .../src/util/getVariantsFromClassName.ts | 16 +- .../src/util/state.ts | 10 +- packages/vscode-tailwindcss/src/extension.ts | 57 ++++++ 6 files changed, 286 insertions(+), 131 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index d98ab34..008c221 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -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 { - 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 { + if (state.jitContext?.getVariants) { + return state.jitContext.getVariants() + } - let result = {} + if (state.jit) { + let result: Array = [] // [name, [sort, fn]] // [name, [[sort, fn]]] Array.from(state.jitContext.variantMap as Map).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(/(? { }) }) - 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) { diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 941554f..adacd13 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -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 & { + 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) { diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts index 1b6c997..454e0f5 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidVariantDiagnostics.ts @@ -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)) diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index 10dfe90..b1709e1 100644 --- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts +++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts @@ -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() 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) diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index cb86377..4c82bb5 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -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 + variants?: Variant[] corePlugins?: string[] modules?: { tailwindcss?: { version: string; module: any } diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index 4b2bdfb..d1cb232 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -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) => {