diff --git a/packages/tailwindcss-language-service/src/util/color.ts b/packages/tailwindcss-language-service/src/util/color.ts index 2adb5c84..3c59f437 100644 --- a/packages/tailwindcss-language-service/src/util/color.ts +++ b/packages/tailwindcss-language-service/src/util/color.ts @@ -56,7 +56,7 @@ const colorRegex = new RegExp( 'gi', ) -function getColorsInString(str: string): (culori.Color | KeywordColor)[] { +function getColorsInString(state: State, str: string): (culori.Color | KeywordColor)[] { if (/(?:box|drop)-shadow/.test(str)) return [] function toColor(match: RegExpMatchArray) { @@ -64,7 +64,7 @@ function getColorsInString(str: string): (culori.Color | KeywordColor)[] { return getKeywordColor(color) ?? culori.parse(color) } - str = replaceCssVarsWithFallbacks(str) + str = replaceCssVarsWithFallbacks(state, str) str = removeColorMixWherePossible(str) let possibleColors = str.matchAll(colorRegex) @@ -73,6 +73,7 @@ function getColorsInString(str: string): (culori.Color | KeywordColor)[] { } function getColorFromDecls( + state: State, decls: Record, ): culori.Color | KeywordColor | null { let props = Object.keys(decls).filter((prop) => { @@ -99,7 +100,9 @@ function getColorFromDecls( const propsToCheck = areAllCustom ? props : nonCustomProps - const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString)) + const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap((str) => { + return getColorsInString(state, str) + })) // check that all of the values are valid colors // if (colors.some((color) => color instanceof TinyColor && !color.isValid)) { @@ -170,7 +173,7 @@ function getColorFromRoot(state: State, css: postcss.Root): culori.Color | Keywo decls[decl.prop].push(decl.value) }) - return getColorFromDecls(decls) + return getColorFromDecls(state, decls) } export function getColor(state: State, className: string): culori.Color | KeywordColor | null { @@ -186,7 +189,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor if (state.classNames) { const item = dlv(state.classNames.classNames, [className, '__info']) if (item && item.__rule) { - return getColorFromDecls(removeMeta(item)) + return getColorFromDecls(state, removeMeta(item)) } } @@ -215,7 +218,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor decls[decl.prop] = decl.value } }) - return getColorFromDecls(decls) + return getColorFromDecls(state, decls) } let parts = getClassNameParts(state, className) @@ -224,7 +227,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor const item = dlv(state.classNames.classNames, [...parts, '__info']) if (!item.__rule) return null - return getColorFromDecls(removeMeta(item)) + return getColorFromDecls(state, removeMeta(item)) } export function getColorFromValue(value: unknown): culori.Color | KeywordColor | null { diff --git a/packages/tailwindcss-language-service/src/util/css-vars.test.ts b/packages/tailwindcss-language-service/src/util/css-vars.test.ts index e1a85078..3b531fd4 100644 --- a/packages/tailwindcss-language-service/src/util/css-vars.test.ts +++ b/packages/tailwindcss-language-service/src/util/css-vars.test.ts @@ -1,26 +1,56 @@ import { expect, test } from 'vitest' import { replaceCssVarsWithFallbacks } from './css-vars' +import { State } from './state' +import { DesignSystem } from './v4' test('replacing CSS variables with their fallbacks (when they have them)', () => { - expect(replaceCssVarsWithFallbacks('var(--foo, red)')).toBe(' red') - expect(replaceCssVarsWithFallbacks('var(--foo, )')).toBe(' ') + let map = new Map([ + ['--known', 'blue'], + ]) - expect(replaceCssVarsWithFallbacks('rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)') - expect(replaceCssVarsWithFallbacks('rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))') + let state: State = { + enabled: true, + designSystem: { + resolveThemeValue: (name) => map.get(name) ?? null, + } as DesignSystem, + } + + expect(replaceCssVarsWithFallbacks(state, 'var(--foo, red)')).toBe(' red') + expect(replaceCssVarsWithFallbacks(state, 'var(--foo, )')).toBe(' ') + + expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)') + expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))') expect( - replaceCssVarsWithFallbacks('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))'), + replaceCssVarsWithFallbacks( + state, + 'rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))', + ), ).toBe('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))') expect( replaceCssVarsWithFallbacks( + state, 'rgb(var(--one, var(--bar, var(--baz), var(--qux), var(--thing))))', ), ).toBe('rgb( var(--baz), var(--qux), var(--thing))') expect( replaceCssVarsWithFallbacks( + state, 'color-mix(in srgb, var(--color-primary, oklch(0 0 0 / 2.5)), var(--color-secondary, oklch(0 0 0 / 2.5)), 50%)', ), ).toBe('color-mix(in srgb, oklch(0 0 0 / 2.5), oklch(0 0 0 / 2.5), 50%)') + + // Known theme keys are replaced with their values + expect(replaceCssVarsWithFallbacks(state, 'var(--known)')).toBe('blue') + + // Values from the theme take precedence over fallbacks + expect(replaceCssVarsWithFallbacks(state, 'var(--known, red)')).toBe('blue') + + // Unknown theme keys use a fallback if provided + expect(replaceCssVarsWithFallbacks(state, 'var(--unknown, red)')).toBe(' red') + + // Unknown theme keys without fallbacks are not replaced + expect(replaceCssVarsWithFallbacks(state, 'var(--unknown)')).toBe('var(--unknown)') }) diff --git a/packages/tailwindcss-language-service/src/util/css-vars.ts b/packages/tailwindcss-language-service/src/util/css-vars.ts index ec6803dd..032153fe 100644 --- a/packages/tailwindcss-language-service/src/util/css-vars.ts +++ b/packages/tailwindcss-language-service/src/util/css-vars.ts @@ -1,4 +1,27 @@ -export function replaceCssVarsWithFallbacks(str: string): string { +import type { State } from './state' + +export function replaceCssVarsWithFallbacks(state: State, str: string): string { + return replaceCssVars(str, (name, fallback) => { + // Replace with the value from the design system first. The design system + // take precedences over other sources as that emulates the behavior of a + // browser where the fallback is only used if the variable is defined. + if (state.designSystem && name.startsWith('--')) { + let value = state.designSystem.resolveThemeValue?.(name) ?? null + if (value !== null) return value + } + + if (fallback) { + return fallback + } + + // Don't touch it since there's no suitable replacement + return null + }) +} + +type CssVarReplacer = (name: string, fallback: string | null) => string | null + +function replaceCssVars(str: string, replace: CssVarReplacer): string { for (let i = 0; i < str.length; ++i) { if (!str.startsWith('var(', i)) continue @@ -13,13 +36,31 @@ export function replaceCssVarsWithFallbacks(str: string): string { } else if (str[j] === ',' && depth === 0 && fallbackStart === null) { fallbackStart = j + 1 } else if (str[j] === ')' && depth === 0) { + let varName: string + let fallback: string | null + if (fallbackStart === null) { - i = j + 1 + varName = str.slice(i + 4, j) + fallback = null + } else { + varName = str.slice(i + 4, fallbackStart - 1) + fallback = str.slice(fallbackStart, j) + } + + let replacement = replace(varName, fallback) + + if (replacement !== null) { + str = str.slice(0, i) + replacement + str.slice(j + 1) + + // We don't want to skip past anything here because `replacement` + // might contain more var(…) calls in which case `i` will already + // be pointing at the right spot to start looking for them break } - let fallbackEnd = j - str = str.slice(0, i) + str.slice(fallbackStart, fallbackEnd) + str.slice(j + 1) + // It can't be replaced so we can avoid unncessary work by skipping over + // the entire var(…) call. + i = j + 1 break } } diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index 75da6316..ca2f9ad2 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -37,4 +37,7 @@ export interface DesignSystem { export interface DesignSystem { compile(classes: string[]): postcss.Root[] toCss(nodes: postcss.Root | postcss.Node[]): string + + // Optional because it did not exist in earlier v4 alpha versions + resolveThemeValue?(path: string): string | undefined } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 612a7268..dd0acd52 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -7,6 +7,7 @@ - Support loading TypeScript configs and plugins in v4 projects ([#1076](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1076)) - Show colors for logical border properties ([#1075](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1075)) - Show all potential class conflicts in v4 projects ([#1077](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1077)) +- Lookup variables in the CSS theme ([#1082](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1082)) ## 0.12.12