diff --git a/package-lock.json b/package-lock.json index fe256a80..d2281bbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -783,9 +783,9 @@ "dev": true }, "node_modules/@types/culori": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.0.1.tgz", - "integrity": "sha512-Mfcyo2pT3IwxIdB2fRZrMp+noSZOOc+719G89h/MqDHcJNEDy22HO0XEso7Ofz6p0kB7XqkhxMBjmCXLQPeyJA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/culori/-/culori-2.1.0.tgz", + "integrity": "sha512-4uJT5CcC9Mi8mACkWShsPHqILMWL0OqoTsfoLJUGzN1mcipcepmmEdzU8b9L1KwyRNN3rnQO39ylI/2VR850zw==" }, "node_modules/@types/debounce": { "version": "1.2.0", @@ -1928,9 +1928,12 @@ } }, "node_modules/culori": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/culori/-/culori-0.20.1.tgz", - "integrity": "sha512-jNZDmufWx4vCHW2fTb62sarHEeIF3WWrUYIv4ZpoQnN2vQU6IRPz1Ra9QnsHUKzdb5lppSuLsdB72rMmBMAd+A==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.1.tgz", + "integrity": "sha512-LSnjA6HuIUOlkfKVbzi2OlToZE8OjFi667JWN9qNymXVXzGDmvuP60SSgC+e92sd7B7158f7Fy3Mb6rXS5EDPw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/data-urls": { "version": "4.0.0", @@ -7419,6 +7422,7 @@ "@tailwindcss/line-clamp": "0.4.2", "@tailwindcss/typography": "0.5.7", "@types/color-name": "^1.1.3", + "@types/culori": "^2.1.0", "@types/debounce": "1.2.0", "@types/dlv": "^1.1.4", "@types/find-up": "^4.0.0", @@ -7433,7 +7437,7 @@ "bun-types": "^1.0.6", "chokidar": "3.5.1", "color-name": "1.1.4", - "culori": "0.20.1", + "culori": "^4.0.1", "debounce": "1.2.0", "deepmerge": "4.2.2", "dlv": "1.1.3", @@ -7525,12 +7529,12 @@ "@csstools/css-parser-algorithms": "2.1.1", "@csstools/css-tokenizer": "2.1.1", "@csstools/media-query-list-parser": "2.0.4", - "@types/culori": "^2.0.0", + "@types/culori": "^2.1.0", "@types/moo": "0.5.3", "@types/semver": "7.3.10", "color-name": "1.1.4", "css.escape": "1.5.1", - "culori": "0.20.1", + "culori": "^4.0.1", "detect-indent": "6.0.0", "dlv": "1.1.3", "dset": "3.1.2", diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 99b896e2..7ddd40da 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -41,6 +41,7 @@ "@tailwindcss/line-clamp": "0.4.2", "@tailwindcss/typography": "0.5.7", "@types/color-name": "^1.1.3", + "@types/culori": "^2.1.0", "@types/debounce": "1.2.0", "@types/dlv": "^1.1.4", "@types/find-up": "^4.0.0", @@ -55,7 +56,7 @@ "bun-types": "^1.0.6", "chokidar": "3.5.1", "color-name": "1.1.4", - "culori": "0.20.1", + "culori": "^4.0.1", "debounce": "1.2.0", "deepmerge": "4.2.2", "dlv": "1.1.3", diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 0907af85..d3ef74df 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -43,17 +43,23 @@ export async function loadDesignSystem( Object.assign(design, { compile(classes: string[]): (postcss.Root | null)[] { let css = design.candidatesToCss(classes) + let errors: any[] = [] let roots = css.map((str) => { if (str === null) return postcss.root() try { return postcss.parse(str.trimEnd()) - } catch { - return null + } catch (err) { + errors.push(err) + return postcss.root() } }) + if (errors.length > 0) { + console.error(JSON.stringify(errors)) + } + return roots }, diff --git a/packages/tailwindcss-language-server/tests/colors/colors.test.js b/packages/tailwindcss-language-server/tests/colors/colors.test.js index 4947ac6d..c7252de7 100644 --- a/packages/tailwindcss-language-server/tests/colors/colors.test.js +++ b/packages/tailwindcss-language-server/tests/colors/colors.test.js @@ -59,15 +59,30 @@ withFixture('basic', (c) => { }) testColors('arbitrary value and opacity modifier', { - text: '
', + text: '
', expected: [ { - range: { start: { line: 0, character: 12 }, end: { line: 0, character: 27 } }, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 26 } }, color: { red: 1, green: 0, blue: 0, - alpha: 0.33, + alpha: 0.5, + }, + }, + ], + }) + + testColors('oklch colors are parsed', { + text: '
', + expected: [ + { + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } }, + color: { + alpha: 1, + red: 0.9475942429386454, + green: 0, + blue: 0.14005415620741646, }, }, ], @@ -135,19 +150,33 @@ withFixture('v4/basic', (c) => { /* testColors('arbitrary value and opacity modifier', { - text: '
', + text: '
', expected: [ { - range: { start: { line: 0, character: 12 }, end: { line: 0, character: 27 } }, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 26 } }, color: { red: 1, green: 0, blue: 0, - // TODO: This is strange, it should be 0.33 - alpha: 0.32941176470588235, + alpha: 0.5, }, }, ], }) */ + + testColors('oklch colors are parsed', { + text: '
', + expected: [ + { + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } }, + color: { + alpha: 1, + red: 0.9475942429386454, + green: 0, + blue: 0.14005415620741646, + }, + }, + ], + }) }) diff --git a/packages/tailwindcss-language-server/tests/colors/presentation.test.js b/packages/tailwindcss-language-server/tests/colors/presentation.test.js index f07e029b..88bbf4ad 100644 --- a/packages/tailwindcss-language-server/tests/colors/presentation.test.js +++ b/packages/tailwindcss-language-server/tests/colors/presentation.test.js @@ -105,6 +105,20 @@ withFixture('basic', (c) => { { label: 'bg-[hsl(0,100%,50%)]' }, ]) }) + + test.concurrent('arbitrary oklch color', async ({ expect }) => { + let textDocument = await c.openDocument({ text: '
' }) + let res = await c.sendRequest('textDocument/colorPresentation', { + color: { red: 1, green: 0, blue: 0, alpha: 1 }, + textDocument, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 39 }, + }, + }) + + expect(res).toEqual([]) + }) }) withFixture('v4/basic', (c) => { @@ -211,4 +225,18 @@ withFixture('v4/basic', (c) => { { label: 'bg-[hsl(0,100%,50%)]' }, ]) }) + + test.concurrent('arbitrary oklch color', async ({ expect }) => { + let textDocument = await c.openDocument({ text: '
' }) + let res = await c.sendRequest('textDocument/colorPresentation', { + color: { red: 1, green: 0, blue: 0, alpha: 1 }, + textDocument, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 39 }, + }, + }) + + expect(res).toEqual([]) + }) }) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 0e8289c6..e80c5a9d 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -2,7 +2,10 @@ import { test } from 'vitest' import { withFixture } from '../common' withFixture('basic', (c) => { - async function testHover(name, { text, lang, position, exact = false, expected, expectedRange, settings }) { + async function testHover( + name, + { text, lang, position, exact = false, expected, expectedRange, settings }, + ) { test.concurrent(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { @@ -99,16 +102,56 @@ withFixture('basic', (c) => { expected: { contents: { kind: 'markdown', - value: [ - '```plaintext', - '1.25rem /* 20px */', - '```', - ].join('\n'), + value: ['```plaintext', '1.25rem /* 20px */', '```'].join('\n'), }, range: { start: { line: 0, character: 24 }, end: { line: 0, character: 35 }, - } + }, + }, + }) + + testHover('color equivalents supports in-gamut oklch/oklab', { + lang: 'html', + text: '
', + position: { line: 0, character: 32 }, + + exact: true, + expected: { + contents: { + language: 'css', + value: [ + '.text-\\[oklch\\(44\\.05\\%_0\\.16_303\\)\\] {', + ' color: oklch(44.05% 0.16 303) /* #663399 */;', + '}', + ].join('\n'), + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 41 }, + }, + }, + }) + + testHover('color equivalents ignores wide-gamut oklch/oklab', { + lang: 'html', + text: '
', + position: { line: 0, character: 32 }, + + exact: true, + expected: { + contents: { + language: 'css', + value: [ + '.text-\\[oklch\\(60\\%_0\\.26_20\\)\\] {', + ' color: oklch(60% 0.26 20);', + '}', + ].join('\n'), + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 37 }, + }, }, }) }) diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index cb8e229b..8212fb25 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -16,12 +16,12 @@ "@csstools/css-parser-algorithms": "2.1.1", "@csstools/css-tokenizer": "2.1.1", "@csstools/media-query-list-parser": "2.0.4", - "@types/culori": "^2.0.0", + "@types/culori": "^2.1.0", "@types/moo": "0.5.3", "@types/semver": "7.3.10", "color-name": "1.1.4", "css.escape": "1.5.1", - "culori": "0.20.1", + "culori": "^4.0.1", "detect-indent": "6.0.0", "dlv": "1.1.3", "dset": "3.1.2", diff --git a/packages/tailwindcss-language-service/src/util/color.ts b/packages/tailwindcss-language-service/src/util/color.ts index 6cb81ad0..f4933c6f 100644 --- a/packages/tailwindcss-language-service/src/util/color.ts +++ b/packages/tailwindcss-language-service/src/util/color.ts @@ -43,7 +43,7 @@ function getKeywordColor(value: unknown): KeywordColor | null { // https://github.com/khalilgharbaoui/coloregex const colorRegex = new RegExp( - `(?:^|\\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( + `(?:^|\\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( namedColors, ).join('|')})(?:$|\\s|\\)|,)`, 'gi', @@ -52,7 +52,7 @@ const colorRegex = new RegExp( function replaceColorVarsWithTheirDefaults(str: string): string { // rgb(var(--primary, 66 66 66)) // -> rgb(66 66 66) - return str.replace(/((?:rgb|hsl)a?\(\s*)var\([^,]+,\s*([^)]+)\)/gi, '$1$2') + return str.replace(/((?:rgba?|hsla?|(?:ok)?(?:lab|lch))\(\s*)var\([^,]+,\s*([^)]+)\)/gi, '$1$2') } function getColorsInString(str: string): (culori.Color | KeywordColor)[] { @@ -205,7 +205,7 @@ export function getColorFromValue(value: unknown): culori.Color | KeywordColor | return 'currentColor' } if ( - !/^\s*(?:rgba?|hsla?)\s*\([^)]+\)\s*$/.test(trimmedValue) && + !/^\s*(?:rgba?|hsla?|(?:ok)?(?:lab|lch))\s*\([^)]+\)\s*$/.test(trimmedValue) && !/^\s*#[0-9a-f]+\s*$/i.test(trimmedValue) && !Object.keys(namedColors).includes(trimmedValue) ) { @@ -218,7 +218,7 @@ export function getColorFromValue(value: unknown): culori.Color | KeywordColor | let toRgb = culori.converter('rgb') export function culoriColorToVscodeColor(color: culori.Color): Color { - let rgb = toRgb(color) + let rgb = culori.clampRgb(toRgb(color)) return { red: rgb.r, green: rgb.g, blue: rgb.b, alpha: rgb.alpha ?? 1 } } diff --git a/packages/tailwindcss-language-service/src/util/colorEquivalents.ts b/packages/tailwindcss-language-service/src/util/colorEquivalents.ts index 3a6f8b78..2cf0ba60 100644 --- a/packages/tailwindcss-language-service/src/util/colorEquivalents.ts +++ b/packages/tailwindcss-language-service/src/util/colorEquivalents.ts @@ -1,13 +1,16 @@ import type { Plugin } from 'postcss' import parseValue from 'postcss-value-parser' +import { inGamut } from 'culori' import { formatColor, getColorFromValue } from './color' import type { Comment } from './comments' +let allowedFunctions = ['rgb', 'rgba', 'hsl', 'hsla', 'lch', 'lab', 'oklch', 'oklab'] + export function equivalentColorValues({ comments }: { comments: Comment[] }): Plugin { return { postcssPlugin: 'plugin', Declaration(decl) { - if (!decl.value.includes('rgb') && !decl.value.includes('hsl')) { + if (!allowedFunctions.some((fn) => decl.value.includes(fn))) { return } @@ -16,12 +19,7 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl return true } - if ( - node.value !== 'rgb' && - node.value !== 'rgba' && - node.value !== 'hsl' && - node.value !== 'hsla' - ) { + if (!allowedFunctions.includes(node.value)) { return false } @@ -30,7 +28,11 @@ export function equivalentColorValues({ comments }: { comments: Comment[] }): Pl return false } - const color = getColorFromValue(`rgb(${values.join(', ')})`) + const color = getColorFromValue(`${node.value}(${values.join(' ')})`) + if (!inGamut('rgb')(color)) { + return false + } + if (!color || typeof color === 'string') { return false } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 50eea278..7ecb7eb6 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -9,6 +9,7 @@ - Show pixel equivalents in completions and hovers of the theme() helper (#935) - Handle `style` exports condition when processing `@import`s (#934) - Highlight `@theme` contents as a rule list (#937) +- Show color decorators for `oklab` and `oklch` colors (#936) ## 0.10.5 diff --git a/types/culori.d.ts b/types/culori.d.ts new file mode 100644 index 00000000..b19404a1 --- /dev/null +++ b/types/culori.d.ts @@ -0,0 +1,5 @@ +import type { Color } from 'culori' + +declare module 'culori' { + export function inGamut(mode: string): (color: Color | string) => boolean +}