Skip to content

Commit

Permalink
Show pixel equivalents in completions and hovers of the theme() hel…
Browse files Browse the repository at this point in the history
…per (#935)

* 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
  • Loading branch information
thecrypticace committed Mar 26, 2024
1 parent 381d70a commit 37c4b96
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 44 deletions.
10 changes: 10 additions & 0 deletions packages/tailwindcss-language-server/src/projects.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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```',
},
})
})
Expand Down Expand Up @@ -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```',
},
})
})
Expand Down Expand Up @@ -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```',
},
})
})
Expand Down
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))/* #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 } },
})
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))/* #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 } },
})
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))/* #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 } },
})
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))/* #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 } },
})
Expand Down
52 changes: 37 additions & 15 deletions packages/tailwindcss-language-server/tests/hover/hover.test.js
Expand Up @@ -2,25 +2,25 @@ 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', {
textDocument,
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)
})
}

Expand All @@ -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 },
Expand All @@ -59,7 +59,7 @@ withFixture('basic', (c) => {
testHover('arbitrary value with theme function', {
text: '<div class="p-[theme(spacing.4)]">',
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 },
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -146,7 +168,7 @@ withFixture('v4/basic', (c) => {
// testHover('arbitrary value with theme function', {
// text: '<div class="p-[theme(spacing.4)]">',
// 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 },
Expand Down
Expand Up @@ -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)),
Expand Down
40 changes: 24 additions & 16 deletions packages/tailwindcss-language-service/src/hoverProvider.ts
Expand Up @@ -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,
Expand All @@ -18,35 +19,42 @@ export async function doHover(
): Promise<Hover> {
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<Hover> {
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,
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-service/src/util/comments.ts
Expand Up @@ -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
}
Expand Down
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/tailwindcss-language-service/src/util/state.ts
Expand Up @@ -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 }
Expand Down
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@
- Support Astro's `class:list` attribute by default (#890)
- Fix hovers and CSS conflict detection in Vue `<style lang="sass">` blocks (#930)
- Add support for `<script type="text/babel">` (#932)
- Show pixel equivalents in completions and hovers of the theme() helper (#935)

## 0.10.5

Expand Down

0 comments on commit 37c4b96

Please sign in to comment.