diff --git a/packages/css/src/config/index.ts b/packages/css/src/config/index.ts index a255d48e5..1fd9476c6 100644 --- a/packages/css/src/config/index.ts +++ b/packages/css/src/config/index.ts @@ -36,14 +36,14 @@ export { } export type VariableValue = number | string | Array -export type VariableDefinition = { [key in '' | `@${string}` | string]?: VariableValue | VariableDefinition } +export type VariableDefinition = { [key in '' | `@${string}` | string]?: VariableValue | VariableDefinition } | VariableValue export type CSSKeyframes = { [key in 'from' | 'to' | string]: PropertiesHyphen } export type AnimationDefinitions = { [key: string]: CSSKeyframes } export type SelectorDefinitions = { [key: string]: string | string[] | SelectorDefinitions } export type MediaQueryDefinitions = { [key: string]: number | string | MediaQueryDefinitions } export type StyleDefinitions = { [key: string]: string | StyleDefinitions } export type RuleDefinitions = { [key in keyof typeof rules | string]?: RuleDefinition } -export type VariableDefinitions = { [key in keyof typeof rules]?: VariableDefinition | VariableValue } & { [key: string]: VariableDefinition | VariableValue } +export type VariableDefinitions = { [key in keyof typeof rules]?: VariableDefinition } & { [key: string]: VariableDefinition } export type UtilityDefinitions = { [key in keyof typeof utilities]?: PropertiesHyphen } & { [key: string]: PropertiesHyphen } export interface FunctionDefinition { unit?: string diff --git a/packages/css/src/config/rules.ts b/packages/css/src/config/rules.ts index 3da84d4be..82f42701a 100644 --- a/packages/css/src/config/rules.ts +++ b/packages/css/src/config/rules.ts @@ -586,7 +586,7 @@ const rules = { } as RuleDefinition, 'text-rendering': { ambiguousKeys: ['text', 't'], - ambiguousValues: ['optimizespeed', 'optimizelegibility', 'geometricprecision'], + ambiguousValues: ['optimizeSpeed', 'optimizeLegibility', 'geometricPrecision'], layer: Layer.Native, } as RuleDefinition, 'text-indent': { diff --git a/packages/css/src/core.ts b/packages/css/src/core.ts index 0ae9e6754..da01069d9 100644 --- a/packages/css/src/core.ts +++ b/packages/css/src/core.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ import { Rule, type NativeRule, type RuleDefinition, type RegisteredRule, AtFeatureComponent } from './rule' -import type { Config, AnimationDefinitions } from './config' +import type { Config, AnimationDefinitions, VariableDefinition } from './config' import { config as defaultConfig } from './config' import Layer from './layer' import hexToRgb from './utils/hex-to-rgb' @@ -9,17 +9,18 @@ import extendConfig from './utils/extend-config' import { type PropertiesHyphen } from 'csstype' import './types/global' // fix: ../css/src/core.ts:1205:16 - error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. -type VariableValue = - { type: 'string', value: string } - | { type: 'number', value: number } - | { type: 'color', value: string, space: 'rgb' | 'hsl' } - -export type Variable = Omit & { - value?: any, - space?: any, +type VariableCommon = { usage?: number, - modes?: { [mode: string]: VariableValue } + group?: string, + name: string, + key: string, + modes?: { [mode: string]: TypeVariable } } +export type StringVariable = { type: 'string', value: string } +export type NumberVariable = { type: 'number', value: number } +export type ColorVariable = { type: 'color', value: string, space: 'rgb' | 'hsl' } +export type TypeVariable = StringVariable | NumberVariable | ColorVariable +export type Variable = TypeVariable & VariableCommon export default class MasterCSS { static config: Config = defaultConfig @@ -88,92 +89,94 @@ export default class MasterCSS { if (variables) { const unexecutedAliasVariable: Record void }> = {} - const parseVariable = (variable: any, name: string, mode?: string) => { - if (typeof variable === undefined || variable === null) - return - + const resolveVariable = (variableDefinition: VariableDefinition, name: string[], mode?: string) => { + if (variableDefinition === undefined || variableDefinition === null) return const addVariable = ( - name: string, - variableValue: VariableValue, + name: string[], + variable: any, replacedMode?: string, alpha?: string ) => { - if (variableValue === undefined) + if (variable === undefined) return - - if (variableValue.type === 'color') { + const flatName = name.join('-') + const groups = name.slice(0, -1) + const key = name[name.length - 1] + variable.key = key + variable.name = flatName + if (groups.length) + variable.group = groups.join('.') + if (variable.type === 'color') { if (alpha) { - const slashIndex = variableValue.value.indexOf('/') - variableValue = { - ...variableValue, + const slashIndex = variable.value.indexOf('/') + variable = { + ...variable, value: slashIndex === -1 - ? variableValue.value + ' / ' + (alpha.startsWith('0.') ? alpha.slice(1) : alpha) - : (variableValue.value.slice(0, slashIndex + 2) + String(+variableValue.value.slice(slashIndex + 2) * +alpha).slice(1)) + ? variable.value + ' / ' + (alpha.startsWith('0.') ? alpha.slice(1) : alpha) + : (variable.value.slice(0, slashIndex + 2) + String(+variable.value.slice(slashIndex + 2) * +alpha).slice(1)) } } - - colorVariableNames[name] = undefined + colorVariableNames[flatName] = undefined } - const currentMode = replacedMode ?? mode if (currentMode !== undefined) { - if (Object.prototype.hasOwnProperty.call(this.variables, name)) { - const variable = this.variables[name] + if (Object.prototype.hasOwnProperty.call(this.variables, flatName)) { + const foundVariable = this.variables[flatName] if (currentMode) { - if (!variable.modes) { - variable.modes = {} + if (!foundVariable.modes) { + foundVariable.modes = {} } - variable.modes[currentMode] = variableValue + foundVariable.modes[currentMode] = variable } else { - variable.value = variableValue.value - if (variableValue.type === 'color') { - variable.space = variableValue.space + foundVariable.value = variable.value + if (variable.type === 'color') { + (foundVariable as ColorVariable).space = variable.space } } } else { if (currentMode) { - const newVariable: Variable = { - type: variableValue.type, - modes: { [currentMode]: variableValue } + const newVariable: any = { + key: variable.key, + type: variable.type, + modes: { [currentMode]: variable } } - if (variableValue.type === 'color') { - newVariable.space = variableValue.space + if (variable.type === 'color') { + newVariable.space = variable.space } - this.variables[name] = newVariable + this.variables[flatName] = newVariable } else { - this.variables[name] = variableValue + this.variables[flatName] = variable } } } else { - this.variables[name] = variableValue + this.variables[flatName] = variable } } - - const type = typeof variable - if (type === 'object') { - if (Array.isArray(variable)) { - addVariable(name, { type: 'string', value: variable.join(',') }) + if (typeof variableDefinition === 'object') { + if (Array.isArray(variableDefinition)) { + addVariable(name, { type: 'string', value: variableDefinition.join(',') }) } else { - const keys = Object.keys(variable) + const keys = Object.keys(variableDefinition) for (const eachKey of keys) { if (eachKey === '' || eachKey.startsWith('@')) { - parseVariable(variable[eachKey], name, (eachKey || keys.some(eachKey => eachKey.startsWith('@'))) ? eachKey.slice(1) : undefined) + resolveVariable(variableDefinition[eachKey] as VariableDefinition, name, (eachKey || keys.some(eachKey => eachKey.startsWith('@'))) ? eachKey.slice(1) : undefined) } else { - parseVariable(variable[eachKey], name + '-' + eachKey, undefined) + resolveVariable(variableDefinition[eachKey] as VariableDefinition, [...name, eachKey]) } } } - } else if (type === 'number') { - addVariable(name, { type: 'number', value: variable }) - addVariable('-' + name, { type: 'number', value: variable * -1 }) - } else if (type === 'string') { - const aliasResult = /^\$\((.*?)\)(?: ?\/ ?(.+?))?$/.exec(variable) + } else if (typeof variableDefinition === 'number') { + addVariable(name, { type: 'number', value: variableDefinition }) + addVariable(['', ...name], { type: 'number', value: variableDefinition * -1 }) + } else if (typeof variableDefinition === 'string') { + const aliasResult = /^\$\((.*?)\)(?: ?\/ ?(.+?))?$/.exec(variableDefinition) + const flatName = name.join('-') if (aliasResult) { - if (!Object.prototype.hasOwnProperty.call(unexecutedAliasVariable, name)) { - unexecutedAliasVariable[name] = {} + if (!Object.prototype.hasOwnProperty.call(unexecutedAliasVariable, flatName)) { + unexecutedAliasVariable[flatName] = {} } - unexecutedAliasVariable[name][mode as string] = () => { - delete unexecutedAliasVariable[name][mode as string] + unexecutedAliasVariable[flatName][mode as string] = () => { + delete unexecutedAliasVariable[flatName][mode as string] const [alias, aliasMode] = aliasResult[1].split('@') if (alias) { @@ -188,7 +191,7 @@ export default class MasterCSS { if (aliasMode === undefined && aliasVariable.modes) { addVariable( name, - { type: aliasVariable.type, value: aliasVariable.value, space: aliasVariable.space }, + { type: aliasVariable.type, value: aliasVariable.value, space: (aliasVariable as ColorVariable).space }, '', aliasResult[2] ) @@ -205,7 +208,7 @@ export default class MasterCSS { ? aliasVariable.modes?.[aliasMode] : aliasVariable if (variable) { - const newVariable = { type: variable.type, value: variable.value } as VariableValue + const newVariable = { type: variable.type, value: variable.value } as Variable if (variable.type === 'color') { (newVariable as any).space = variable.space } @@ -216,20 +219,20 @@ export default class MasterCSS { } } } else { - const hexColorResult = /^#([A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.exec(variable) + const hexColorResult = /^#([A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.exec(variableDefinition) if (hexColorResult) { const [r, g, b, a] = hexToRgb(hexColorResult[1]) addVariable(name, { type: 'color', value: `${r} ${g} ${b}${a === 1 ? '' : ' / ' + a}`, space: 'rgb' }) } else { - const rgbFunctionResult = /^rgb\( *([0-9]{1,3})(?: *, *| +)([0-9]{1,3})(?: *, *| +)([0-9]{1,3}) *(?:(?:,|\/) *(.*?) *)?\)$/.exec(variable) + const rgbFunctionResult = /^rgb\( *([0-9]{1,3})(?: *, *| +)([0-9]{1,3})(?: *, *| +)([0-9]{1,3}) *(?:(?:,|\/) *(.*?) *)?\)$/.exec(variableDefinition) if (rgbFunctionResult) { addVariable(name, { type: 'color', value: rgbFunctionResult[1] + ' ' + rgbFunctionResult[2] + ' ' + rgbFunctionResult[3] + (rgbFunctionResult[4] ? ' / ' + (rgbFunctionResult[4].startsWith('0.') ? rgbFunctionResult[4].slice(1) : rgbFunctionResult[4]) : ''), space: 'rgb' }) } else { - const hslFunctionResult = /^hsl\((.*?)\)$/.exec(variable) + const hslFunctionResult = /^hsl\((.*?)\)$/.exec(variableDefinition) if (hslFunctionResult) { addVariable(name, { type: 'color', value: hslFunctionResult[1], space: 'hsl' }) } else { - addVariable(name, { type: 'string', value: variable }) + addVariable(name, { type: 'string', value: variableDefinition }) } } } @@ -237,8 +240,9 @@ export default class MasterCSS { } } for (const parnetKey in variables) { - parseVariable(variables[parnetKey], parnetKey) + resolveVariable(variables[parnetKey], [parnetKey]) } + // todo: address to the target variable for (const name of Object.keys(unexecutedAliasVariable)) { for (const mode of Object.keys(unexecutedAliasVariable[name])) { unexecutedAliasVariable[name][mode]?.() @@ -332,29 +336,31 @@ export default class MasterCSS { return b[0].localeCompare(a[0]) }) .forEach(([id, eachRuleDefinition], index: number) => { - const eachRegisteredRule: RegisteredRule = { + const EachRule: RegisteredRule = { id, variables: {}, matchers: {}, order: rulesEntriesLength - 1 - index, definition: eachRuleDefinition } - this.Rules.push(eachRegisteredRule) - const { matcher, layer, key, subkey, ambiguousKeys, ambiguousValues } = eachRuleDefinition + this.Rules.push(EachRule) + const { matcher, layer, subkey, ambiguousKeys, ambiguousValues } = eachRuleDefinition + let { key } = eachRuleDefinition if (layer === Layer.Utility) { - eachRegisteredRule.id = '.' + id - eachRegisteredRule.matchers.arbitrary = new RegExp('^' + escapeString(id) + '(?=!|\\*|>|\\+|~|:|\\[|@|_|\\.|$)', 'm') + EachRule.id = '.' + id + EachRule.matchers.arbitrary = new RegExp('^' + escapeString(id) + '(?=!|\\*|>|\\+|~|:|\\[|@|_|\\.|$)', 'm') + } + const keyPatterns = [] + if (layer === Layer.NativeShorthand || layer === Layer.Native) { + if (!key) eachRuleDefinition.key = key = id + keyPatterns.push(id) } - // todo: 不可使用 startsWith 判斷,應改為更精準的從 config.variables 取得目標變數群組,但 config.variables 中的值還沒被 resolve 像是 Array - const addResolvedVariables = (prefix: string) => { + const addResolvedVariables = (groupName: string) => { for (const eachVariableName in this.variables) { - if (eachVariableName.startsWith(prefix + '-') || eachVariableName.startsWith('-' + prefix + '-')) { - const simplifiedName = eachVariableName.slice(prefix.length + (prefix.startsWith('-') ? 0 : 1)) - eachRegisteredRule.variables[simplifiedName] = { - ...this.variables[eachVariableName], - name: eachVariableName, - } + const eachVariable = this.variables[eachVariableName] + if (eachVariable.group === groupName) { + EachRule.variables[eachVariable.key] = eachVariable } } } @@ -370,15 +376,11 @@ export default class MasterCSS { addResolvedVariables(id) const colorsPatten = colorNames.join('|') - const keyPatterns = [] - if (layer === Layer.NativeShorthand || layer === Layer.Native) { - keyPatterns.push(id) - } if (!matcher) { if (!key && !subkey) { keyPatterns.push(id) - } else if (key || subkey) { - if (key) keyPatterns.push(key) + } else { + if (key && !keyPatterns.includes(key)) keyPatterns.push(key) if (subkey) keyPatterns.push(subkey) if (layer === Layer.Shorthand) { keyPatterns.push(id) @@ -386,7 +388,7 @@ export default class MasterCSS { } if (ambiguousKeys?.length) { const ambiguousKeyPattern = ambiguousKeys.length > 1 ? `(?:${ambiguousKeys.join('|')})` : ambiguousKeys[0] - const variableKeys = Object.keys(eachRegisteredRule.variables) + const variableKeys = Object.keys(EachRule.variables) if (ambiguousValues?.length) { const ambiguousValuePatterns = [] for (const eachAmbiguousValue of ambiguousValues) { @@ -396,21 +398,17 @@ export default class MasterCSS { ambiguousValuePatterns.unshift(`${eachAmbiguousValue}\\b`) } } - eachRegisteredRule.matchers.value = new RegExp(`^${ambiguousKeyPattern}:(?:${ambiguousValuePatterns.join('|')})[^|]*?(?:@|$)`) + EachRule.matchers.value = new RegExp(`^${ambiguousKeyPattern}:(?:${ambiguousValuePatterns.join('|')})[^|]*?(?:@|$)`) } if (variableKeys.length) { - eachRegisteredRule.matchers.variable = new RegExp(`^${ambiguousKeyPattern}:(?:${variableKeys.join('|')}(?![a-zA-Z0-9-]))[^|]*?(?:@|$)`) + EachRule.matchers.variable = new RegExp(`^${ambiguousKeyPattern}:(?:${variableKeys.join('|')}(?![a-zA-Z0-9-]))[^|]*?(?:@|$)`) } } - // if (id === 'background-clip') { - // console.log(eachRegisteredRule) - // } } else { - eachRegisteredRule.matchers.arbitrary = matcher as RegExp + EachRule.matchers.arbitrary = matcher as RegExp } - eachRegisteredRule.key = key || id if (keyPatterns.length) { - eachRegisteredRule.matchers.key = new RegExp(`^${keyPatterns.length > 1 ? `(${keyPatterns.join('|')})` : keyPatterns[0]}:`) + EachRule.matchers.key = new RegExp(`^${keyPatterns.length > 1 ? `(${keyPatterns.join('|')})` : keyPatterns[0]}:`) } }) } @@ -426,29 +424,29 @@ export default class MasterCSS { * 1. variable * @example fg:primary bg:blue */ - for (const eachRegisteredRule of this.Rules) { - if (eachRegisteredRule.matchers.variable?.test(className)) return eachRegisteredRule + for (const EachRule of this.Rules) { + if (EachRule.matchers.variable?.test(className)) return EachRule } /** * 2. value (ambiguous.key * ambiguous.values) * @example bg:current box:content font:12 */ - for (const eachRegisteredRule of this.Rules) { - if (eachRegisteredRule.matchers.value?.test(className)) return eachRegisteredRule + for (const EachRule of this.Rules) { + if (EachRule.matchers.value?.test(className)) return EachRule } /** * 3. full key * @example text-align:center color:blue-40 */ - for (const eachRegisteredRule of this.Rules) { - if (eachRegisteredRule.matchers.key?.test(className)) return eachRegisteredRule + for (const EachRule of this.Rules) { + if (EachRule.matchers.key?.test(className)) return EachRule } /** * 4. arbitrary * @example custom RegExp, utility */ - for (const eachRegisteredRule of this.Rules) { - if (eachRegisteredRule.matchers.arbitrary?.test(className)) return eachRegisteredRule + for (const EachRule of this.Rules) { + if (EachRule.matchers.arbitrary?.test(className)) return EachRule } } @@ -1076,7 +1074,7 @@ export default class MasterCSS { if (variable.usage) { variable.usage++ } else { - const addProperty = (mode: string, variableValue: VariableValue) => { + const addProperty = (mode: string, variable: TypeVariable) => { let nativeRule = this.variablesNativeRules[mode] if (!nativeRule) { let cssRule: CSSStyleRule @@ -1161,7 +1159,7 @@ export default class MasterCSS { const propertyName = '--' + eachVariableName if (!initializing || !(nativeRule.cssRule as CSSStyleRule).style.getPropertyValue(propertyName)) { - (nativeRule.cssRule as CSSStyleRule).style.setProperty(propertyName, String(variableValue.value)) + (nativeRule.cssRule as CSSStyleRule).style.setProperty(propertyName, String(variable.value)) } } if (variable.value) { diff --git a/packages/css/src/rule.ts b/packages/css/src/rule.ts index 4adad42bd..9fc70c1c9 100644 --- a/packages/css/src/rule.ts +++ b/packages/css/src/rule.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -import MasterCSS, { type Variable } from './core' +import MasterCSS, { ColorVariable, type Variable } from './core' import cssEscape from 'css-shared/utils/css-escape' import Layer from './layer' import { type PropertiesHyphen } from 'csstype' @@ -569,7 +569,7 @@ export class Rule { handleVariable( (variable) => { if (bypassParsing) { - currentValue += eachValueComponent.text = variable.value + currentValue += eachValueComponent.text = String(variable.value) } else { const valueComponent = this.parseValue(variable.value, unit) as NumericValueComponent currentValue += eachValueComponent.text = valueComponent.value + (valueComponent.unit ?? '') @@ -587,7 +587,7 @@ export class Rule { const alpha = eachValueComponent.alpha ? '/' + eachValueComponent.alpha : '' handleVariable( (variable) => { - currentValue += eachValueComponent.text = `${variable['space']}(${variable.value}${alpha})` + currentValue += eachValueComponent.text = `${(variable as ColorVariable)['space']}(${variable.value}${alpha})` }, () => { currentValue += eachValueComponent.text = `${variable.space}(var(--${eachValueComponent.name})${alpha})` @@ -856,13 +856,13 @@ export interface RegisteredRule { value?: RegExp arbitrary?: RegExp } - variables?: any + variables: Record order: number definition: RuleDefinition } export interface RuleDefinition { - layer: Layer + layer?: Layer matcher?: RegExp key?: string subkey?: string diff --git a/packages/css/tests/config/variables/font.test.ts b/packages/css/tests/config/variables/font.test.ts new file mode 100644 index 000000000..9747faaf3 --- /dev/null +++ b/packages/css/tests/config/variables/font.test.ts @@ -0,0 +1,18 @@ +it('should be able to access related font variables using inherited rules', () => { + expect(Object.keys(new MasterCSS().Rules.find(({ id }) => id === 'font')?.variables || {})).toEqual([ + // font-family + 'mono', + 'sans', + 'serif', + // font-weight + 'thin', + 'extralight', + 'light', + 'regular', + 'medium', + 'semibold', + 'bold', + 'extrabold', + 'heavy' + ]) +}) \ No newline at end of file diff --git a/packages/css/tests/rule.test.ts b/packages/css/tests/rule.test.ts index 3dd89ef3d..240ef26f4 100644 --- a/packages/css/tests/rule.test.ts +++ b/packages/css/tests/rule.test.ts @@ -28,26 +28,26 @@ test('registered Rule', () => { test('variables', () => { const css = new MasterCSS({ variables: { - lv1: { + a: { 1: 'test', - lv2: { + b: { 2: 'test' } } }, rules: { content: { - variables: ['lv1.lv2'] + variables: ['a.b'] } } }) - expect(css.variables['lv1-1']).toMatchObject({ type: 'string', value: 'test', group: 'lv1' }) - expect(css.variables['lv1-lv2-2']).toMatchObject({ type: 'string', value: 'test', group: 'lv1.lv2' }) + expect(css.variables['a-1']).toMatchObject({ type: 'string', value: 'test', group: 'a' }) + expect(css.variables['a-b-2']).toMatchObject({ type: 'string', value: 'test', group: 'a.b' }) expect(css.Rules.find(({ id }) => id === 'content')).toEqual({ definition: { key: 'content', layer: -1, - variables: ['lv1.lv2'] + variables: ['a.b'] }, id: 'content', matchers: { @@ -57,10 +57,10 @@ test('variables', () => { variables: { 2: { key: '2', - name: 'lv1-lv2-2', + name: 'a-b-2', type: 'string', value: 'test', - group: 'lv1.lv2' + group: 'a.b' } } }) diff --git a/packages/language-service/playground/hint-syntax-completions.html b/packages/language-service/playground/hint-syntax-completions.html index a017f5c55..8b187d112 100644 --- a/packages/language-service/playground/hint-syntax-completions.html +++ b/packages/language-service/playground/hint-syntax-completions.html @@ -1 +1 @@ -
hello world
\ No newline at end of file +
hello world
\ No newline at end of file diff --git a/packages/language-service/playground/master.css.ts b/packages/language-service/playground/master.css.ts index 587e12ea5..7c93d369d 100644 --- a/packages/language-service/playground/master.css.ts +++ b/packages/language-service/playground/master.css.ts @@ -1,7 +1,12 @@ -import { Config } from '@master/css' +import { Config, variables } from '@master/css' export default { styles: { btn: 'inline-flex' + }, + variables: { + 'font-family': { + sans: ['Inter', ...variables['font-family'].sans], + } } } as Config \ No newline at end of file diff --git a/packages/language-service/src/utils/get-main-completion-items.ts b/packages/language-service/src/utils/get-main-completion-items.ts index f1408b190..017cc412b 100644 --- a/packages/language-service/src/utils/get-main-completion-items.ts +++ b/packages/language-service/src/utils/get-main-completion-items.ts @@ -7,14 +7,12 @@ import type { IPropertyData, IValueData } from 'vscode-css-languageservice' export default function getMainCompletionItems(css: MasterCSS = new MasterCSS()): CompletionItem[] { const nativeProperties = cssDataProvider.provideProperties() const completionItems: CompletionItem[] = [] + const addedKeys = new Set() process.env.VSCODE_IPC_HOOK && console.time('getMainCompletionItems') - for (const ruleId in css.config.rules) { - const eachRule = css.config.rules[ruleId] - const nativeCSSPropertyData = nativeProperties.find(({ name }) => name === ruleId) - // todo: key alias - completionItems.push({ - label: ruleId + ':', - sortText: ruleId, + for (const id in css.config.rules) { + const eachRule = css.config.rules[id] + const nativeCSSPropertyData = nativeProperties.find(({ name }) => name === id) + const eachCompletionItem = { kind: CompletionItemKind.Property, documentation: getCSSDataDocumentation(nativeCSSPropertyData), detail: nativeCSSPropertyData?.syntax, @@ -22,7 +20,44 @@ export default function getMainCompletionItems(css: MasterCSS = new MasterCSS()) title: 'triggerSuggest', command: 'editor.action.triggerSuggest' } - }) + } + if (eachRule?.key) { + addedKeys.add(eachRule.key) + completionItems.push({ + ...eachCompletionItem, + label: eachRule.key + ':', + sortText: eachRule.key + }) + } + if (eachRule?.subkey) { + addedKeys.add(eachRule.subkey) + completionItems.push({ + ...eachCompletionItem, + label: eachRule.subkey + ':', + sortText: eachRule.subkey + }) + } + if (eachRule?.ambiguousKeys?.length) { + for (const ambiguousKey of eachRule.ambiguousKeys) { + if (addedKeys.has(ambiguousKey)) { + continue + } + /** + * Ambiguous keys are added to the completion list + * @example text: t: + */ + completionItems.push({ + kind: CompletionItemKind.Property, + detail: 'ambiguous key', + label: ambiguousKey + ':', + sortText: ambiguousKey, + command: { + title: 'triggerSuggest', + command: 'editor.action.triggerSuggest' + } + }) + } + } } // todo: test remap utility to native css property diff --git a/packages/language-service/src/utils/get-value-completion-items.ts b/packages/language-service/src/utils/get-value-completion-items.ts index 4a907bc34..266e2c550 100644 --- a/packages/language-service/src/utils/get-value-completion-items.ts +++ b/packages/language-service/src/utils/get-value-completion-items.ts @@ -6,9 +6,48 @@ import { getCSSDataDocumentation } from './get-css-data-documentation' export default function getValueCompletionItems(key: string, css: MasterCSS = new MasterCSS()): CompletionItem[] { const nativeProperties = cssDataProvider.provideProperties() const completionItems: CompletionItem[] = [] - const nativeCSSPropertyData = nativeProperties.find((x: { name: string }) => x.name === key) - if (!nativeCSSPropertyData) return completionItems - nativeCSSPropertyData.values?.forEach(value => { + for (const EachRule of css.Rules) { + const nativePropertyData = nativeProperties.find((x: { name: string }) => x.name === EachRule.id) + /** + * Scoped variables + */ + if (EachRule.definition.key === key || EachRule.definition.subkey === key) { + for (const variableName in EachRule.variables) { + if (variableName.startsWith('-')) continue + const variable = EachRule.variables[variableName] + completionItems.push({ + label: variableName, + kind: CompletionItemKind.Variable, + detail: '(variable) ' + variable.value + }) + } + } + /** + * Ambiguous values + * @example text: -> center, left, right, justify + * @example t: -> center, left, right, justify + */ + if (EachRule.definition.ambiguousKeys?.includes(key) && EachRule.definition.ambiguousValues?.length) { + for (const ambiguousValue of EachRule.definition.ambiguousValues) { + if (typeof ambiguousValue !== 'string') continue + const nativeValueData = nativePropertyData?.values?.find((x: { name: string }) => x.name === ambiguousValue) + completionItems.push({ + label: ambiguousValue, + kind: CompletionItemKind.Value, + sortText: ambiguousValue, + documentation: getCSSDataDocumentation(nativeValueData, { + generatedCSS: generateCSS([key + ':' + ambiguousValue], css) + }), + detail: EachRule.id + ': ' + ambiguousValue + }) + } + } + } + /** + * Native values + */ + nativeProperties.find((x: { name: string }) => x.name === key)?.values?.forEach(value => { + if (completionItems.find(x => x.label === value.name)) return completionItems.push({ label: value.name, kind: CompletionItemKind.Value, @@ -21,5 +60,6 @@ export default function getValueCompletionItems(key: string, css: MasterCSS = ne detail: key + ': ' + value.name }) }) + return completionItems } \ No newline at end of file diff --git a/packages/language-service/tests/hint-syntax-completions.test.ts b/packages/language-service/tests/hint-syntax-completions.test.ts index 12171bf2f..a342e5c42 100644 --- a/packages/language-service/tests/hint-syntax-completions.test.ts +++ b/packages/language-service/tests/hint-syntax-completions.test.ts @@ -23,42 +23,54 @@ const simulateHintingCompletions = (target: string, { quotes = true, settings }: // it('types a', () => expect(simulateHintingCompletions('a')?.length).toBeDefined()) test.todo('following test require e2e -> packages/language-server') -// it('types " should hint completions', () => expect(simulateHintingCompletions('"', { quotes: false })?.length).toBeGreaterThan(0)) +it('types " should hint completions', () => expect(simulateHintingCompletions('""', { quotes: false })?.length).toBeGreaterThan(0)) it('types should hint completions', () => expect(simulateHintingCompletions('text:center ')?.length).toBeGreaterThan(0)) it('types "text:center" should not hint completions', () => expect(simulateHintingCompletions('text:center')?.length).toBe(0)) test.todo('types any trigger character in "" should not hint') test.todo(`types any trigger character in '' should not hint`) -test.todo('keys') -// describe('keys', () => { -// // it('should not hint selectors', () => expect(simulateHintingCompletions('text:')?.[0]).not.toMatchObject({ insertText: 'active' })) -// test('@delay on invoked', () => expect(simulateHintingCompletions('"', { quotes: false })?.find(({ label }) => label === '@delay:')).toMatchObject({ label: '@delay:' })) -// test('~delay on invoked', () => expect(simulateHintingCompletions('"', { quotes: false })?.find(({ label }) => label === '~delay:')).toMatchObject({ label: '~delay:' })) -// it('starts with @', () => expect(simulateHintingCompletions('@')?.[0]).toMatchObject({ label: 'delay:' })) -// it('starts with @d and list related', () => expect(simulateHintingCompletions('@d')?.map(({ label }) => label)).toEqual([ -// 'delay:', -// 'direction:', -// 'duration:' -// ])) -// it('starts with @ and list related', () => expect(simulateHintingCompletions('@')?.map(({ label }) => label)).toEqual([ -// 'delay:', -// 'direction:', -// 'duration:', -// 'easing:', -// 'fill-mode:', -// 'iteration-count:', -// 'name:', -// 'play-state:', -// ])) -// it('starts with ~', () => expect(simulateHintingCompletions('~')?.[0]).toMatchObject({ label: 'delay:' })) -// it('starts with ~ and list related', () => expect(simulateHintingCompletions('~')?.map(({ label }) => label)).toEqual([ -// 'delay:', -// 'duration:', -// 'easing:', -// 'property:' -// ])) -// }) +describe('keys', () => { + it('should not hint selectors', () => expect(simulateHintingCompletions('text:')?.[0]).not.toMatchObject({ insertText: 'active' })) + test('@delay on invoked', () => expect(simulateHintingCompletions('""', { quotes: false })?.find(({ label }) => label === '@delay:')).toMatchObject({ label: '@delay:' })) + test('~delay on invoked', () => expect(simulateHintingCompletions('""', { quotes: false })?.find(({ label }) => label === '~delay:')).toMatchObject({ label: '~delay:' })) + it('starts with @', () => expect(simulateHintingCompletions('@')?.[0]).toMatchObject({ label: 'delay:' })) + it('starts with @d and list related', () => expect(simulateHintingCompletions('@d')?.map(({ label }) => label)).toEqual([ + 'delay:', + 'direction:', + 'duration:' + ])) + it('starts with @ and list related', () => expect(simulateHintingCompletions('@')?.map(({ label }) => label)).toEqual([ + 'delay:', + 'direction:', + 'duration:', + 'easing:', + 'fill:', + 'iteration:', + 'name:', + 'play:', + ])) + it('starts with ~', () => expect(simulateHintingCompletions('~')?.[0]).toMatchObject({ label: 'delay:' })) + it('starts with ~ and list related', () => expect(simulateHintingCompletions('~')?.map(({ label }) => label)).toEqual([ + 'delay:', + 'duration:', + 'easing:', + 'property:' + ])) + test('native property', () => expect(simulateHintingCompletions('f')?.map(({ label }) => label)).toContain('font-size:')) + describe('ambiguous', () => { + test('t', () => expect(simulateHintingCompletions('t')?.map(({ label }) => label)).toContain('t:')) + test('t', () => expect(simulateHintingCompletions('t')?.map(({ label }) => label)).toContain('text:')) + }) +}) + +describe('values', () => { + test('scoped variables', () => expect(simulateHintingCompletions('font:')?.map(({ label }) => label)).toContain('semibold')) + describe('ambiguous', () => { + test('text:capitalize', () => expect(simulateHintingCompletions('text:')?.map(({ label }) => label)).toContain('capitalize')) + test('text:center', () => expect(simulateHintingCompletions('text:')?.map(({ label }) => label)).toContain('center')) + }) +}) describe('utilities', () => { it('types a', () => expect(simulateHintingCompletions('a')?.find(({ label }) => label === 'abs')).toMatchObject({ label: 'abs' }))