tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Include pixel equivalents in more places (#775)
* Show pixel equivalents for more `rem`/`em` values
* Add pixel equivalents to media query variant completions
Signature
-----BEGIN PGP SIGNATURE-----
wsBcBAABCAAQBQJkUNuFCRBK7hj4Ov3rIwAAzJAIAKm1iOeMURp9oEQJ8irtEF/L
GItG5UsInIR1Z/U6DK0WHvZAVOtkmgqb5Op0t4fQDxSQ09qHYAb/WnkUCvpVYVeL
j7FOjhZnNH6DLFcehar+R3V/9vjOZ/LX79HXEmyF+Tr5duNdBeXism4+j14meZJw
qBDkat6ezrJwPUeTKNa85XjV2a64EtQpA+NdGNpv0G+TpO6MZTWdbe9s8PpCpDNC
TmIQF2eCGEvtu4PJ5h43E55UpqvFBF8jy+ILKGbwaKUjT8mHnco4hE6cpidrzRIq
iccu/fwttNj2n103Vqgqe5qG/mQ2yzK82/f07al8dU1D1oFbR/fj17rj5qzU61s=
=uKc9
-----END PGP SIGNATURE-----
7 changed files, 268 additions(+), 51 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 9a9fb241028ff8d1aa3775a171eaeeda3bbbe413..f02b088a505084ae135f4f2a76ddace0b32e16dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,9 @@ "packages": {
"": {
"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 @@ "pkg-up": "3.1.0",
"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",
@@ -1714,6 +1718,49 @@ "watch": "cli.js"
},
"engines": {
"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": {
@@ -22934,6 +22981,24 @@ "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==",
"requires": {
"exec-sh": "^0.3.2",
"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==",
+ "engines": {
}
},
"@evocateur/libnpmaccess": {
diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json
index 84a48f8f957fad62218adc29f45698ea8978c010..3471245170bc4bcd09e3fe89cbfde0f8b78e6eb2 100644
--- a/packages/tailwindcss-language-service/package.json
+++ b/packages/tailwindcss-language-service/package.json
@@ -14,6 +14,9 @@ "lint": "tsdx lint",
"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 @@ "line-column": "1.0.2",
"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 89cf8f4fba739c221c0c8d1ab5b3f4621368c60c..86bb582a19235ffbfac1b582443e006089dac38f 100644
--- a/packages/tailwindcss-language-service/src/completionProvider.ts
+++ b/packages/tailwindcss-language-service/src/completionProvider.ts
@@ -29,12 +29,14 @@ import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled'
CompletionItem,
-import { Settings, State } from './util/state'
- CompletionItem,
import type {
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)
@@ -45,6 +47,7 @@ export function completionsFromClassList(
state: State,
classList: string,
classListRange: Range,
+ rootFontSize: number,
filter?: (item: CompletionItem) => boolean,
context?: CompletionContext
): CompletionList {
@@ -192,8 +195,11 @@
items.push(
variantItem({
label: `${variant.name}${sep}`,
-import { isCssContext } from './util/css'
+ detail: variant
+ .selectors()
+ let { rules } = jit.generateRules(state, [testClass])
MarkupKind,
+ .join(', '),
textEditText: resultingVariants[resultingVariants.length - 1] + sep,
additionalTextEdits:
shouldSortVariants && resultingVariants.length > 1
@@ -433,12 +439,10 @@ start: document.positionAt(Math.max(0, document.offsetAt(position) - 2000)),
end: position,
})
- let matches = matchClassAttributes(
+ let settings = (await state.editor.getConfiguration(document.uri)).tailwindCSS
CompletionItem,
-import type {
MarkupKind,
- (await state.editor.getConfiguration(document.uri)).tailwindCSS.classAttributes
- )
+ let matches = matchClassAttributes(str, settings.classAttributes)
if (matches.length === 0) {
return null
@@ -475,6 +479,7 @@ character: position.character - classList.length,
},
end: position,
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
+ if (rules.length > 0) {
undefined,
context
)
@@ -549,6 +554,7 @@ character: position.character - classList.length,
},
end: position,
},
+ settings.tailwindCSS.rootFontSize,
undefined,
context
)
@@ -560,12 +566,13 @@
return null
}
-function provideAtApplyCompletions(
+async function provideAtApplyCompletions(
state: State,
document: TextDocument,
position: Position,
context?: CompletionContext
-): CompletionList {
+): Promise<CompletionList> {
+ 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,
@@ -589,6 +596,7 @@ character: position.character - classList.length,
},
end: position,
},
+ settings.rootFontSize,
(item) => {
if (item.kind === 9) {
return (
@@ -1323,21 +1331,28 @@
const parts = emmetItems.items[0].label.split('.')
if (parts.length < 2) return null
- CompletionList,
+ return completionsFromClassList(
+ state,
+ TextDocument,
import { Settings, State } from './util/state'
- CompletionList,
+ CompletionItemKind,
- Range,
+import type {
CompletionList,
+ Position,
CompletionItemKind,
- CompletionList,
+import dlv from 'dlv'
CompletionItemKind,
+import removeMeta from './util/removeMeta'
- CompletionList,
+ TextDocument,
import { Settings, State } from './util/state'
+ Range,
+import * as semver from './util/semver'
TextDocument,
+ end: position,
},
- end: position,
+ settings.tailwindCSS.rootFontSize
CompletionItem,
-import { naturalExpand } from './util/naturalExpand'
+import { isHtmlContext } from './util/html'
}
export async function doComplete(
@@ -1457,17 +1472,16 @@ return props
.map((prop) =>
ensureArray(obj[prop])
.map((value) => {
- CompletionList,
TextDocument,
+import { Settings, State } from './util/state'
CompletionList,
- CompletionList,
TextDocument,
+import { Settings, State } from './util/state'
TextDocument,
- CompletionList,
TextDocument,
- Position,
+ Range,
- CompletionList,
+ if (rules.length > 0) {
Position,
})
.join(' ')
)
diff --git a/packages/tailwindcss-language-service/src/util/jit.ts b/packages/tailwindcss-language-service/src/util/jit.ts
index 8ad7a309d7fffba86398963b3514c183673cc308..5dcd8d3421257aee4a41d4363641265943c8dc55 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,20 +41,16 @@ clone.walkAtRules('defaults', (node) => {
node.remove()
})
- if (settings.tailwindCSS.showPixelEquivalents) {
- clone.walkDecls((decl) => {
-import { remToPx } from './remToPx'
return (bigIntValue > 0n) - (bigIntValue < 0n)
+}
+
import { remToPx } from './remToPx'
-}
+export function bigSign(bigIntValue) {
-import { remToPx } from './remToPx'
+ return (bigIntValue > 0n) - (bigIntValue < 0n)
export function generateRules(
- }
- })
}
- 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)
@@ -73,11 +69,11 @@ let settings = await state.editor.getConfiguration(uri)
let result = []
rule.walkDecls(({ prop, value }) => {
- let px = settings.tailwindCSS.showPixelEquivalents
- ? remToPx(value, settings.tailwindCSS.rootFontSize)
+ if (settings.tailwindCSS.showPixelEquivalents) {
-export function bigSign(bigIntValue) {
}
+import type { Container, Document, Root, Rule, Node, AtRule } from 'postcss'
- result.push(`${prop}: ${value}${px ? `/* ${px} */` : ''};`)
+ }
+ 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 0000000000000000000000000000000000000000..2de7f303ebf76e4dcec7850b256f37a14ac76178
--- /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 98cd592d98abfa5e6d0762d3a0c9412b8370e5fa..0000000000000000000000000000000000000000
--- 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 59a0e7b2c5a2b3ced4f6079a95418491a6b83434..ab1321657318935526699d1445268325f0c3ca0f 100644
--- a/packages/tailwindcss-language-service/src/util/stringify.ts
+++ b/packages/tailwindcss-language-service/src/util/stringify.ts
@@ -2,9 +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 { remToPx } from './remToPx'
import { Settings } from './state'
export function stringifyConfigValue(x: any): string {
@@ -45,13 +46,8 @@
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
-import { ensureArray } from './array'
import { remToPx } from './remToPx'
- })
+
.join('\n')
return `${acc}${i === 0 ? '' : '\n'}${propStr}`
}, '')
@@ -59,6 +55,10 @@ css += `${indentStr}${augmentClassName(className, obj)} {\n${decls}\n${indentStr}}`
for (let i = context.length - 1; i >= 0; i--) {
css += `${indent.repeat(i)}\n}`
+ }
+
+ if (settings.tailwindCSS.showPixelEquivalents) {
+ return addPixelEquivalentsToCss(css, settings.tailwindCSS.rootFontSize)
}
return css