Skip to content

Commit b5c9b2b

Browse files
committed
Add color format options (rgb, hex)
Formats are displayed in completions and as comments in hovered css. This helps with comparing code to designs.
1 parent f3f396b commit b5c9b2b

File tree

9 files changed

+141
-17
lines changed

9 files changed

+141
-17
lines changed

packages/tailwindcss-language-server/src/server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ async function getConfiguration(uri?: string) {
279279
recommendedVariantOrder: 'warning',
280280
},
281281
showPixelEquivalents: true,
282+
colorFormat: 'rgb',
282283
includeLanguages: {},
283284
files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] },
284285
experimental: {

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

+22-16
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
import type { TextDocument } from 'vscode-languageserver-textdocument'
1212
import dlv from 'dlv'
1313
import removeMeta from './util/removeMeta'
14-
import { getColor, getColorFromValue } from './util/color'
14+
import { formatColor, getColor, getColorFromValue } from './util/color'
1515
import { isHtmlContext } from './util/html'
1616
import { isCssContext } from './util/css'
1717
import { findLast, matchClassAttributes } from './util/find'
@@ -30,7 +30,6 @@ import { validateApply } from './util/validateApply'
3030
import { flagEnabled } from './util/flagEnabled'
3131
import * as jit from './util/jit'
3232
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
33-
import * as culori from 'culori'
3433
import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib'
3534
import {
3635
addPixelEquivalentsToMediaQuery,
@@ -42,14 +41,16 @@ let isUtil = (className) =>
4241
? className.__info.some((x) => x.__source === 'utilities')
4342
: className.__info.__source === 'utilities'
4443

45-
export function completionsFromClassList(
44+
export async function completionsFromClassList(
4645
state: State,
46+
document: TextDocument,
4747
classList: string,
4848
classListRange: Range,
4949
rootFontSize: number,
5050
filter?: (item: CompletionItem) => boolean,
5151
context?: CompletionContext
52-
): CompletionList {
52+
): Promise<CompletionList> {
53+
const settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS
5354
let classNames = classList.split(/[\s+]/)
5455
const partialClassName = classNames[classNames.length - 1]
5556
let sep = state.separator
@@ -109,7 +110,7 @@ export function completionsFromClassList(
109110
if (color !== null) {
110111
kind = 16
111112
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
112-
documentation = culori.formatRgb(color)
113+
documentation = formatColor(color, settings)
113114
}
114115
}
115116

@@ -260,7 +261,7 @@ export function completionsFromClassList(
260261
let documentation: string | undefined
261262

262263
if (color && typeof color !== 'string') {
263-
documentation = culori.formatRgb(color)
264+
documentation = formatColor(color, settings)
264265
}
265266

266267
items.push({
@@ -307,7 +308,7 @@ export function completionsFromClassList(
307308
if (color !== null) {
308309
kind = 16
309310
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
310-
documentation = culori.formatRgb(color)
311+
documentation = formatColor(color, settings)
311312
}
312313
}
313314

@@ -393,7 +394,7 @@ export function completionsFromClassList(
393394
if (color !== null) {
394395
kind = 16
395396
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
396-
documentation = culori.formatRgb(color)
397+
documentation = formatColor(color, settings)
397398
}
398399
}
399400

@@ -466,8 +467,9 @@ async function provideClassAttributeCompletions(
466467
}
467468
}
468469

469-
return completionsFromClassList(
470+
return await completionsFromClassList(
470471
state,
472+
document,
471473
classList,
472474
{
473475
start: {
@@ -541,8 +543,9 @@ async function provideCustomClassNameCompletions(
541543
classList = containerMatch[1].substr(0, cursor - matchStart)
542544
}
543545

544-
return completionsFromClassList(
546+
return await completionsFromClassList(
545547
state,
548+
document,
546549
classList,
547550
{
548551
start: {
@@ -583,8 +586,9 @@ async function provideAtApplyCompletions(
583586

584587
const classList = match.groups.classList
585588

586-
return completionsFromClassList(
589+
return await completionsFromClassList(
587590
state,
591+
document,
588592
classList,
589593
{
590594
start: {
@@ -631,11 +635,11 @@ async function provideClassNameCompletions(
631635
return null
632636
}
633637

634-
function provideCssHelperCompletions(
638+
async function provideCssHelperCompletions(
635639
state: State,
636640
document: TextDocument,
637641
position: Position
638-
): CompletionList {
642+
): Promise<CompletionList> {
639643
if (!isCssContext(state, document, position)) {
640644
return null
641645
}
@@ -666,6 +670,7 @@ function provideCssHelperCompletions(
666670
return null
667671
}
668672

673+
const settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS
669674
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
670675
let parts = path.split(/([\[\].]+)/)
671676
let keys = parts.filter((_, i) => i % 2 === 0)
@@ -744,7 +749,7 @@ function provideCssHelperCompletions(
744749
// VS Code bug causes some values to not display in some cases
745750
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
746751
...(color && typeof color !== 'string' && (color.alpha ?? 1) !== 0
747-
? { documentation: culori.formatRgb(color) }
752+
? { documentation: formatColor(color, settings) }
748753
: {}),
749754
...(insertClosingBrace ? { textEditText: `${item}]` } : {}),
750755
additionalTextEdits: replaceDot
@@ -1328,8 +1333,9 @@ async function provideEmmetCompletions(
13281333
const parts = emmetItems.items[0].label.split('.')
13291334
if (parts.length < 2) return null
13301335

1331-
return completionsFromClassList(
1336+
return await completionsFromClassList(
13321337
state,
1338+
document,
13331339
parts[parts.length - 1],
13341340
{
13351341
start: {
@@ -1352,7 +1358,7 @@ export async function doComplete(
13521358

13531359
const result =
13541360
(await provideClassNameCompletions(state, document, position, context)) ||
1355-
provideCssHelperCompletions(state, document, position) ||
1361+
(await provideCssHelperCompletions(state, document, position)) ||
13561362
provideCssDirectiveCompletions(state, document, position) ||
13571363
provideScreenDirectiveCompletions(state, document, position) ||
13581364
provideVariantsDirectiveCompletions(state, document, position) ||

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import dlv from 'dlv'
2-
import { State } from './state'
2+
import { State, TailwindCssSettings } from './state'
33
import removeMeta from './removeMeta'
44
import { ensureArray, dedupe, flatten } from './array'
55
import type { Color } from 'vscode-languageserver'
@@ -192,3 +192,14 @@ export function culoriColorToVscodeColor(color: culori.Color): Color {
192192
let rgb = toRgb(color)
193193
return { red: rgb.r, green: rgb.g, blue: rgb.b, alpha: rgb.alpha ?? 1 }
194194
}
195+
196+
export function formatColor(color: culori.Color, settings: TailwindCssSettings): string {
197+
switch (settings.colorFormat) {
198+
case 'hex':
199+
const hasAlpha = color.alpha !== undefined && color.alpha !== 1;
200+
return hasAlpha ? culori.formatHex8(color) : culori.formatHex(color)
201+
case 'rgb':
202+
default:
203+
return culori.formatRgb(color)
204+
}
205+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { Plugin } from 'postcss'
2+
import parseValue from 'postcss-value-parser'
3+
import postcss from 'postcss'
4+
import { TailwindCssSettings } from './state'
5+
import { formatColor, getColorFromValue } from './color'
6+
7+
type Comment = { index: number; value: string }
8+
9+
export function addColorEquivalentsToCss(css: string, settings: TailwindCssSettings): string {
10+
if (!css.includes('rgb')) {
11+
return css
12+
}
13+
14+
let comments: Comment[] = []
15+
16+
try {
17+
postcss([postcssPlugin({ comments, settings })]).process(css, { from: undefined }).css
18+
} catch {
19+
return css
20+
}
21+
22+
return applyComments(css, comments)
23+
}
24+
25+
function applyComments(str: string, comments: Comment[]): string {
26+
let offset = 0
27+
28+
for (let comment of comments) {
29+
let index = comment.index + offset
30+
let commentStr = `/* ${comment.value} */`
31+
str = str.slice(0, index) + commentStr + str.slice(index)
32+
offset += commentStr.length
33+
}
34+
35+
return str
36+
}
37+
38+
function postcssPlugin({
39+
comments,
40+
settings,
41+
}: {
42+
comments: Comment[]
43+
settings: TailwindCssSettings
44+
}): Plugin {
45+
return {
46+
postcssPlugin: 'plugin',
47+
Declaration(decl) {
48+
if (!decl.value.includes('rgb')) {
49+
return
50+
}
51+
52+
parseValue(decl.value).walk((node) => {
53+
if (node.type !== 'function') {
54+
return true
55+
}
56+
57+
if (node.value !== 'rgb') {
58+
return false
59+
}
60+
61+
const values = node.nodes.filter((n) => n.type === 'word').map((n) => n.value)
62+
if (values.length < 3) {
63+
return false
64+
}
65+
66+
const color = getColorFromValue(`rgb(${values.join(', ')})`);
67+
if (!color || typeof color === 'string') {
68+
return false
69+
}
70+
71+
comments.push({
72+
index:
73+
decl.source.start.offset +
74+
`${decl.prop}${decl.raws.between}`.length +
75+
node.sourceEndIndex,
76+
value: formatColor(color, settings),
77+
})
78+
79+
return false
80+
})
81+
},
82+
}
83+
}
84+
postcssPlugin.postcss = true

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

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { State } from './state'
22
import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss'
33
import { addPixelEquivalentsToCss, addPixelEquivalentsToValue } from './pixelEquivalents'
4+
import { addColorEquivalentsToCss } from './colorEquivalent';
45

56
export function bigSign(bigIntValue) {
67
// @ts-ignore
@@ -46,6 +47,9 @@ export async function stringifyRoot(state: State, root: Root, uri?: string): Pro
4647
if (settings.tailwindCSS.showPixelEquivalents) {
4748
css = addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize)
4849
}
50+
if (settings.tailwindCSS.colorFormat !== 'rgb') {
51+
css = addColorEquivalentsToCss(css, settings.tailwindCSS)
52+
}
4953

5054
return css
5155
.replace(/([^;{}\s])(\n\s*})/g, (_match, before, after) => `${before};${after}`)

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

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type TailwindCssSettings = {
4949
codeActions: boolean
5050
validate: boolean
5151
showPixelEquivalents: boolean
52+
colorFormat: 'rgb' | 'hex'
5253
rootFontSize: number
5354
colorDecorators: boolean
5455
lint: {

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

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import stringifyObject from 'stringify-object'
66
import isObject from './isObject'
77
import { Settings } from './state'
88
import { addPixelEquivalentsToCss } from './pixelEquivalents'
9+
import { addColorEquivalentsToCss } from './colorEquivalent'
910

1011
export function stringifyConfigValue(x: any): string {
1112
if (isObject(x)) return `${Object.keys(x).length} values`
@@ -58,6 +59,9 @@ export function stringifyCss(className: string, obj: any, settings: Settings): s
5859
if (settings.tailwindCSS.showPixelEquivalents) {
5960
return addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize)
6061
}
62+
if (settings.tailwindCSS.colorFormat !== 'rgb') {
63+
return addColorEquivalentsToCss(css, settings.tailwindCSS)
64+
}
6165

6266
return css
6367
}

packages/vscode-tailwindcss/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ Controls whether the editor should render inline color decorators for Tailwind C
9090

9191
Show `px` equivalents for `rem` CSS values in completions and hovers. **Default: `true`**
9292

93+
## `tailwindCSS.colorFormat`
94+
95+
The format to use for color values in completions and hovers. **Default: `rgb`**
96+
9397
### `tailwindCSS.rootFontSize`
9498

9599
Root font size in pixels. Used to convert `rem` CSS values to their `px` equivalents. See [`tailwindCSS.showPixelEquivalents`](#tailwindcssshowpixelequivalents). **Default: `16`**

packages/vscode-tailwindcss/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,15 @@
295295
"default": true,
296296
"markdownDescription": "Show `px` equivalents for `rem` CSS values."
297297
},
298+
"tailwindCSS.colorFormat": {
299+
"type": "string",
300+
"enum": [
301+
"rgb",
302+
"hex"
303+
],
304+
"default": "rgb",
305+
"markdownDescription": "The format to use for color values in completions and hovers."
306+
},
298307
"tailwindCSS.rootFontSize": {
299308
"type": "number",
300309
"default": 16,

0 commit comments

Comments
 (0)