Skip to content

Commit 745c3ab

Browse files
Show color decorators for oklab and oklch colors (#936)
* Refactor * Fix CS * Bump minimum culori version * Add support for parsing more colorspaces * Update lockfile * Add tests for color presentations * Update changelog * Handle parsing failures the same as unknown classes * Stringify errors before logging
1 parent ecce42b commit 745c3ab

File tree

11 files changed

+159
-40
lines changed

11 files changed

+159
-40
lines changed

package-lock.json

+13-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tailwindcss-language-server/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@tailwindcss/line-clamp": "0.4.2",
4242
"@tailwindcss/typography": "0.5.7",
4343
"@types/color-name": "^1.1.3",
44+
"@types/culori": "^2.1.0",
4445
"@types/debounce": "1.2.0",
4546
"@types/dlv": "^1.1.4",
4647
"@types/find-up": "^4.0.0",
@@ -55,7 +56,7 @@
5556
"bun-types": "^1.0.6",
5657
"chokidar": "3.5.1",
5758
"color-name": "1.1.4",
58-
"culori": "0.20.1",
59+
"culori": "^4.0.1",
5960
"debounce": "1.2.0",
6061
"deepmerge": "4.2.2",
6162
"dlv": "1.1.3",

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ export async function loadDesignSystem(
4343
Object.assign(design, {
4444
compile(classes: string[]): (postcss.Root | null)[] {
4545
let css = design.candidatesToCss(classes)
46+
let errors: any[] = []
4647

4748
let roots = css.map((str) => {
4849
if (str === null) return postcss.root()
4950

5051
try {
5152
return postcss.parse(str.trimEnd())
52-
} catch {
53-
return null
53+
} catch (err) {
54+
errors.push(err)
55+
return postcss.root()
5456
}
5557
})
5658

59+
if (errors.length > 0) {
60+
console.error(JSON.stringify(errors))
61+
}
62+
5763
return roots
5864
},
5965

packages/tailwindcss-language-server/tests/colors/colors.test.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,30 @@ withFixture('basic', (c) => {
5959
})
6060

6161
testColors('arbitrary value and opacity modifier', {
62-
text: '<div class="bg-[red]/[0.33]">',
62+
text: '<div class="bg-[red]/[0.5]">',
6363
expected: [
6464
{
65-
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 27 } },
65+
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 26 } },
6666
color: {
6767
red: 1,
6868
green: 0,
6969
blue: 0,
70-
alpha: 0.33,
70+
alpha: 0.5,
71+
},
72+
},
73+
],
74+
})
75+
76+
testColors('oklch colors are parsed', {
77+
text: '<div class="bg-[oklch(60%_0.25_25)]">',
78+
expected: [
79+
{
80+
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } },
81+
color: {
82+
alpha: 1,
83+
red: 0.9475942429386454,
84+
green: 0,
85+
blue: 0.14005415620741646,
7186
},
7287
},
7388
],
@@ -135,19 +150,33 @@ withFixture('v4/basic', (c) => {
135150

136151
/*
137152
testColors('arbitrary value and opacity modifier', {
138-
text: '<div class="bg-[red]/[0.33]">',
153+
text: '<div class="bg-[red]/[0.5]">',
139154
expected: [
140155
{
141-
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 27 } },
156+
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 26 } },
142157
color: {
143158
red: 1,
144159
green: 0,
145160
blue: 0,
146-
// TODO: This is strange, it should be 0.33
147-
alpha: 0.32941176470588235,
161+
alpha: 0.5,
148162
},
149163
},
150164
],
151165
})
152166
*/
167+
168+
testColors('oklch colors are parsed', {
169+
text: '<div class="bg-[oklch(60%_0.25_25)]">',
170+
expected: [
171+
{
172+
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } },
173+
color: {
174+
alpha: 1,
175+
red: 0.9475942429386454,
176+
green: 0,
177+
blue: 0.14005415620741646,
178+
},
179+
},
180+
],
181+
})
153182
})

packages/tailwindcss-language-server/tests/colors/presentation.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ withFixture('basic', (c) => {
105105
{ label: 'bg-[hsl(0,100%,50%)]' },
106106
])
107107
})
108+
109+
test.concurrent('arbitrary oklch color', async ({ expect }) => {
110+
let textDocument = await c.openDocument({ text: '<div class="bg-[oklch(44.05%_0.16_303)]">' })
111+
let res = await c.sendRequest('textDocument/colorPresentation', {
112+
color: { red: 1, green: 0, blue: 0, alpha: 1 },
113+
textDocument,
114+
range: {
115+
start: { line: 0, character: 12 },
116+
end: { line: 0, character: 39 },
117+
},
118+
})
119+
120+
expect(res).toEqual([])
121+
})
108122
})
109123

