Skip to content

Commit 3014df5

Browse files
Lookup variables in the CSS theme (#1082)
1 parent 712b5ff commit 3014df5

File tree

5 files changed

+94
-16
lines changed

5 files changed

+94
-16
lines changed

packages/tailwindcss-language-service/src/util/color.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ const colorRegex = new RegExp(
5656
'gi',
5757
)
5858

59-
function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
59+
function getColorsInString(state: State, str: string): (culori.Color | KeywordColor)[] {
6060
if (/(?:box|drop)-shadow/.test(str)) return []
6161

6262
function toColor(match: RegExpMatchArray) {
6363
let color = match[1].replace(/var\([^)]+\)/, '1')
6464
return getKeywordColor(color) ?? culori.parse(color)
6565
}
6666

67-
str = replaceCssVarsWithFallbacks(str)
67+
str = replaceCssVarsWithFallbacks(state, str)
6868
str = removeColorMixWherePossible(str)
6969

7070
let possibleColors = str.matchAll(colorRegex)
@@ -73,6 +73,7 @@ function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
7373
}
7474

7575
function getColorFromDecls(
76+
state: State,
7677
decls: Record<string, string | string[]>,
7778
): culori.Color | KeywordColor | null {
7879
let props = Object.keys(decls).filter((prop) => {
@@ -99,7 +100,9 @@ function getColorFromDecls(
99100

100101
const propsToCheck = areAllCustom ? props : nonCustomProps
101102

102-
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap(getColorsInString))
103+
const colors = propsToCheck.flatMap((prop) => ensureArray(decls[prop]).flatMap((str) => {
104+
return getColorsInString(state, str)
105+
}))
103106

104107
// check that all of the values are valid colors
105108
// if (colors.some((color) => color instanceof TinyColor && !color.isValid)) {
@@ -170,7 +173,7 @@ function getColorFromRoot(state: State, css: postcss.Root): culori.Color | Keywo
170173
decls[decl.prop].push(decl.value)
171174
})
172175

173-
return getColorFromDecls(decls)
176+
return getColorFromDecls(state, decls)
174177
}
175178

176179
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
186189
if (state.classNames) {
187190
const item = dlv(state.classNames.classNames, [className, '__info'])
188191
if (item && item.__rule) {
189-
return getColorFromDecls(removeMeta(item))
192+
return getColorFromDecls(state, removeMeta(item))
190193
}
191194
}
192195

@@ -215,7 +218,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
215218
decls[decl.prop] = decl.value
216219
}
217220
})
218-
return getColorFromDecls(decls)
221+
return getColorFromDecls(state, decls)
219222
}
220223

221224
let parts = getClassNameParts(state, className)
@@ -224,7 +227,7 @@ export function getColor(state: State, className: string): culori.Color | Keywor
224227
const item = dlv(state.classNames.classNames, [...parts, '__info'])
225228
if (!item.__rule) return null
226229

227-
return getColorFromDecls(removeMeta(item))
230+
return getColorFromDecls(state, removeMeta(item))
228231
}
229232

230233
export function getColorFromValue(value: unknown): culori.Color | KeywordColor | null {
Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,56 @@
11
import { expect, test } from 'vitest'
22
import { replaceCssVarsWithFallbacks } from './css-vars'
3+
import { State } from './state'
4+
import { DesignSystem } from './v4'
35

46
test('replacing CSS variables with their fallbacks (when they have them)', () => {
5-
expect(replaceCssVarsWithFallbacks('var(--foo, red)')).toBe(' red')
6-
expect(replaceCssVarsWithFallbacks('var(--foo, )')).toBe(' ')
7+
let map = new Map<string, string>([
8+
['--known', 'blue'],
9+
])
710

8-
expect(replaceCssVarsWithFallbacks('rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
9-
expect(replaceCssVarsWithFallbacks('rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')
11+
let state: State = {
12+
enabled: true,
13+
designSystem: {
14+
resolveThemeValue: (name) => map.get(name) ?? null,
15+
} as DesignSystem,
16+
}
17+
18+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, red)')).toBe(' red')
19+
expect(replaceCssVarsWithFallbacks(state, 'var(--foo, )')).toBe(' ')
20+
21+
expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, 255 0 0))')).toBe('rgb( 255 0 0)')
22+
expect(replaceCssVarsWithFallbacks(state, 'rgb(var(--foo, var(--bar)))')).toBe('rgb( var(--bar))')
1023

1124
expect(
12-
replaceCssVarsWithFallbacks('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))'),
25+
replaceCssVarsWithFallbacks(
26+
state,
27+
'rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))',
28+
),
1329
).toBe('rgb(var(var(--bar, var(--baz), var(--qux), var(--thing))))')
1430

1531
expect(
1632
replaceCssVarsWithFallbacks(
33+
state,
1734
'rgb(var(--one, var(--bar, var(--baz), var(--qux), var(--thing))))',
1835
),
1936
).toBe('rgb( var(--baz), var(--qux), var(--thing))')
2037

2138
expect(
2239
replaceCssVarsWithFallbacks(
40+
state,
2341
'color-mix(in srgb, var(--color-primary, oklch(0 0 0 / 2.5)), var(--color-secondary, oklch(0 0 0 / 2.5)), 50%)',
2442
),
2543
).toBe('color-mix(in srgb, oklch(0 0 0 / 2.5), oklch(0 0 0 / 2.5), 50%)')
44+
45+
// Known theme keys are replaced with their values
46+
expect(replaceCssVarsWithFallbacks(state, 'var(--known)')).toBe('blue')
47+
48+
// Values from the theme take precedence over fallbacks
49+
expect(replaceCssVarsWithFallbacks(state, 'var(--known, red)')).toBe('blue')
50+
51+
// Unknown theme keys use a fallback if provided
52+
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown, red)')).toBe(' red')
53+
54+
// Unknown theme keys without fallbacks are not replaced
55+
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown)')).toBe('var(--unknown)')
2656
})

