diff --git a/packages/tailwindcss-language-server/tests/colors/colors.test.js b/packages/tailwindcss-language-server/tests/colors/colors.test.js
index 317d167a..2a584591 100644
--- a/packages/tailwindcss-language-server/tests/colors/colors.test.js
+++ b/packages/tailwindcss-language-server/tests/colors/colors.test.js
@@ -1,5 +1,13 @@
import { test, expect } from 'vitest'
-import { withFixture } from '../common'
+import { init, withFixture } from '../common'
+import { css, defineTest } from '../../src/testing'
+import { DocumentColorRequest } from 'vscode-languageserver'
+
+const color = (red, green, blue, alpha) => ({ red, green, blue, alpha })
+const range = (startLine, startCol, endLine, endCol) => ({
+ start: { line: startLine, character: startCol },
+ end: { line: endLine, character: endCol },
+})
withFixture('basic', (c) => {
async function testColors(name, { text, expected }) {
@@ -300,3 +308,48 @@ withFixture('v4/basic', (c) => {
],
})
})
+
+defineTest({
+ name: 'v4: colors are recursively resolved from the theme',
+ fs: {
+ 'app.css': css`
+ @import 'tailwindcss';
+ @theme {
+ --color-*: initial;
+ --color-primary: #ff0000;
+ --color-level-1: var(--color-primary);
+ --color-level-2: var(--color-level-1);
+ --color-level-3: var(--color-level-2);
+ --color-level-4: var(--color-level-3);
+ --color-level-5: var(--color-level-4);
+ }
+ `,
+ },
+ prepare: async ({ root }) => ({ c: await init(root) }),
+ handle: async ({ c }) => {
+ let textDocument = await c.openDocument({
+ lang: 'html',
+ text: '
',
+ })
+
+ expect(c.project).toMatchObject({
+ tailwind: {
+ version: '4.0.0',
+ isDefaultVersion: true,
+ },
+ })
+
+ let colors = await c.sendRequest(DocumentColorRequest.type, {
+ textDocument,
+ })
+
+ expect(colors).toEqual([
+ { range: range(0, 12, 0, 22), color: color(1, 0, 0, 1) },
+ { range: range(0, 23, 0, 33), color: color(1, 0, 0, 1) },
+ { range: range(0, 34, 0, 44), color: color(1, 0, 0, 1) },
+ { range: range(0, 45, 0, 55), color: color(1, 0, 0, 1) },
+ { range: range(0, 56, 0, 66), color: color(1, 0, 0, 1) },
+ { range: range(0, 67, 0, 77), color: color(1, 0, 0, 1) },
+ ])
+ },
+})
diff --git a/packages/tailwindcss-language-service/src/util/rewriting/add-theme-values.ts b/packages/tailwindcss-language-service/src/util/rewriting/add-theme-values.ts
index ad167eba..b0f30891 100644
--- a/packages/tailwindcss-language-service/src/util/rewriting/add-theme-values.ts
+++ b/packages/tailwindcss-language-service/src/util/rewriting/add-theme-values.ts
@@ -14,16 +14,18 @@ export function addThemeValues(css: string, state: State, settings: TailwindCssS
let replaced: Range[] = []
css = replaceCssCalc(css, (expr) => {
- let inlined = replaceCssVars(expr.value, ({ name }) => {
- if (!name.startsWith('--')) return null
+ let inlined = replaceCssVars(expr.value, {
+ replace({ name }) {
+ if (!name.startsWith('--')) return null
- let value = resolveVariableValue(state.designSystem, name)
- if (value === null) return null
+ let value = resolveVariableValue(state.designSystem, name)
+ if (value === null) return null
- // Inline CSS calc expressions in theme values
- value = replaceCssCalc(value, (expr) => evaluateExpression(expr.value))
+ // Inline CSS calc expressions in theme values
+ value = replaceCssCalc(value, (expr) => evaluateExpression(expr.value))
- return value
+ return value
+ },
})
let evaluated = evaluateExpression(inlined)
@@ -62,53 +64,56 @@ export function addThemeValues(css: string, state: State, settings: TailwindCssS
return null
})
- css = replaceCssVars(css, ({ name, range }) => {
- if (!name.startsWith('--')) return null
+ css = replaceCssVars(css, {
+ recursive: false,
+ replace({ name, range }) {
+ if (!name.startsWith('--')) return null
+
+ for (let r of replaced) {
+ if (r.start <= range.start && r.end >= range.end) {
+ return null
+ }
+ }
+
+ let value = resolveVariableValue(state.designSystem, name)
+ if (value === null) return null
+
+ let px = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
+ if (px !== value) {
+ comments.push({
+ index: range.end + 1,
+ value: `${value} = ${px}`,
+ })
- for (let r of replaced) {
- if (r.start <= range.start && r.end >= range.end) {
return null
}
- }
- let value = resolveVariableValue(state.designSystem, name)
- if (value === null) return null
+ let color = getEquivalentColor(value)
+ if (color !== value) {
+ comments.push({
+ index: range.end + 1,
+ value: `${value} = ${color}`,
+ })
- let px = addPixelEquivalentsToValue(value, settings.rootFontSize, false)
- if (px !== value) {
- comments.push({
- index: range.end + 1,
- value: `${value} = ${px}`,
- })
+ return null
+ }
- return null
- }
+ // Inline CSS calc expressions in theme values
+ value = replaceCssCalc(value, (expr) => {
+ let evaluated = evaluateExpression(expr.value)
+ if (!evaluated) return null
+ if (evaluated === expr.value) return null
+
+ return `calc(${expr.value}) ≈ ${evaluated}`
+ })
- let color = getEquivalentColor(value)
- if (color !== value) {
comments.push({
index: range.end + 1,
- value: `${value} = ${color}`,
+ value,
})
return null
- }
-
- // Inline CSS calc expressions in theme values
- value = replaceCssCalc(value, (expr) => {
- let evaluated = evaluateExpression(expr.value)
- if (!evaluated) return null
- if (evaluated === expr.value) return null
-
- return `calc(${expr.value}) ≈ ${evaluated}`
- })
-
- comments.push({
- index: range.end + 1,
- value,
- })
-
- return null
+ },
})
return applyComments(css, comments)
diff --git a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts
index 67fee890..6eb840ef 100644
--- a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts
+++ b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts
@@ -9,11 +9,23 @@ import { State, TailwindCssSettings } from '../state'
import { DesignSystem } from '../v4'
test('replacing CSS variables with their fallbacks (when they have them)', () => {
- let map = new Map([['--known', 'blue']])
+ let map = new Map([
+ ['--known', 'blue'],
+ ['--level-1', 'var(--known)'],
+ ['--level-2', 'var(--level-1)'],
+ ['--level-3', 'var(--level-2)'],
+
+ ['--circular-1', 'var(--circular-3)'],
+ ['--circular-2', 'var(--circular-1)'],
+ ['--circular-3', 'var(--circular-2)'],
+
+ ['--escaped\\,name', 'green'],
+ ])
let state: State = {
enabled: true,
designSystem: {
+ theme: { prefix: null } as any,
resolveThemeValue: (name) => map.get(name) ?? null,
} as DesignSystem,
}
@@ -48,6 +60,9 @@ test('replacing CSS variables with their fallbacks (when they have them)', () =>
// Known theme keys are replaced with their values
expect(replaceCssVarsWithFallbacks(state, 'var(--known)')).toBe('blue')
+ // Escaped commas are not treated as separators
+ expect(replaceCssVarsWithFallbacks(state, 'var(--escaped\\,name)')).toBe('green')
+
// Values from the theme take precedence over fallbacks
expect(replaceCssVarsWithFallbacks(state, 'var(--known, red)')).toBe('blue')
@@ -56,6 +71,17 @@ test('replacing CSS variables with their fallbacks (when they have them)', () =>
// Unknown theme keys without fallbacks are not replaced
expect(replaceCssVarsWithFallbacks(state, 'var(--unknown)')).toBe('var(--unknown)')
+
+ // Fallbacks are replaced recursively
+ expect(replaceCssVarsWithFallbacks(state, 'var(--unknown,var(--unknown-2,red))')).toBe('red')
+ expect(replaceCssVarsWithFallbacks(state, 'var(--level-1)')).toBe('blue')
+ expect(replaceCssVarsWithFallbacks(state, 'var(--level-2)')).toBe('blue')
+ expect(replaceCssVarsWithFallbacks(state, 'var(--level-3)')).toBe('blue')
+
+ // Circular replacements don't cause infinite loops
+ expect(replaceCssVarsWithFallbacks(state, 'var(--circular-1)')).toBe('var(--circular-3)')
+ expect(replaceCssVarsWithFallbacks(state, 'var(--circular-2)')).toBe('var(--circular-1)')
+ expect(replaceCssVarsWithFallbacks(state, 'var(--circular-3)')).toBe('var(--circular-2)')
})
test('Evaluating CSS calc expressions', () => {
@@ -80,6 +106,7 @@ test('Inlining calc expressions using the design system', () => {
let state: State = {
enabled: true,
designSystem: {
+ theme: { prefix: null } as any,
resolveThemeValue: (name) => map.get(name) ?? null,
} as DesignSystem,
}
diff --git a/packages/tailwindcss-language-service/src/util/rewriting/inline-theme-values.ts b/packages/tailwindcss-language-service/src/util/rewriting/inline-theme-values.ts
index 69010c11..c002ab9c 100644
--- a/packages/tailwindcss-language-service/src/util/rewriting/inline-theme-values.ts
+++ b/packages/tailwindcss-language-service/src/util/rewriting/inline-theme-values.ts
@@ -8,25 +8,29 @@ export function inlineThemeValues(css: string, state: State) {
if (!state.designSystem) return css
css = replaceCssCalc(css, (expr) => {
- let inlined = replaceCssVars(expr.value, ({ name, fallback }) => {
- if (!name.startsWith('--')) return null
+ let inlined = replaceCssVars(expr.value, {
+ replace({ name, fallback }) {
+ if (!name.startsWith('--')) return null
- let value = resolveVariableValue(state.designSystem, name)
- if (value === null) return fallback
+ let value = resolveVariableValue(state.designSystem, name)
+ if (value === null) return fallback
- return value
+ return value
+ },
})
return evaluateExpression(inlined)
})
- css = replaceCssVars(css, ({ name, fallback }) => {
- if (!name.startsWith('--')) return null
+ css = replaceCssVars(css, {
+ replace({ name, fallback }) {
+ if (!name.startsWith('--')) return null
- let value = resolveVariableValue(state.designSystem, name)
- if (value === null) return fallback
+ let value = resolveVariableValue(state.designSystem, name)
+ if (value === null) return fallback
- return value
+ return value
+ },
})
return css
diff --git a/packages/tailwindcss-language-service/src/util/rewriting/replacements.ts b/packages/tailwindcss-language-service/src/util/rewriting/replacements.ts
index dd3d0bb4..df63e1ae 100644
--- a/packages/tailwindcss-language-service/src/util/rewriting/replacements.ts
+++ b/packages/tailwindcss-language-service/src/util/rewriting/replacements.ts
@@ -16,12 +16,31 @@ export interface Range {
end: number
}
+export interface ReplacerOptions {
+ /**
+ * Whether or not the replacement should be performed recursively
+ *
+ * default: true
+ */
+ recursive?: boolean
+
+ /**
+ * How to replace the CSS variable
+ */
+ replace: CssVarReplacer
+}
+
export type CssVarReplacer = (node: CssVariable) => string | null
/**
* Replace all var expressions in a string using the replacer function
*/
-export function replaceCssVars(str: string, replace: CssVarReplacer): string {
+export function replaceCssVars(
+ str: string,
+ { replace, recursive = true }: ReplacerOptions,
+): string {
+ let seen = new Set()
+
for (let i = 0; i < str.length; ++i) {
if (!str.startsWith('var(', i)) continue
@@ -33,6 +52,8 @@ export function replaceCssVars(str: string, replace: CssVarReplacer): string {
depth++
} else if (str[j] === ')' && depth > 0) {
depth--
+ } else if (str[j] === '\\') {
+ j++
} else if (str[j] === ',' && depth === 0 && fallbackStart === null) {
fallbackStart = j + 1
} else if (str[j] === ')' && depth === 0) {
@@ -58,9 +79,20 @@ export function replaceCssVars(str: string, replace: CssVarReplacer): string {
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
+ // Move the index back one so it can look at the spot again since it'll
+ // be incremented by the outer loop. However, since we're replacing
+ // variables recursively we might end up in a loop so we need to keep
+ // track of which variables we've already seen and where they were
+ // replaced to avoid infinite loops.
+ if (recursive) {
+ let key = `${i}:${replacement}`
+
+ if (!seen.has(key)) {
+ seen.add(key)
+ i -= 1
+ }
+ }
+
break
}
}
diff --git a/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts b/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts
index b2d5d7ce..728b53bf 100644
--- a/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts
+++ b/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts
@@ -3,20 +3,22 @@ import { resolveVariableValue } from './lookup'
import { replaceCssVars } from './replacements'
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 = resolveVariableValue(state.designSystem, name)
- if (value !== null) return value
- }
+ return replaceCssVars(str, {
+ replace({ 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 = resolveVariableValue(state.designSystem, name)
+ if (value !== null) return value
+ }
- if (fallback) {
- return fallback
- }
+ if (fallback) {
+ return fallback
+ }
- // Don't touch it since there's no suitable replacement
- return null
+ // Don't touch it since there's no suitable replacement
+ return null
+ },
})
}
diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md
index ca62ab97..fc7e2ab7 100644
--- a/packages/vscode-tailwindcss/CHANGELOG.md
+++ b/packages/vscode-tailwindcss/CHANGELOG.md
@@ -12,6 +12,9 @@
- Make sure `@slot` isn't considered an unknown at-rule ([#1165](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1165))
- Fix equivalent calculation when using prefixes in v4 ([#1166](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1166))
- Fix use of `tailwindCSS.experimental.configFile` option when using the bundled version of v4 ([#1167](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1167))
+- Recursively resolve values from the theme ([#1168](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1168))
+- Handle theme keys containing escaped commas ([#1168](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1168))
+- Show colors for utilities when they point to CSS variables contained in the theme ([#1168](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1168))
## 0.14.2