Skip to content

Commit 4b46fc5

Browse files
Add color format options (rgb, hex) (#831)
* Add color format options (rgb, hex) Formats are displayed in completions and as comments in hovered css. This helps with comparing code to designs. * Start separate process for each withFixture block This will give us a guaranteed way to isolate global state in tests * Add tests * Remove `colorFormat` setting wip * Always call `addColorEquivalentsToCss` for Tailwind v2 and below * Generate hex for RGB and HSL colors * Refactor * Refactor * Refactor * Update tests * Update for v4 * Use stringify methods * Update tests --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent c3bbd2f commit 4b46fc5

File tree

12 files changed

+272
-95
lines changed

12 files changed

+272
-95
lines changed

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

+142-37
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { test } from 'vitest'
22
import { withFixture } from '../common'
33

4-
withFixture('basic', (c) => {
5-
async function completion({
4+
function buildCompletion(c) {
5+
return async function completion({
66
lang,
77
text,
88
position,
@@ -19,6 +19,10 @@ withFixture('basic', (c) => {
1919
context,
2020
})
2121
}
22+
}
23+
24+
withFixture('basic', (c) => {
25+
let completion = buildCompletion(c)
2226

2327
async function expectCompletions({ expect, lang, text, position, settings }) {
2428
let result = await completion({ lang, text, position, settings })
@@ -148,7 +152,7 @@ withFixture('basic', (c) => {
148152
expect(result).toBe(null)
149153
})
150154

151-
test('classRegex matching empty string', async ({ expect }) => {
155+
test.concurrent('classRegex matching empty string', async ({ expect }) => {
152156
try {
153157
let result = await completion({
154158
text: "let _ = ''",
@@ -203,24 +207,87 @@ withFixture('basic', (c) => {
203207
})
204208
})
205209

206-
withFixture('overrides-variants', (c) => {
207-
async function completion({
208-
lang,
209-
text,
210-
position,
211-
context = {
212-
triggerKind: 1,
213-
},
214-
settings,
215-
}) {
216-
let textDocument = await c.openDocument({ text, lang, settings })
210+
withFixture('basic', (c) => {
211+
let completion = buildCompletion(c)
217212

218-
return c.sendRequest('textDocument/completion', {
219-
textDocument,
220-
position,
221-
context,
213+
test('Completions have default pixel equivalents (1rem == 16px)', async ({ expect }) => {
214+
let result = await completion({
215+
lang: 'html',
216+
text: '<div class=""></div>',
217+
position: { line: 0, character: 12 },
222218
})
223-
}
219+
220+
let item = result.items.find((item) => item.label === 'text-sm')
221+
let resolved = await c.sendRequest('completionItem/resolve', item)
222+
223+
expect(resolved).toEqual({
224+
...item,
225+
detail: 'font-size: 0.875rem/* 14px */; line-height: 1.25rem/* 20px */;',
226+
documentation: {
227+
kind: 'markdown',
228+
value:
229+
'```css\n.text-sm {\n font-size: 0.875rem/* 14px */;\n line-height: 1.25rem/* 20px */;\n}\n```',
230+
},
231+
})
232+
})
233+
})
234+
235+
withFixture('basic', (c) => {
236+
let completion = buildCompletion(c)
237+
238+
test('Completions have customizable pixel equivalents (1rem == 10px)', async ({ expect }) => {
239+
await c.updateSettings({
240+
tailwindCSS: {
241+
rootFontSize: 10,
242+
},
243+
})
244+
245+
let result = await completion({
246+
lang: 'html',
247+
text: '<div class=""></div>',
248+
position: { line: 0, character: 12 },
249+
})
250+
251+
let item = result.items.find((item) => item.label === 'text-sm')
252+
253+
let resolved = await c.sendRequest('completionItem/resolve', item)
254+
255+
expect(resolved).toEqual({
256+
...item,
257+
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
258+
documentation: {
259+
kind: 'markdown',
260+
value:
261+
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
262+
},
263+
})
264+
})
265+
})
266+
267+
withFixture('basic', (c) => {
268+
let completion = buildCompletion(c)
269+
270+
test('Completions have color equivalents presented as hex', async ({ expect }) => {
271+
let result = await completion({
272+
lang: 'html',
273+
text: '<div class=""></div>',
274+
position: { line: 0, character: 12 },
275+
})
276+
277+
let item = result.items.find((item) => item.label === 'bg-red-500')
278+
279+
let resolved = await c.sendRequest('completionItem/resolve', item)
280+
281+
expect(resolved).toEqual({
282+
...item,
283+
detail: '--tw-bg-opacity: 1; background-color: rgb(239 68 68 / var(--tw-bg-opacity));',
284+
documentation: '#ef4444',
285+
})
286+
})
287+
})
288+
289+
withFixture('overrides-variants', (c) => {
290+
let completion = buildCompletion(c)
224291

225292
test.concurrent(
226293
'duplicate variant + value pairs do not produce multiple completions',
@@ -236,23 +303,7 @@ withFixture('overrides-variants', (c) => {
236303
})
237304

238305
withFixture('v4/basic', (c) => {
239-
async function completion({
240-
lang,
241-
text,
242-
position,
243-
context = {
244-
triggerKind: 1,
245-
},
246-
settings,
247-
}) {
248-
let textDocument = await c.openDocument({ text, lang, settings })
249-
250-
return c.sendRequest('textDocument/completion', {
251-
textDocument,
252-
position,
253-
context,
254-
})
255-
}
306+
let completion = buildCompletion(c)
256307

257308
async function expectCompletions({ expect, lang, text, position, settings }) {
258309
let result = await completion({ lang, text, position, settings })
@@ -439,11 +490,65 @@ withFixture('v4/basic', (c) => {
439490

440491
expect(resolved).toEqual({
441492
...item,
442-
detail: 'text-transform: uppercase',
493+
detail: 'text-transform: uppercase;',
443494
documentation: {
444495
kind: 'markdown',
445496
value: '```css\n.uppercase {\n text-transform: uppercase;\n}\n```',
446497
},
447498
})
448499
})
449500
})
501+
502+
withFixture('v4/basic', (c) => {
503+
let completion = buildCompletion(c)
504+
505+
test('Completions have customizable pixel equivalents (1rem == 10px)', async ({ expect }) => {
506+
await c.updateSettings({
507+
tailwindCSS: {
508+
rootFontSize: 10,
509+
},
510+
})
511+
512+
let result = await completion({
513+
lang: 'html',
514+
text: '<div class=""></div>',
515+
position: { line: 0, character: 12 },
516+
})
517+
518+
let item = result.items.find((item) => item.label === 'text-sm')
519+
520+
let resolved = await c.sendRequest('completionItem/resolve', item)
521+
522+
expect(resolved).toEqual({
523+
...item,
524+
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
525+
documentation: {
526+
kind: 'markdown',
527+
value:
528+
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
529+
},
530+
})
531+
})
532+
})
533+
534+
withFixture('v4/basic', (c) => {
535+
let completion = buildCompletion(c)
536+
537+
test('Completions have color equivalents presented as hex', async ({ expect }) => {
538+
let result = await completion({
539+
lang: 'html',
540+
text: '<div class=""></div>',
541+
position: { line: 0, character: 12 },
542+
})
543+
544+
let item = result.items.find((item) => item.label === 'bg-red-500')
545+
546+
let resolved = await c.sendRequest('completionItem/resolve', item)
547+
548+
expect(resolved).toEqual({
549+
...item,
550+
detail: 'background-color: #ef4444;',
551+
documentation: '#ef4444',
552+
})
553+
})
554+
})

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));\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));\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
})

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));\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));\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
})

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -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));\n' +
41+
' background-color: rgb(239 68 68 / var(--tw-bg-opacity))/* #ef4444 */;\n' +
4242
'}',
4343
expectedRange: {
4444
start: { line: 0, character: 12 },

packages/tailwindcss-language-service/src/completionProvider.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import type { TextDocument } from 'vscode-languageserver-textdocument'
1313
import dlv from 'dlv'
1414
import removeMeta from './util/removeMeta'
15-
import { getColor, getColorFromValue } from './util/color'
15+
import { formatColor, getColor, getColorFromValue } from './util/color'
1616
import { isHtmlContext } from './util/html'
1717
import { isCssContext } from './util/css'
1818
import { findLast, matchClassAttributes } from './util/find'
@@ -33,7 +33,6 @@ import { validateApply } from './util/validateApply'
3333
import { flagEnabled } from './util/flagEnabled'
3434
import * as jit from './util/jit'
3535
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
36-
import * as culori from 'culori'
3736
import {
3837
addPixelEquivalentsToMediaQuery,
3938
addPixelEquivalentsToValue,
@@ -102,9 +101,10 @@ export function completionsFromClassList(
102101
if (color !== null) {
103102
kind = CompletionItemKind.Color
104103
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
105-
documentation = culori.formatRgb(color)
104+
documentation = formatColor(color)
106105
}
107106
}
107+
108108
return {
109109
label: className,
110110
...(documentation ? { documentation } : {}),
@@ -298,7 +298,7 @@ export function completionsFromClassList(
298298
let documentation: string | undefined
299299

300300
if (color && typeof color !== 'string') {
301-
documentation = culori.formatRgb(color)
301+
documentation = formatColor(color)
302302
}
303303

304304
items.push({
@@ -367,7 +367,7 @@ export function completionsFromClassList(
367367
if (color !== null) {
368368
kind = CompletionItemKind.Color
369369
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
370-
documentation = culori.formatRgb(color)
370+
documentation = formatColor(color)
371371
}
372372
}
373373

@@ -528,7 +528,7 @@ export function completionsFromClassList(
528528
let documentation: string | undefined
529529

530530
if (color && typeof color !== 'string') {
531-
documentation = culori.formatRgb(color)
531+
documentation = formatColor(color)
532532
}
533533

534534
items.push({
@@ -575,7 +575,7 @@ export function completionsFromClassList(
575575
if (color !== null) {
576576
kind = CompletionItemKind.Color
577577
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
578-
documentation = culori.formatRgb(color)
578+
documentation = formatColor(color)
579579
}
580580
}
581581

@@ -661,7 +661,7 @@ export function completionsFromClassList(
661661
if (color !== null) {
662662
kind = CompletionItemKind.Color
663663
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
664-
documentation = culori.formatRgb(color)
664+
documentation = formatColor(color)
665665
}
666666
}
667667

@@ -1050,7 +1050,7 @@ function provideCssHelperCompletions(
10501050
// VS Code bug causes some values to not display in some cases
10511051
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
10521052
...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0
1053-
? { documentation: culori.formatRgb(color) }
1053+
? { documentation: formatColor(color) }
10541054
: {}),
10551055
...(insertClosingBrace ? { textEditText: `${item}]` } : {}),
10561056
additionalTextEdits: replaceDot
@@ -1727,7 +1727,9 @@ export async function resolveCompletionItem(
17271727
decls.push(node)
17281728
})
17291729

1730-
item.detail = state.designSystem.toCss(decls)
1730+
item.detail = await jit.stringifyDecls(state, postcss.rule({
1731+
nodes: decls,
1732+
}))
17311733
} else {
17321734
item.detail = `${rules.length} rules`
17331735
}
@@ -1736,7 +1738,7 @@ export async function resolveCompletionItem(
17361738
if (!item.documentation) {
17371739
item.documentation = {
17381740
kind: 'markdown' as typeof MarkupKind.Markdown,
1739-
value: ['```css', state.designSystem.toCss(rules), '```'].join('\n'),
1741+
value: ['```css', await jit.stringifyRoot(state, postcss.root({ nodes: rules })), '```'].join('\n'),
17401742
}
17411743
}
17421744

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dlv from 'dlv'
22
import { State } from './state'
33
import removeMeta from './removeMeta'
4-
import { ensureArray, dedupe, flatten } from './array'
4+
import { ensureArray, dedupe } from './array'
55
import type { Color } from 'vscode-languageserver'
66
import { getClassNameParts } from './getClassNameAtPosition'
77
import * as jit from './jit'
@@ -229,3 +229,11 @@ export function culoriColorToVscodeColor(color: culori.Color): Color {
229229
let rgb = toRgb(color)
230230
return { red: rgb.r, green: rgb.g, blue: rgb.b, alpha: rgb.alpha ?? 1 }
231231
}
232+
233+
export function formatColor(color: culori.Color): string {
234+
if (color.alpha === undefined || color.alpha === 1) {
235+
return culori.formatHex(color)
236+
}
237+
238+
return culori.formatHex8(color)
239+
}

0 commit comments

Comments
 (0)