Skip to content

Commit 37c4b96

Browse files
Show pixel equivalents in completions and hovers of the theme() helper (#935)
* Refactor * Don’t wrap string theme values in quotes * Transform shown theme values when possible This internal helper is what Tailwind uses to emit the value into the CSS. * Show pixel equivlents for theme values * Add spaces to equivalents comments * Update changelog
1 parent 381d70a commit 37c4b96

File tree

12 files changed

+95
-44
lines changed

12 files changed

+95
-44
lines changed

Diff for: packages/tailwindcss-language-server/src/projects.ts

+10
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ export async function createProjectService(
415415
let pluginVersions: string | undefined
416416
let browserslist: string[] | undefined
417417
let resolveConfigFn: (config: any) => any
418+
let transformThemeValueFn: (section: any) => (value: any) => any
418419
let loadConfigFn: (path: string) => any
419420
let featureFlags: FeatureFlags = { future: [], experimental: [] }
420421
let applyComplexClasses: any
@@ -513,6 +514,13 @@ export async function createProjectService(
513514
}
514515
}
515516

517+
try {
518+
let fn = require(resolveFrom(tailwindDir, './lib/util/transformThemeValue.js'))
519+
transformThemeValueFn = fn.default ?? fn
520+
} catch {
521+
//
522+
}
523+
516524
try {
517525
loadConfigFn = require(resolveFrom(tailwindDir, './loadConfig.js'))
518526
} catch {}
@@ -633,6 +641,7 @@ export async function createProjectService(
633641
console.error(util.format(error))
634642
tailwindcss = require('tailwindcss')
635643
resolveConfigFn = require('tailwindcss/resolveConfig')
644+
transformThemeValueFn = require('tailwindcss/lib/util/transformThemeValue').default
636645
loadConfigFn = require('tailwindcss/loadConfig')
637646
postcss = require('postcss')
638647
tailwindcssVersion = require('tailwindcss/package.json').version
@@ -660,6 +669,7 @@ export async function createProjectService(
660669
postcssSelectorParser: { module: postcssSelectorParser },
661670
resolveConfig: { module: resolveConfigFn },
662671
loadConfig: { module: loadConfigFn },
672+
transformThemeValue: { module: transformThemeValueFn },
663673
jit: jitModules,
664674
}
665675
state.browserslist = browserslist

Diff for: packages/tailwindcss-language-server/tests/completions/completions.test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ withFixture('basic', (c) => {
222222

223223
expect(resolved).toEqual({
224224
...item,
225-
detail: 'font-size: 0.875rem/* 14px */; line-height: 1.25rem/* 20px */;',
225+
detail: 'font-size: 0.875rem /* 14px */; line-height: 1.25rem /* 20px */;',
226226
documentation: {
227227
kind: 'markdown',
228228
value:
229-
'```css\n.text-sm {\n font-size: 0.875rem/* 14px */;\n line-height: 1.25rem/* 20px */;\n}\n```',
229+
'```css\n.text-sm {\n font-size: 0.875rem /* 14px */;\n line-height: 1.25rem /* 20px */;\n}\n```',
230230
},
231231
})
232232
})
@@ -254,11 +254,11 @@ withFixture('basic', (c) => {
254254

255255
expect(resolved).toEqual({
256256
...item,
257-
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
257+
detail: 'font-size: 0.875rem /* 8.75px */; line-height: 1.25rem /* 12.5px */;',
258258
documentation: {
259259
kind: 'markdown',
260260
value:
261-
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
261+
'```css\n.text-sm {\n font-size: 0.875rem /* 8.75px */;\n line-height: 1.25rem /* 12.5px */;\n}\n```',
262262
},
263263
})
264264
})
@@ -521,11 +521,11 @@ withFixture('v4/basic', (c) => {
521521

522522
expect(resolved).toEqual({
523523
...item,
524-
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
524+
detail: 'font-size: 0.875rem /* 8.75px */; line-height: 1.25rem /* 12.5px */;',
525525
documentation: {
526526
kind: 'markdown',
527527
value:
528-
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
528+
'```css\n.text-sm {\n font-size: 0.875rem /* 8.75px */;\n line-height: 1.25rem /* 12.5px */;\n}\n```',
529529
},
530530
})
531531
})

Diff for: packages/tailwindcss-language-server/tests/env/multi-config-content.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ withFixture('multi-config-content', (c) => {
1313
contents: {
1414
language: 'css',
1515
value:
16-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity))/* #ff0000 */;\n}',
16+
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity)) /* #ff0000 */;\n}',
1717
},
1818
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
1919
})
@@ -30,7 +30,7 @@ withFixture('multi-config-content', (c) => {
3030
contents: {
3131
language: 'css',
3232
value:
33-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity))/* #0000ff */;\n}',
33+
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity)) /* #0000ff */;\n}',
3434
},
3535
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
3636
})

