Skip to content

Commit

Permalink
Show color decorators for oklab and oklch colors (#936)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace committed Mar 27, 2024
1 parent ecce42b commit 745c3ab
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 40 deletions.
22 changes: 13 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/tailwindcss-language-server/package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Expand Up @@ -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
},

Expand Down
43 changes: 36 additions & 7 deletions packages/tailwindcss-language-server/tests/colors/colors.test.js
Expand Up @@ -59,15 +59,30 @@ withFixture('basic', (c) => {
})

testColors('arbitrary value and opacity modifier', {
text: '<div class="bg-[red]/[0.33]">',
text: '<div class="bg-[red]/[0.5]">',
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: '<div class="bg-[oklch(60%_0.25_25)]">',
expected: [
{
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } },
color: {
alpha: 1,
red: 0.9475942429386454,
green: 0,
blue: 0.14005415620741646,
},
},
],
Expand Down Expand Up @@ -135,19 +150,33 @@ withFixture('v4/basic', (c) => {

/*
testColors('arbitrary value and opacity modifier', {
text: '<div class="bg-[red]/[0.33]">',
text: '<div class="bg-[red]/[0.5]">',
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: '<div class="bg-[oklch(60%_0.25_25)]">',
expected: [
{
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 35 } },
color: {
alpha: 1,
red: 0.9475942429386454,
green: 0,
blue: 0.14005415620741646,
},
},
],
})
})
Expand Up @@ -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: '<div class="bg-[oklch(44.05%_0.16_303)]">' })
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) => {
Expand Down Expand Up @@ -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: '<div class="bg-[oklch(44.05%_0.16_303)]">' })
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([])
})
})
57 changes: 50 additions & 7 deletions packages/tailwindcss-language-server/tests/hover/hover.test.js
Expand Up @@ -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', {
Expand Down Expand Up @@ -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: '<div class="text-[oklch(44.05%_0.16_303)]">',
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: '<div class="text-[oklch(60%_0.26_20)]">',
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 },
},
},
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/tailwindcss-language-service/package.json
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions packages/tailwindcss-language-service/src/util/color.ts
Expand Up @@ -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',
Expand All @@ -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)[] {
Expand Down Expand Up @@ -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)
) {
Expand All @@ -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 }
}

Expand Down
18 changes: 10 additions & 8 deletions 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
}

Expand All @@ -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
}

Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Expand Up @@ -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

Expand Down

0 comments on commit 745c3ab

Please sign in to comment.