From 37c4b96edaf8ed95367fe964213497a242105f01 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 26 Mar 2024 10:44:03 -0400 Subject: [PATCH] Show pixel equivalents in completions and hovers of the `theme()` helper (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor * Don’t wrap string theme values in quotes * Transform shown theme values when possible This internal helper is what Tailwind uses to emit the value into the CSS. * Show pixel equivlents for theme values * Add spaces to equivalents comments * Update changelog --- .../src/projects.ts | 10 ++++ .../tests/completions/completions.test.js | 12 ++--- .../tests/env/multi-config-content.test.js | 4 +- .../tests/env/multi-config.test.js | 4 +- .../tests/hover/hover.test.js | 52 +++++++++++++------ .../getInvalidConfigPathDiagnostics.ts | 10 +++- .../src/hoverProvider.ts | 40 ++++++++------ .../src/util/comments.ts | 2 +- .../src/util/pixelEquivalents.ts | 2 +- .../src/util/state.ts | 1 + .../src/util/stringify.ts | 1 + packages/vscode-tailwindcss/CHANGELOG.md | 1 + 12 files changed, 95 insertions(+), 44 deletions(-) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index fc15964f..0d923154 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -415,6 +415,7 @@ export async function createProjectService( let pluginVersions: string | undefined let browserslist: string[] | undefined let resolveConfigFn: (config: any) => any + let transformThemeValueFn: (section: any) => (value: any) => any let loadConfigFn: (path: string) => any let featureFlags: FeatureFlags = { future: [], experimental: [] } let applyComplexClasses: any @@ -513,6 +514,13 @@ export async function createProjectService( } } + try { + let fn = require(resolveFrom(tailwindDir, './lib/util/transformThemeValue.js')) + transformThemeValueFn = fn.default ?? fn + } catch { + // + } + try { loadConfigFn = require(resolveFrom(tailwindDir, './loadConfig.js')) } catch {} @@ -633,6 +641,7 @@ export async function createProjectService( console.error(util.format(error)) tailwindcss = require('tailwindcss') resolveConfigFn = require('tailwindcss/resolveConfig') + transformThemeValueFn = require('tailwindcss/lib/util/transformThemeValue').default loadConfigFn = require('tailwindcss/loadConfig') postcss = require('postcss') tailwindcssVersion = require('tailwindcss/package.json').version @@ -660,6 +669,7 @@ export async function createProjectService( postcssSelectorParser: { module: postcssSelectorParser }, resolveConfig: { module: resolveConfigFn }, loadConfig: { module: loadConfigFn }, + transformThemeValue: { module: transformThemeValueFn }, jit: jitModules, } state.browserslist = browserslist diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 2e3de946..656841a5 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -222,11 +222,11 @@ withFixture('basic', (c) => { expect(resolved).toEqual({ ...item, - detail: 'font-size: 0.875rem/* 14px */; line-height: 1.25rem/* 20px */;', + 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```', + '```css\n.text-sm {\n font-size: 0.875rem /* 14px */;\n line-height: 1.25rem /* 20px */;\n}\n```', }, }) }) @@ -254,11 +254,11 @@ withFixture('basic', (c) => { expect(resolved).toEqual({ ...item, - detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;', + 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```', + '```css\n.text-sm {\n font-size: 0.875rem /* 8.75px */;\n line-height: 1.25rem /* 12.5px */;\n}\n```', }, }) }) @@ -521,11 +521,11 @@ withFixture('v4/basic', (c) => { expect(resolved).toEqual({ ...item, - detail: 'font-size: 0.875rem/* 8.75px */; line-height: 1.25rem/* 12.5px */;', + 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```', + '```css\n.text-sm {\n font-size: 0.875rem /* 8.75px */;\n line-height: 1.25rem /* 12.5px */;\n}\n```', }, }) }) diff --git a/packages/tailwindcss-language-server/tests/env/multi-config-content.test.js b/packages/tailwindcss-language-server/tests/env/multi-config-content.test.js index 591a3325..0bdccd13 100644 --- a/packages/tailwindcss-language-server/tests/env/multi-config-content.test.js +++ b/packages/tailwindcss-language-server/tests/env/multi-config-content.test.js @@ -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))/* #ff0000 */;\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 } }, }) @@ -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))/* #0000ff */;\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 } }, }) diff --git a/packages/tailwindcss-language-server/tests/env/multi-config.test.js b/packages/tailwindcss-language-server/tests/env/multi-config.test.js index 9d34eb62..5050314c 100644 --- a/packages/tailwindcss-language-server/tests/env/multi-config.test.js +++ b/packages/tailwindcss-language-server/tests/env/multi-config.test.js @@ -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))/* #ff0000 */;\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 } }, }) @@ -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))/* #0000ff */;\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 } }, }) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index f1b847c3..0e8289c6 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -2,7 +2,7 @@ import { test } from 'vitest' import { withFixture } from '../common' withFixture('basic', (c) => { - async function testHover(name, { text, lang, position, 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', { @@ -10,17 +10,17 @@ withFixture('basic', (c) => { position, }) - expect(res).toEqual( - expected - ? { - contents: { - language: 'css', - value: expected, - }, - range: expectedRange, - } - : expected, - ) + if (!exact && expected) { + expected = { + contents: { + language: 'css', + value: expected, + }, + range: expectedRange, + } + } + + expect(res).toEqual(expected) }) } @@ -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))/* #ef4444 */;\n' + + ' background-color: rgb(239 68 68 / var(--tw-bg-opacity)) /* #ef4444 */;\n' + '}', expectedRange: { start: { line: 0, character: 12 }, @@ -59,7 +59,7 @@ withFixture('basic', (c) => { testHover('arbitrary value with theme function', { text: '
', position: { line: 0, character: 13 }, - expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem/* 16px */;\n' + '}', + expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}', expectedRange: { start: { line: 0, character: 12 }, end: { line: 0, character: 32 }, @@ -89,6 +89,28 @@ withFixture('basic', (c) => { end: { line: 2, character: 18 }, }, }) + + testHover('showPixelEquivalents works with theme()', { + lang: 'tailwindcss', + text: `.foo { font-size: theme(fontSize.xl) }`, + position: { line: 0, character: 32 }, + + exact: true, + expected: { + contents: { + kind: 'markdown', + value: [ + '```plaintext', + '1.25rem /* 20px */', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 24 }, + end: { line: 0, character: 35 }, + } + }, + }) }) withFixture('v4/basic', (c) => { @@ -146,7 +168,7 @@ withFixture('v4/basic', (c) => { // testHover('arbitrary value with theme function', { // text: '
', // position: { line: 0, character: 13 }, - // expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem/* 16px */;\n' + '}', + // expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}', // expectedRange: { // start: { line: 0, character: 12 }, // end: { line: 0, character: 32 }, diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts index 1f8941ae..c3e79308 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts @@ -23,9 +23,17 @@ export function validateConfigPath( base: string[] = [], ): { isValid: true; value: any } | { isValid: false; reason: string; suggestions: string[] } { let keys = Array.isArray(path) ? path : stringToPath(path) - let value = dlv(state.config, [...base, ...keys]) + let fullPath = [...base, ...keys] + let value = dlv(state.config, fullPath) let suggestions: string[] = [] + // This property may not exist in the state object because of compatability with Tailwind Play + let transformThemeValue = state.modules?.transformThemeValue?.module ?? ((_: any) => (value: any) => value) + + if (fullPath[0] === 'theme' && fullPath[1]) { + value = transformThemeValue(fullPath[1])(value) + } + function findAlternativePath(): string[] { let points = combinations('123456789'.substr(0, keys.length - 1)).map((x) => x.split('').map((x) => parseInt(x, 10)), diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index d3869337..6ed47ec5 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -10,6 +10,7 @@ import * as jit from './util/jit' import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics' import { isWithinRange } from './util/isWithinRange' import type { TextDocument } from 'vscode-languageserver-textdocument' +import { addPixelEquivalentsToValue } from './util/pixelEquivalents' export async function doHover( state: State, @@ -18,35 +19,42 @@ export async function doHover( ): Promise { return ( (await provideClassNameHover(state, document, position)) || - provideCssHelperHover(state, document, position) + (await provideCssHelperHover(state, document, position)) ) } -function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover { +async function provideCssHelperHover(state: State, document: TextDocument, position: Position): Promise { if (!isCssContext(state, document, position)) { return null } + const settings = await state.editor.getConfiguration(document.uri) + let helperFns = findHelperFunctionsInRange(document, { start: { line: position.line, character: 0 }, end: { line: position.line + 1, character: 0 }, }) for (let helperFn of helperFns) { - if (isWithinRange(position, helperFn.ranges.path)) { - let validated = validateConfigPath( - state, - helperFn.path, - helperFn.helper === 'theme' ? ['theme'] : [], - ) - let value = validated.isValid ? stringifyConfigValue(validated.value) : null - if (value === null) { - return null - } - return { - contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, - range: helperFn.ranges.path, - } + if (!isWithinRange(position, helperFn.ranges.path)) continue + + let validated = validateConfigPath( + state, + helperFn.path, + helperFn.helper === 'theme' ? ['theme'] : [], + ) + + // This property may not exist in the state object because of compatability with Tailwind Play + let value = validated.isValid ? stringifyConfigValue(validated.value) : null + if (value === null) return null + + if (settings.tailwindCSS.showPixelEquivalents) { + value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize) + } + + return { + contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, + range: helperFn.ranges.path, } } diff --git a/packages/tailwindcss-language-service/src/util/comments.ts b/packages/tailwindcss-language-service/src/util/comments.ts index fef04267..1a989643 100644 --- a/packages/tailwindcss-language-service/src/util/comments.ts +++ b/packages/tailwindcss-language-service/src/util/comments.ts @@ -5,7 +5,7 @@ export function applyComments(str: string, comments: Comment[]): string { for (let comment of comments) { let index = comment.index + offset - let commentStr = `/* ${comment.value} */` + let commentStr = ` /* ${comment.value} */` str = str.slice(0, index) + commentStr + str.slice(index) offset += commentStr.length } diff --git a/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts b/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts index b562c33d..1901923c 100644 --- a/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts +++ b/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts @@ -20,7 +20,7 @@ export function addPixelEquivalentsToValue(value: string, rootFontSize: number): return false } - let commentStr = `/* ${parseFloat(unit.number) * rootFontSize}px */` + let commentStr = ` /* ${parseFloat(unit.number) * rootFontSize}px */` value = value.slice(0, node.sourceEndIndex) + commentStr + value.slice(node.sourceEndIndex) return false diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 3f94f259..abe8863c 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -107,6 +107,7 @@ export interface State { postcss?: { version: string; module: Postcss } postcssSelectorParser?: { module: any } resolveConfig?: { module: any } + transformThemeValue?: { module: any } loadConfig?: { module: any } jit?: { generateRules: { module: any } diff --git a/packages/tailwindcss-language-service/src/util/stringify.ts b/packages/tailwindcss-language-service/src/util/stringify.ts index eaa70c2e..725d224d 100644 --- a/packages/tailwindcss-language-service/src/util/stringify.ts +++ b/packages/tailwindcss-language-service/src/util/stringify.ts @@ -11,6 +11,7 @@ import { addEquivalents } from './equivalents' export function stringifyConfigValue(x: any): string { if (isObject(x)) return `${Object.keys(x).length} values` if (typeof x === 'function') return 'ƒ' + if (typeof x === 'string') return x return stringifyObject(x, { inlineCharacterLimit: Infinity, singleQuotes: false, diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 8773adba..75602d67 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -6,6 +6,7 @@ - Support Astro's `class:list` attribute by default (#890) - Fix hovers and CSS conflict detection in Vue `