Diff for: packages/tailwindcss-language-server/tests/env/multi-config.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ withFixture('multi-config', (c) => {
1313
contents: {
1414
language: 'css',
1515
value:
16-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity))/* #ff0000 */;\n}',
16+
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity)) /* #ff0000 */;\n}',
1717
},
1818
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
1919
})
@@ -30,7 +30,7 @@ withFixture('multi-config', (c) => {
3030
contents: {
3131
language: 'css',
3232
value:
33-
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity))/* #0000ff */;\n}',
33+
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity)) /* #0000ff */;\n}',
3434
},
3535
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
3636
})

Diff for: packages/tailwindcss-language-server/tests/hover/hover.test.js

+37-15
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ import { test } from 'vitest'
22
import { withFixture } from '../common'
33

44
withFixture('basic', (c) => {
5-
async function testHover(name, { text, lang, position, expected, expectedRange, settings }) {
5+
async function testHover(name, { text, lang, position, exact = false, expected, expectedRange, settings }) {
66
test.concurrent(name, async ({ expect }) => {
77
let textDocument = await c.openDocument({ text, lang, settings })
88
let res = await c.sendRequest('textDocument/hover', {
99
textDocument,
1010
position,
1111
})
1212

13-
expect(res).toEqual(
14-
expected
15-
? {
16-
contents: {
17-
language: 'css',
18-
value: expected,
19-
},
20-
range: expectedRange,
21-
}
22-
: expected,
23-
)
13+
if (!exact && expected) {
14+
expected = {
15+
contents: {
16+
language: 'css',
17+
value: expected,
18+
},
19+
range: expectedRange,
20+
}
21+
}
22+
23+
expect(res).toEqual(expected)
2424
})
2525
}
2626

@@ -38,7 +38,7 @@ withFixture('basic', (c) => {
3838
expected:
3939
'.bg-red-500 {\n' +
4040
' --tw-bg-opacity: 1;\n' +
41-
' background-color: rgb(239 68 68 / var(--tw-bg-opacity))/* #ef4444 */;\n' +
41+
' background-color: rgb(239 68 68 / var(--tw-bg-opacity)) /* #ef4444 */;\n' +
4242
'}',
4343
expectedRange: {
4444
start: { line: 0, character: 12 },
@@ -59,7 +59,7 @@ withFixture('basic', (c) => {
5959
testHover('arbitrary value with theme function', {
6060
text: '<div class="p-[theme(spacing.4)]">',
6161
position: { line: 0, character: 13 },
62-
expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem/* 16px */;\n' + '}',
62+
expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}',
6363
expectedRange: {
6464
start: { line: 0, character: 12 },
6565
end: { line: 0, character: 32 },
@@ -89,6 +89,28 @@ withFixture('basic', (c) => {
8989
end: { line: 2, character: 18 },
9090
},
9191
})
92+
93+
testHover('showPixelEquivalents works with theme()', {
94+
lang: 'tailwindcss',
95+
text: `.foo { font-size: theme(fontSize.xl) }`,
96+
position: { line: 0, character: 32 },
97+
98+
exact: true,
99+
expected: {
100+
contents: {
101+
kind: 'markdown',
102+
value: [
103+
'```plaintext',
104+
'1.25rem /* 20px */',
105+
'```',
106+
].join('\n'),
107+
},
108+
range: {
109+
start: { line: 0, character: 24 },
110+
end: { line: 0, character: 35 },
111+
}
112+
},
113+
})
92114
})
93115

94116
withFixture('v4/basic', (c) => {
@@ -146,7 +168,7 @@ withFixture('v4/basic', (c) => {
146168
// testHover('arbitrary value with theme function', {
147169
// text: '<div class="p-[theme(spacing.4)]">',
148170
// position: { line: 0, character: 13 },
149-
// expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem/* 16px */;\n' + '}',
171+
// expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}',
150172
// expectedRange: {
151173
// start: { line: 0, character: 12 },
152174
// end: { line: 0, character: 32 },

Diff for: packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ export function validateConfigPath(
2323
base: string[] = [],
2424
): { isValid: true; value: any } | { isValid: false; reason: string; suggestions: string[] } {
2525
let keys = Array.isArray(path) ? path : stringToPath(path)
26-
let value = dlv(state.config, [...base, ...keys])
26+
let fullPath = [...base, ...keys]
27+
let value = dlv(state.config, fullPath)
2728
let suggestions: string[] = []
2829

30+
// This property may not exist in the state object because of compatability with Tailwind Play
31+
let transformThemeValue = state.modules?.transformThemeValue?.module ?? ((_: any) => (value: any) => value)
32+
33+
if (fullPath[0] === 'theme' && fullPath[1]) {
34+
value = transformThemeValue(fullPath[1])(value)
35+
}
36+
2937
function findAlternativePath(): string[] {
3038
let points = combinations('123456789'.substr(0, keys.length - 1)).map((x) =>
3139
x.split('').map((x) => parseInt(x, 10)),

Diff for: packages/tailwindcss-language-service/src/hoverProvider.ts

+24-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as jit from './util/jit'
1010
import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics'
1111
import { isWithinRange } from './util/isWithinRange'
1212
import type { TextDocument } from 'vscode-languageserver-textdocument'
13+
import { addPixelEquivalentsToValue } from './util/pixelEquivalents'
1314

1415
export async function doHover(
1516
state: State,
@@ -18,35 +19,42 @@ export async function doHover(
1819
): Promise<Hover> {
1920
return (
2021
(await provideClassNameHover(state, document, position)) ||
21-
provideCssHelperHover(state, document, position)
22+
(await provideCssHelperHover(state, document, position))
2223
)
2324
}
2425

25-
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
26+
async function provideCssHelperHover(state: State, document: TextDocument, position: Position): Promise<Hover> {
2627
if (!isCssContext(state, document, position)) {
2728
return null
2829
}
2930

31+
const settings = await state.editor.getConfiguration(document.uri)
32+
3033
let helperFns = findHelperFunctionsInRange(document, {
3134
start: { line: position.line, character: 0 },
3235
end: { line: position.line + 1, character: 0 },
3336
})
3437

3538
for (let helperFn of helperFns) {
36-
if (isWithinRange(position, helperFn.ranges.path)) {
37-
let validated = validateConfigPath(
38-
state,
39-
helperFn.path,
40-
helperFn.helper === 'theme' ? ['theme'] : [],
41-
)
42-
let value = validated.isValid ? stringifyConfigValue(validated.value) : null
43-
if (value === null) {
44-
return null
45-
}
46-
return {
47-
contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') },
48-
range: helperFn.ranges.path,
49-
}
39+
if (!isWithinRange(position, helperFn.ranges.path)) continue
40+
41+
let validated = validateConfigPath(
42+
state,
43+
helperFn.path,
44+
helperFn.helper === 'theme' ? ['theme'] : [],
45+
)
46+
47+
// This property may not exist in the state object because of compatability with Tailwind Play
48+
let value = validated.isValid ? stringifyConfigValue(validated.value) : null
49+
if (value === null) return null
50+
51+
if (settings.tailwindCSS.showPixelEquivalents) {
52+
value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize)
53+
}
54+
55+
return {
56+
contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') },
57+
range: helperFn.ranges.path,
5058
}
5159
}
5260

Diff for: packages/tailwindcss-language-service/src/util/comments.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function applyComments(str: string, comments: Comment[]): string {
55

66
for (let comment of comments) {
77
let index = comment.index + offset
8-
let commentStr = `/* ${comment.value} */`
8+
let commentStr = ` /* ${comment.value} */`
99
str = str.slice(0, index) + commentStr + str.slice(index)
1010
offset += commentStr.length
1111
}

Diff for: packages/tailwindcss-language-service/src/util/pixelEquivalents.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function addPixelEquivalentsToValue(value: string, rootFontSize: number):
2020
return false
2121
}
2222

23-
let commentStr = `/* ${parseFloat(unit.number) * rootFontSize}px */`
23+
let commentStr = ` /* ${parseFloat(unit.number) * rootFontSize}px */`
2424
value = value.slice(0, node.sourceEndIndex) + commentStr + value.slice(node.sourceEndIndex)
2525

2626
return false

Diff for: packages/tailwindcss-language-service/src/util/state.ts

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export interface State {
107107
postcss?: { version: string; module: Postcss }
108108
postcssSelectorParser?: { module: any }
109109
resolveConfig?: { module: any }
110+
transformThemeValue?: { module: any }
110111
loadConfig?: { module: any }
111112
jit?: {
112113
generateRules: { module: any }

Diff for: packages/tailwindcss-language-service/src/util/stringify.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { addEquivalents } from './equivalents'
1111
export function stringifyConfigValue(x: any): string {
1212
if (isObject(x)) return `${Object.keys(x).length} values`
1313
if (typeof x === 'function') return 'ƒ'
14+
if (typeof x === 'string') return x
1415
return stringifyObject(x, {
1516
inlineCharacterLimit: Infinity,
1617
singleQuotes: false,

Diff for: packages/vscode-tailwindcss/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Support Astro's `class:list` attribute by default (#890)
77
- Fix hovers and CSS conflict detection in Vue `<style lang="sass">` blocks (#930)
88
- Add support for `<script type="text/babel">` (#932)
9+
- Show pixel equivalents in completions and hovers of the theme() helper (#935)
910

1011
## 0.10.5
1112

0 commit comments

Comments
 (0)