From 460d921ca34fe7b52966a02dadb0a505f82fb3f7 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 2 May 2023 10:44:37 +0100 Subject: [PATCH] Include pixel equivalents in more places (#775) * Show pixel equivalents for more `rem`/`em` values * Add pixel equivalents to media query variant completions --- package-lock.json | 64 ++++++++ .../tailwindcss-language-service/package.json | 4 + .../src/completionProvider.ts | 51 +++--- .../src/util/jit.ts | 22 ++- .../src/util/pixelEquivalents.ts | 147 ++++++++++++++++++ .../src/util/remToPx.ts | 9 -- .../src/util/stringify.ts | 13 +- 7 files changed, 263 insertions(+), 47 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/util/pixelEquivalents.ts delete mode 100644 packages/tailwindcss-language-service/src/util/remToPx.ts diff --git a/package-lock.json b/package-lock.json index 9a9fb24..f02b088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,9 @@ "": { "name": "root", "dependencies": { + "@csstools/css-parser-algorithms": "2.1.1", + "@csstools/css-tokenizer": "2.1.1", + "@csstools/media-query-list-parser": "2.0.4", "@parcel/watcher": "2.0.3", "@tailwindcss/aspect-ratio": "0.4.2", "@tailwindcss/container-queries": "0.1.0", @@ -46,6 +49,7 @@ "postcss": "8.3.9", "postcss-load-config": "3.0.1", "postcss-selector-parser": "6.0.2", + "postcss-value-parser": "4.2.0", "prettier": "2.3.0", "resolve": "1.20.0", "rimraf": "3.0.2", @@ -1716,6 +1720,49 @@ "node": ">=0.1.95" } }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz", + "integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.1.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz", + "integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz", + "integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1" + } + }, "node_modules/@evocateur/libnpmaccess": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", @@ -22936,6 +22983,23 @@ "minimist": "^1.2.0" } }, + "@csstools/css-parser-algorithms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz", + "integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==", + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz", + "integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==" + }, + "@csstools/media-query-list-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz", + "integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==", + "requires": {} + }, "@evocateur/libnpmaccess": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz", diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 84a48f8..3471245 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -14,6 +14,9 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@csstools/media-query-list-parser": "2.0.4", + "@csstools/css-parser-algorithms": "2.1.1", + "@csstools/css-tokenizer": "2.1.1", "@types/culori": "^2.0.0", "@types/moo": "0.5.3", "@types/semver": "7.3.10", @@ -28,6 +31,7 @@ "moo": "0.5.1", "postcss": "8.3.9", "postcss-selector-parser": "6.0.2", + "postcss-value-parser": "4.2.0", "semver": "7.3.7", "sift-string": "0.0.2", "stringify-object": "3.3.0", diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 89cf8f4..86bb582 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -28,11 +28,14 @@ import { ensureArray } from './util/array' import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers' import { validateApply } from './util/validateApply' import { flagEnabled } from './util/flagEnabled' -import { remToPx } from './util/remToPx' import * as jit from './util/jit' import { getVariantsFromClassName } from './util/getVariantsFromClassName' import * as culori from 'culori' import Regex from 'becke-ch--regex--s0-0-v1--base--pl--lib' +import { + addPixelEquivalentsToMediaQuery, + addPixelEquivalentsToValue, +} from './util/pixelEquivalents' let isUtil = (className) => Array.isArray(className.__info) @@ -43,6 +46,7 @@ export function completionsFromClassList( state: State, classList: string, classListRange: Range, + rootFontSize: number, filter?: (item: CompletionItem) => boolean, context?: CompletionContext ): CompletionList { @@ -190,7 +194,10 @@ export function completionsFromClassList( items.push( variantItem({ label: `${variant.name}${sep}`, - detail: variant.selectors().join(', '), + detail: variant + .selectors() + .map((selector) => addPixelEquivalentsToMediaQuery(selector, rootFontSize)) + .join(', '), textEditText: resultingVariants[resultingVariants.length - 1] + sep, additionalTextEdits: shouldSortVariants && resultingVariants.length > 1 @@ -430,10 +437,9 @@ async function provideClassAttributeCompletions( end: position, }) - let matches = matchClassAttributes( - str, - (await state.editor.getConfiguration(document.uri)).tailwindCSS.classAttributes - ) + let settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS + + let matches = matchClassAttributes(str, settings.classAttributes) if (matches.length === 0) { return null @@ -470,6 +476,7 @@ async function provideClassAttributeCompletions( }, end: position, }, + settings.rootFontSize, undefined, context ) @@ -544,6 +551,7 @@ async function provideCustomClassNameCompletions( }, end: position, }, + settings.tailwindCSS.rootFontSize, undefined, context ) @@ -555,12 +563,13 @@ async function provideCustomClassNameCompletions( return null } -function provideAtApplyCompletions( +async function provideAtApplyCompletions( state: State, document: TextDocument, position: Position, context?: CompletionContext -): CompletionList { +): Promise { + let settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS let str = document.getText({ start: { line: Math.max(position.line - 30, 0), character: 0 }, end: position, @@ -584,6 +593,7 @@ function provideAtApplyCompletions( }, end: position, }, + settings.rootFontSize, (item) => { if (item.kind === 9) { return ( @@ -1318,13 +1328,18 @@ async function provideEmmetCompletions( const parts = emmetItems.items[0].label.split('.') if (parts.length < 2) return null - return completionsFromClassList(state, parts[parts.length - 1], { - start: { - line: position.line, - character: position.character - parts[parts.length - 1].length, + return completionsFromClassList( + state, + parts[parts.length - 1], + { + start: { + line: position.line, + character: position.character - parts[parts.length - 1].length, + }, + end: position, }, - end: position, - }) + settings.tailwindCSS.rootFontSize + ) } export async function doComplete( @@ -1444,10 +1459,10 @@ function stringifyDecls(obj: any, settings: Settings): string { .map((prop) => ensureArray(obj[prop]) .map((value) => { - const px = settings.tailwindCSS.showPixelEquivalents - ? remToPx(value, settings.tailwindCSS.rootFontSize) - : undefined - return `${prop}: ${value}${px ? `/* ${px} */` : ''};` + if (settings.tailwindCSS.showPixelEquivalents) { + value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize) + } + return `${prop}: ${value};` }) .join(' ') ) diff --git a/packages/tailwindcss-language-service/src/util/jit.ts b/packages/tailwindcss-language-service/src/util/jit.ts index 8ad7a30..5dcd8d3 100644 --- a/packages/tailwindcss-language-service/src/util/jit.ts +++ b/packages/tailwindcss-language-service/src/util/jit.ts @@ -1,6 +1,6 @@ import { State } from './state' import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss' -import { remToPx } from './remToPx' +import { addPixelEquivalentsToCss, addPixelEquivalentsToValue } from './pixelEquivalents' export function bigSign(bigIntValue) { // @ts-ignore @@ -41,17 +41,13 @@ export async function stringifyRoot(state: State, root: Root, uri?: string): Pro node.remove() }) + let css = clone.toString() + if (settings.tailwindCSS.showPixelEquivalents) { - clone.walkDecls((decl) => { - let px = remToPx(decl.value, settings.tailwindCSS.rootFontSize) - if (px) { - decl.value = `${decl.value}/* ${px} */` - } - }) + css = addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize) } - return clone - .toString() + return css .replace(/([^;{}\s])(\n\s*})/g, (_match, before, after) => `${before};${after}`) .replace(/^(?: )+/gm, (indent: string) => ' '.repeat((indent.length / 4) * settings.editor.tabSize) @@ -70,10 +66,10 @@ export async function stringifyDecls(state: State, rule: Rule, uri?: string): Pr let result = [] rule.walkDecls(({ prop, value }) => { - let px = settings.tailwindCSS.showPixelEquivalents - ? remToPx(value, settings.tailwindCSS.rootFontSize) - : undefined - result.push(`${prop}: ${value}${px ? `/* ${px} */` : ''};`) + if (settings.tailwindCSS.showPixelEquivalents) { + value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize) + } + result.push(`${prop}: ${value};`) }) return result.join(' ') } diff --git a/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts b/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts new file mode 100644 index 0000000..2de7f30 --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/pixelEquivalents.ts @@ -0,0 +1,147 @@ +import type { Plugin } from 'postcss' +import parseValue from 'postcss-value-parser' +import { parse as parseMediaQueryList } from '@csstools/media-query-list-parser' +import postcss from 'postcss' +import { isTokenNode } from '@csstools/css-parser-algorithms' + +type Comment = { index: number; value: string } + +export function addPixelEquivalentsToValue(value: string, rootFontSize: number): string { + if (!value.includes('rem')) { + return value + } + + parseValue(value).walk((node) => { + if (node.type !== 'word') { + return true + } + + let unit = parseValue.unit(node.value) + if (!unit || unit.unit !== 'rem') { + return false + } + + let commentStr = `/* ${parseFloat(unit.number) * rootFontSize}px */` + value = value.slice(0, node.sourceEndIndex) + commentStr + value.slice(node.sourceEndIndex) + + return false + }) + + return value +} + +export function addPixelEquivalentsToCss(css: string, rootFontSize: number): string { + if (!css.includes('em')) { + return css + } + + let comments: Comment[] = [] + + try { + postcss([postcssPlugin({ comments, rootFontSize })]).process(css, { from: undefined }).css + } catch { + return css + } + + return applyComments(css, comments) +} + +function applyComments(str: string, comments: Comment[]): string { + let offset = 0 + + for (let comment of comments) { + let index = comment.index + offset + let commentStr = `/* ${comment.value} */` + str = str.slice(0, index) + commentStr + str.slice(index) + offset += commentStr.length + } + + return str +} + +function getPixelEquivalentsForMediaQuery(params: string, rootFontSize: number): Comment[] { + let comments: Comment[] = [] + + try { + parseMediaQueryList(params).forEach((mediaQuery) => { + mediaQuery.walk(({ node }) => { + if ( + isTokenNode(node) && + node.type === 'token' && + node.value[0] === 'dimension-token' && + (node.value[4].type === 'integer' || node.value[4].type === 'number') && + (node.value[4].unit === 'rem' || node.value[4].unit === 'em') + ) { + comments.push({ + index: params.length - (params.length - node.value[3] - 1), + value: `${node.value[4].value * rootFontSize}px`, + }) + } + }) + }) + } catch {} + + return comments +} + +export function addPixelEquivalentsToMediaQuery(query: string, rootFontSize: number): string { + return query.replace(/(?<=^\s*@media\s*).*?$/, (params) => { + let comments = getPixelEquivalentsForMediaQuery(params, rootFontSize) + return applyComments(params, comments) + }) +} + +function postcssPlugin({ + comments, + rootFontSize, +}: { + comments: Comment[] + rootFontSize: number +}): Plugin { + return { + postcssPlugin: 'plugin', + AtRule: { + media(atRule) { + if (!atRule.params.includes('em')) { + return + } + + comments.push( + ...getPixelEquivalentsForMediaQuery(atRule.params, rootFontSize).map( + ({ index, value }) => ({ + index: index + atRule.source.start.offset + `@media${atRule.raws.afterName}`.length, + value, + }) + ) + ) + }, + }, + Declaration(decl) { + if (!decl.value.includes('rem')) { + return + } + + parseValue(decl.value).walk((node) => { + if (node.type !== 'word') { + return true + } + + let unit = parseValue.unit(node.value) + if (!unit || unit.unit !== 'rem') { + return false + } + + comments.push({ + index: + decl.source.start.offset + + `${decl.prop}${decl.raws.between}`.length + + node.sourceEndIndex, + value: `${parseFloat(unit.number) * rootFontSize}px`, + }) + + return false + }) + }, + } +} +postcssPlugin.postcss = true diff --git a/packages/tailwindcss-language-service/src/util/remToPx.ts b/packages/tailwindcss-language-service/src/util/remToPx.ts deleted file mode 100644 index 98cd592..0000000 --- a/packages/tailwindcss-language-service/src/util/remToPx.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function remToPx(value: string, rootSize: number = 16): string | undefined { - if (/^-?[0-9.]+rem$/.test(value)) { - let number = parseFloat(value.substr(0, value.length - 3)) - if (!isNaN(number)) { - return `${number * rootSize}px` - } - } - return undefined -} diff --git a/packages/tailwindcss-language-service/src/util/stringify.ts b/packages/tailwindcss-language-service/src/util/stringify.ts index 59a0e7b..ab13216 100644 --- a/packages/tailwindcss-language-service/src/util/stringify.ts +++ b/packages/tailwindcss-language-service/src/util/stringify.ts @@ -2,10 +2,10 @@ import removeMeta from './removeMeta' import dlv from 'dlv' import escapeClassName from 'css.escape' import { ensureArray } from './array' -import { remToPx } from './remToPx' import stringifyObject from 'stringify-object' import isObject from './isObject' import { Settings } from './state' +import { addPixelEquivalentsToCss } from './pixelEquivalents' export function stringifyConfigValue(x: any): string { if (isObject(x)) return `${Object.keys(x).length} values` @@ -45,12 +45,7 @@ export function stringifyCss(className: string, obj: any, settings: Settings): s const indentStr = indent.repeat(context.length) const decls = props.reduce((acc, curr, i) => { const propStr = ensureArray(obj[curr]) - .map((val) => { - const px = settings.tailwindCSS.showPixelEquivalents - ? remToPx(val, settings.tailwindCSS.rootFontSize) - : undefined - return `${indentStr + indent}${curr}: ${val}${px ? `/* ${px} */` : ''};` - }) + .map((val) => `${indentStr + indent}${curr}: ${val};`) .join('\n') return `${acc}${i === 0 ? '' : '\n'}${propStr}` }, '') @@ -60,6 +55,10 @@ export function stringifyCss(className: string, obj: any, settings: Settings): s css += `${indent.repeat(i)}\n}` } + if (settings.tailwindCSS.showPixelEquivalents) { + return addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize) + } + return css }