packages/tailwindcss-language-service/src/util/css-vars.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
1-
export function replaceCssVarsWithFallbacks(str: string): string {
1+
import type { State } from './state'
2+
3+
export function replaceCssVarsWithFallbacks(state: State, str: string): string {
4+
return replaceCssVars(str, (name, fallback) => {
5+
// Replace with the value from the design system first. The design system
6+
// take precedences over other sources as that emulates the behavior of a
7+
// browser where the fallback is only used if the variable is defined.
8+
if (state.designSystem && name.startsWith('--')) {
9+
let value = state.designSystem.resolveThemeValue?.(name) ?? null
10+
if (value !== null) return value
11+
}
12+
13+
if (fallback) {
14+
return fallback
15+
}
16+
17+
// Don't touch it since there's no suitable replacement
18+
return null
19+
})
20+
}
21+
22+
type CssVarReplacer = (name: string, fallback: string | null) => string | null
23+
24+
function replaceCssVars(str: string, replace: CssVarReplacer): string {
225
for (let i = 0; i < str.length; ++i) {
326
if (!str.startsWith('var(', i)) continue
427

@@ -13,13 +36,31 @@ export function replaceCssVarsWithFallbacks(str: string): string {
1336
} else if (str[j] === ',' && depth === 0 && fallbackStart === null) {
1437
fallbackStart = j + 1
1538
} else if (str[j] === ')' && depth === 0) {
39+
let varName: string
40+
let fallback: string | null
41+
1642
if (fallbackStart === null) {
17-
i = j + 1
43+
varName = str.slice(i + 4, j)
44+
fallback = null
45+
} else {
46+
varName = str.slice(i + 4, fallbackStart - 1)
47+
fallback = str.slice(fallbackStart, j)
48+
}
49+
50+
let replacement = replace(varName, fallback)
51+
52+
if (replacement !== null) {
53+
str = str.slice(0, i) + replacement + str.slice(j + 1)
54+
55+
// We don't want to skip past anything here because `replacement`
56+
// might contain more var(…) calls in which case `i` will already
57+
// be pointing at the right spot to start looking for them
1858
break
1959
}
2060

21-
let fallbackEnd = j
22-
str = str.slice(0, i) + str.slice(fallbackStart, fallbackEnd) + str.slice(j + 1)
61+
// It can't be replaced so we can avoid unncessary work by skipping over
62+
// the entire var(…) call.
63+
i = j + 1
2364
break
2465
}
2566
}

packages/tailwindcss-language-service/src/util/v4/design-system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ export interface DesignSystem {
3737
export interface DesignSystem {
3838
compile(classes: string[]): postcss.Root[]
3939
toCss(nodes: postcss.Root | postcss.Node[]): string
40+
41+
// Optional because it did not exist in earlier v4 alpha versions
42+
resolveThemeValue?(path: string): string | undefined
4043
}

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Support loading TypeScript configs and plugins in v4 projects ([#1076](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1076))
88
- Show colors for logical border properties ([#1075](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1075))
99
- Show all potential class conflicts in v4 projects ([#1077](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1077))
10+
- Lookup variables in the CSS theme ([#1082](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1082))
1011

1112
## 0.12.12
1213

0 commit comments

Comments
 (0)