Skip to content

Commit

Permalink
Add color format options (rgb, hex) (#831)
Browse files Browse the repository at this point in the history
* 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 <jordan@cryptica.me>
  • Loading branch information
flostellbrink and thecrypticace committed Mar 11, 2024
1 parent c3bbd2f commit 4b46fc5
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 95 deletions.
@@ -1,8 +1,8 @@
import { test } from 'vitest'
import { withFixture } from '../common'

withFixture('basic', (c) => {
async function completion({
function buildCompletion(c) {
return async function completion({
lang,
text,
position,
Expand All @@ -19,6 +19,10 @@ withFixture('basic', (c) => {
context,
})
}
}

withFixture('basic', (c) => {
let completion = buildCompletion(c)

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

test('classRegex matching empty string', async ({ expect }) => {
test.concurrent('classRegex matching empty string', async ({ expect }) => {
try {
let result = await completion({
text: "let _ = ''",
Expand Down Expand Up @@ -203,24 +207,87 @@ withFixture('basic', (c) => {
})
})

withFixture('overrides-variants', (c) => {
async function completion({
lang,
text,
position,
context = {
triggerKind: 1,
},
settings,
}) {
let textDocument = await c.openDocument({ text, lang, settings })
withFixture('basic', (c) => {
let completion = buildCompletion(c)

return c.sendRequest('textDocument/completion', {
textDocument,
position,
context,
test('Completions have default pixel equivalents (1rem == 16px)', async ({ expect }) => {
let result = await completion({
lang: 'html',
text: '<div class=""></div>',
position: { line: 0, character: 12 },
})
}

let item = result.items.find((item) => item.label === 'text-sm')
let resolved = await c.sendRequest('completionItem/resolve', item)

expect(resolved).toEqual({
...item,
detail: 'font-size: 0.875rem/* 14px */; line-height: 1.25rem/* 20px */;',
documentation: {
kind: 'markdown',
value:
'```css\n.text-sm {\n font-size: 0.875rem/* 14px */;\n line-height: 1.25rem/* 20px */;\n}\n```',
},
})
})
})

withFixture('basic', (c) => {
let completion = buildCompletion(c)

test('Completions have customizable pixel equivalents (1rem == 10px)', async ({ expect }) => {
await c.updateSettings({
tailwindCSS: {
rootFontSize: 10,
},
})

let result = await completion({
lang: 'html',
text: '<div class=""></div>',
position: { line: 0, character: 12 },
})

let item = result.items.find((item) => item.label === 'text-sm')

let resolved = await c.sendRequest('completionItem/resolve', item)

expect(resolved).toEqual({
...item,
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
documentation: {
kind: 'markdown',
value:
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
},
})
})
})

withFixture('basic', (c) => {
let completion = buildCompletion(c)

test('Completions have color equivalents presented as hex', async ({ expect }) => {
let result = await completion({
lang: 'html',
text: '<div class=""></div>',
position: { line: 0, character: 12 },
})

let item = result.items.find((item) => item.label === 'bg-red-500')

let resolved = await c.sendRequest('completionItem/resolve', item)

expect(resolved).toEqual({
...item,
detail: '--tw-bg-opacity: 1; background-color: rgb(239 68 68 / var(--tw-bg-opacity));',
documentation: '#ef4444',
})
})
})

withFixture('overrides-variants', (c) => {
let completion = buildCompletion(c)

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

withFixture('v4/basic', (c) => {
async function completion({
lang,
text,
position,
context = {
triggerKind: 1,
},
settings,
}) {
let textDocument = await c.openDocument({ text, lang, settings })

return c.sendRequest('textDocument/completion', {
textDocument,
position,
context,
})
}
let completion = buildCompletion(c)

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

expect(resolved).toEqual({
...item,
detail: 'text-transform: uppercase',
detail: 'text-transform: uppercase;',
documentation: {
kind: 'markdown',
value: '```css\n.uppercase {\n text-transform: uppercase;\n}\n```',
},
})
})
})

withFixture('v4/basic', (c) => {
let completion = buildCompletion(c)

test('Completions have customizable pixel equivalents (1rem == 10px)', async ({ expect }) => {
await c.updateSettings({
tailwindCSS: {
rootFontSize: 10,
},
})

let result = await completion({
lang: 'html',
text: '<div class=""></div>',
position: { line: 0, character: 12 },
})

let item = result.items.find((item) => item.label === 'text-sm')

let resolved = await c.sendRequest('completionItem/resolve', item)

expect(resolved).toEqual({
...item,
detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;',
documentation: {
kind: 'markdown',
value:
'```css\n.text-sm {\n font-size: 0.875rem/* 8.75px */;\n line-height: 1.25rem/* 12.5px */;\n}\n```',
},
})
})
})

withFixture('v4/basic', (c) => {
let completion = buildCompletion(c)

test('Completions have color equivalents presented as hex', async ({ expect }) => {
let result = await completion({
lang: 'html',
text: '<div class=""></div>',
position: { line: 0, character: 12 },
})

let item = result.items.find((item) => item.label === 'bg-red-500')

let resolved = await c.sendRequest('completionItem/resolve', item)

expect(resolved).toEqual({
...item,
detail: 'background-color: #ef4444;',
documentation: '#ef4444',
})
})
})
Expand Up @@ -13,7 +13,7 @@ withFixture('multi-config-content', (c) => {
contents: {
language: 'css',
value:
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity));\n}',
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity))/* #ff0000 */;\n}',
},
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
})
Expand All @@ -30,7 +30,7 @@ withFixture('multi-config-content', (c) => {
contents: {
language: 'css',
value:
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity));\n}',
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity))/* #0000ff */;\n}',
},
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
})
Expand Down
Expand Up @@ -13,7 +13,7 @@ withFixture('multi-config', (c) => {
contents: {
language: 'css',
value:
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity));\n}',
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity))/* #ff0000 */;\n}',
},
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
})
Expand All @@ -30,7 +30,7 @@ withFixture('multi-config', (c) => {
contents: {
language: 'css',
value:
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity));\n}',
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity))/* #0000ff */;\n}',
},
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
})
Expand Down
Expand Up @@ -38,7 +38,7 @@ withFixture('basic', (c) => {
expected:
'.bg-red-500 {\n' +
' --tw-bg-opacity: 1;\n' +
' background-color: rgb(239 68 68 / var(--tw-bg-opacity));\n' +
' background-color: rgb(239 68 68 / var(--tw-bg-opacity))/* #ef4444 */;\n' +
'}',
expectedRange: {
start: { line: 0, character: 12 },
Expand Down
24 changes: 13 additions & 11 deletions packages/tailwindcss-language-service/src/completionProvider.ts
Expand Up @@ -12,7 +12,7 @@ import {
import type { TextDocument } from 'vscode-languageserver-textdocument'
import dlv from 'dlv'
import removeMeta from './util/removeMeta'
import { getColor, getColorFromValue } from './util/color'
import { formatColor, getColor, getColorFromValue } from './util/color'
import { isHtmlContext } from './util/html'
import { isCssContext } from './util/css'
import { findLast, matchClassAttributes } from './util/find'
Expand All @@ -33,7 +33,6 @@ import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled'
import * as jit from './util/jit'
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
import * as culori from 'culori'
import {
addPixelEquivalentsToMediaQuery,
addPixelEquivalentsToValue,
Expand Down Expand Up @@ -102,9 +101,10 @@ export function completionsFromClassList(
if (color !== null) {
kind = CompletionItemKind.Color
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}
}

return {
label: className,
...(documentation ? { documentation } : {}),
Expand Down Expand Up @@ -298,7 +298,7 @@ export function completionsFromClassList(
let documentation: string | undefined

if (color && typeof color !== 'string') {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}

items.push({
Expand Down Expand Up @@ -367,7 +367,7 @@ export function completionsFromClassList(
if (color !== null) {
kind = CompletionItemKind.Color
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}
}

Expand Down Expand Up @@ -528,7 +528,7 @@ export function completionsFromClassList(
let documentation: string | undefined

if (color && typeof color !== 'string') {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}

items.push({
Expand Down Expand Up @@ -575,7 +575,7 @@ export function completionsFromClassList(
if (color !== null) {
kind = CompletionItemKind.Color
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}
}

Expand Down Expand Up @@ -661,7 +661,7 @@ export function completionsFromClassList(
if (color !== null) {
kind = CompletionItemKind.Color
if (typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
documentation = culori.formatRgb(color)
documentation = formatColor(color)
}
}

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

item.detail = state.designSystem.toCss(decls)
item.detail = await jit.stringifyDecls(state, postcss.rule({
nodes: decls,
}))
} else {
item.detail = `${rules.length} rules`
}
Expand All @@ -1736,7 +1738,7 @@ export async function resolveCompletionItem(
if (!item.documentation) {
item.documentation = {
kind: 'markdown' as typeof MarkupKind.Markdown,
value: ['```css', state.designSystem.toCss(rules), '```'].join('\n'),
value: ['```css', await jit.stringifyRoot(state, postcss.root({ nodes: rules })), '```'].join('\n'),
}
}

Expand Down
10 changes: 9 additions & 1 deletion packages/tailwindcss-language-service/src/util/color.ts
@@ -1,7 +1,7 @@
import dlv from 'dlv'
import { State } from './state'
import removeMeta from './removeMeta'
import { ensureArray, dedupe, flatten } from './array'
import { ensureArray, dedupe } from './array'
import type { Color } from 'vscode-languageserver'
import { getClassNameParts } from './getClassNameAtPosition'
import * as jit from './jit'
Expand Down Expand Up @@ -229,3 +229,11 @@ export function culoriColorToVscodeColor(color: culori.Color): Color {
let rgb = toRgb(color)
return { red: rgb.r, green: rgb.g, blue: rgb.b, alpha: rgb.alpha ?? 1 }
}

export function formatColor(color: culori.Color): string {
if (color.alpha === undefined || color.alpha === 1) {
return culori.formatHex(color)
}

return culori.formatHex8(color)
}

0 comments on commit 4b46fc5

Please sign in to comment.