110124
withFixture('v4/basic', (c) => {
@@ -211,4 +225,18 @@ withFixture('v4/basic', (c) => {
211225
{ label: 'bg-[hsl(0,100%,50%)]' },
212226
])
213227
})
228+
229+
test.concurrent('arbitrary oklch color', async ({ expect }) => {
230+
let textDocument = await c.openDocument({ text: '<div class="bg-[oklch(44.05%_0.16_303)]">' })
231+
let res = await c.sendRequest('textDocument/colorPresentation', {
232+
color: { red: 1, green: 0, blue: 0, alpha: 1 },
233+
textDocument,
234+
range: {
235+
start: { line: 0, character: 12 },
236+
end: { line: 0, character: 39 },
237+
},
238+
})
239+
240+
expect(res).toEqual([])
241+
})
214242
})

packages/tailwindcss-language-server/tests/hover/hover.test.js

+50-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { test } from 'vitest'
22
import { withFixture } from '../common'
33

44
withFixture('basic', (c) => {
5-
async function testHover(name, { text, lang, position, exact = false, expected, expectedRange, settings }) {
5+
async function testHover(
6+
name,
7+
{ text, lang, position, exact = false, expected, expectedRange, settings },
8+
) {
69
test.concurrent(name, async ({ expect }) => {
710
let textDocument = await c.openDocument({ text, lang, settings })
811
let res = await c.sendRequest('textDocument/hover', {
@@ -99,16 +102,56 @@ withFixture('basic', (c) => {
99102
expected: {
100103
contents: {
101104
kind: 'markdown',
102-
value: [
103-
'```plaintext',
104-
'1.25rem /* 20px */',
105-
'```',
106-
].join('\n'),
105+
value: ['```plaintext', '1.25rem /* 20px */', '```'].join('\n'),
107106
},
108107
range: {
109108
start: { line: 0, character: 24 },
110109
end: { line: 0, character: 35 },
111-
}
110+
},
111+
},
112+
})
113+
114+
testHover('color equivalents supports in-gamut oklch/oklab', {
115+
lang: 'html',
116+
text: '<div class="text-[oklch(44.05%_0.16_303)]">',
117+
position: { line: 0, character: 32 },
118+
119+
exact: true,
120+
expected: {
121+
contents: {
122+
language: 'css',
123+
value: [
124+
'.text-\\[oklch\\(44\\.05\\%_0\\.16_303\\)\\] {',
125+
' color: oklch(44.05% 0.16 303) /* #663399 */;',
126+
'}',
127+
].join('\n'),
128+
},
129+
range: {
130+
start: { line: 0, character: 12 },
131+
end: { line: 0, character: 41 },
132+
},
133+
},
134+
})
135+
136+
testHover('color equivalents ignores wide-gamut oklch/oklab', {
137+
lang: 'html',
138+
text: '<div class="text-[oklch(60%_0.26_20)]">',
139+
position: { line: 0, character: 32 },
140+
141+
exact: true,
142+
expected: {
143+
contents: {
144+
language: 'css',
145+
value: [
146+
'.text-\\[oklch\\(60\\%_0\\.26_20\\)\\] {',
147+
' color: oklch(60% 0.26 20);',
148+
'}',
149+
].join('\n'),
150+
},
151+
range: {
152+
start: { line: 0, character: 12 },
153+
end: { line: 0, character: 37 },
154+
},
112155
},
113156
})
114157
})

packages/tailwindcss-language-service/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
"@csstools/css-parser-algorithms": "2.1.1",
1717
"@csstools/css-tokenizer": "2.1.1",
1818
"@csstools/media-query-list-parser": "2.0.4",
19-
"@types/culori": "^2.0.0",
19+
"@types/culori": "^2.1.0",
2020
"@types/moo": "0.5.3",
2121
"@types/semver": "7.3.10",
2222
"color-name": "1.1.4",
2323
"css.escape": "1.5.1",
24-
"culori": "0.20.1",
24+
"culori": "^4.0.1",
2525
"detect-indent": "6.0.0",
2626
"dlv": "1.1.3",
2727
"dset": "3.1.2",

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function getKeywordColor(value: unknown): KeywordColor | null {
4343

4444
// https://github.com/khalilgharbaoui/coloregex
4545
const colorRegex = new RegExp(
46-
`(?:^|\\s|\\(|,)(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\\(\\s*(-?[\\d.]+%?(\\s*[,/]\\s*|\\s+)+){2,3}\\s*([\\d.]+%?|var\\([^)]+\\))?\\)|transparent|currentColor|${Object.keys(
46+
`(?:^|\\s|\\(|,)(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgba?|hsla?|(?:ok)?(?:lab|lch))\\(\\s*(-?[\\d.]+%?(\\s*[,/]\\s*|\\s+)+){2,3}\\s*([\\d.]+%?|var\\([^)]+\\))?\\)|transparent|currentColor|${Object.keys(
4747
namedColors,
4848
).join('|')})(?:$|\\s|\\)|,)`,
4949
'gi',
@@ -52,7 +52,7 @@ const colorRegex = new RegExp(
5252
function replaceColorVarsWithTheirDefaults(str: string): string {
5353
// rgb(var(--primary, 66 66 66))
5454
// -> rgb(66 66 66)
55-
return str.replace(/((?:rgb|hsl)a?\(\s*)var\([^,]+,\s*([^)]+)\)/gi, '$1$2')
55+
return str.replace(/((?:rgba?|hsla?|(?:ok)?(?:lab|lch))\(\s*)var\([^,]+,\s*([^)]+)\)/gi, '$1$2')
5656
}
5757

5858
function getColorsInString(str: string): (culori.Color | KeywordColor)[] {
@@ -205,7 +205,7 @@ export function getColorFromValue(value: unknown): culori.Color | KeywordColor |
205205
return 'currentColor'
206206
}
207207
if (
208-
!/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) &&
208+
!/^\s*(?:rgba?|hsla?|(?:ok)?(?:lab|lch))\s*\([^)]+\)\s*$/.test(trimmedValue) &&
209209
!/^\s*#[0-9a-f]+\s*$/i.test(trimmedValue) &&
210210
!Object.keys(namedColors).includes(trimmedValue)
211211
) {
@@ -218,7 +218,7 @@ export function getColorFromValue(value: unknown): culori.Color | KeywordColor |
218218
let toRgb = culori.converter('rgb')
219219

220220
export function culoriColorToVscodeColor(color: culori.Color): Color {
221-
let rgb = toRgb(color)
221+
let rgb = culori.clampRgb(toRgb(color))
222222
return { red: rgb.r, green: rgb.g, blue: rgb.b, alpha: rgb.alpha ?? 1 }
223223
}
224224

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

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { Plugin } from 'postcss'
22
import parseValue from 'postcss-value-parser'
3+
import { inGamut } from 'culori'
34
import { formatColor, getColorFromValue } from './color'
45
import type { Comment } from './comments'
56

7+
let allowedFunctions = ['rgb', 'rgba', 'hsl', 'hsla', 'lch', 'lab', 'oklch', 'oklab']
8+
69
export function equivalentColorValues({ comments }: { comments: Comment[] }): Plugin {
710
return {
811
postcssPlugin: 'plugin',
912
Declaration(decl) {
10-
if (!decl.value.includes('rgb') && !decl.value.includes('hsl')) {
13+
if (!allowedFunctions.some((fn) => decl.value.includes(fn))) {
1114
return
1215
}
1316

@@ -16,12 +19,7 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl
1619
return true
1720
}
1821

19-
if (
20-
node.value !== 'rgb' &&
21-
node.value !== 'rgba' &&
22-
node.value !== 'hsl' &&
23-
node.value !== 'hsla'
24-
) {
22+
if (!allowedFunctions.includes(node.value)) {
2523
return false
2624
}
2725

@@ -30,7 +28,11 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl
3028
return false
3129
}
3230

33-
const color = getColorFromValue(`rgb(${values.join(', ')})`)
31+
const color = getColorFromValue(`${node.value}(${values.join(' ')})`)
32+
if (!inGamut('rgb')(color)) {
33+
return false
34+
}
35+
3436
if (!color || typeof color === 'string') {
3537
return false
3638
}

packages/vscode-tailwindcss/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Show pixel equivalents in completions and hovers of the theme() helper (#935)
1010
- Handle `style` exports condition when processing `@import`s (#934)
1111
- Highlight `@theme` contents as a rule list (#937)
12+
- Show color decorators for `oklab` and `oklch` colors (#936)
1213

1314
## 0.10.5
1415

0 commit comments

Comments
 (0)