Skip to content

Commit 3d9911e

Browse files
committed
fix: renderTokenColorPreview for semanticTokens
add a guard on the condition name to check it exists chore: use noUncheckedIndexedAccess feat: semantic tokens color preview on completion details also remove completion cache feat: {shorthand} is for {propName} completion details
1 parent a9db1ae commit 3d9911e

17 files changed

+277
-62
lines changed

packages/language-server/src/features/color-hints.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ export function registerColorHints(extension: PandaExtension) {
3636
// Add 1 color hint for each condition
3737
if (match.token.extensions.conditions) {
3838
if (settings['color-hints.semantic-tokens.enabled']) {
39-
Object.values(match.token.extensions.conditions).forEach((value) => {
39+
Object.entries(match.token.extensions.conditions).forEach(([cond, value]) => {
40+
if (!ctx.conditions.get(cond) && cond !== 'base') return
4041
const [tokenRef] = ctx.tokens.getReferences(value)
42+
if (!tokenRef) return
43+
4144
const color = color2kToVsCodeColor(tokenRef.value)
4245
if (!color) return
4346

packages/language-server/src/features/completion.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { PandaExtension } from '../index'
33
import { onError } from '../tokens/error'
44

55
export function registerCompletion(extension: PandaExtension) {
6-
const { connection, documents, documentReady, getClosestCompletionList, getPandaSettings } = extension
6+
const { connection, documents, documentReady, getClosestCompletionList, getCompletionDetails, getPandaSettings } =
7+
extension
78

89
// This handler provides the initial list of the completion items.
910
connection.onCompletion(
@@ -29,5 +30,8 @@ export function registerCompletion(extension: PandaExtension) {
2930
)
3031

3132
// This handler resolves additional information for the item selected in the completion list.
32-
connection.onCompletionResolve((item) => item)
33+
connection.onCompletionResolve(async (item) => {
34+
await getCompletionDetails(item)
35+
return item
36+
})
3337
}

packages/language-server/src/setup-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function setupBuilder(
7575
)
7676
console.log('🐼 Workspaces builders ready !')
7777

78-
if (configPathList.length === 1) {
78+
if (configPathList.length === 1 && configPathList[0]) {
7979
return builderResolver.isContextSynchronizing(configPathList[0])
8080
}
8181
}

packages/language-server/src/tokens/expand-token-fn.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const matchToken = (str: string, callback: (tokenPath: string, match: RegExpExec
77
let match: RegExpExecArray | null
88

99
while ((match = tokenRegex.exec(str)) != null) {
10-
match[1]
10+
// eslint-disable-next-line no-extra-semi
11+
;(match[1] ?? '')
1112
.split(',')
1213
.map((s) => s.trim())
1314
.filter(Boolean)

packages/language-server/src/tokens/is-color.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ export const isColor = (str: string) => {
190190
if (result) {
191191
const flavor = result[1]
192192
const alpha = result[2]
193-
const rh = result[3]
194-
const gs = result[4]
195-
const bl = result[5]
196-
const a = result[6]
193+
const rh = result[3] ?? ''
194+
const gs = result[4] ?? ''
195+
const bl = result[5] ?? ''
196+
const a = result[6] ?? ''
197197

198198
// alpha test
199199
if ((alpha === 'a' && !a) || (a && alpha === '')) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { parseToRgba } from 'color2k'
2+
// import type { PandaVSCodeSettings } from '@pandacss/extension-shared'
3+
4+
// taken from https://github.com/nderscore/tamagui-typescript-plugin/blob/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/src/metadata.ts
5+
6+
const squirclePath = `M 0,12 C 0,0 0,0 12,0 24,0 24,0 24,12 24,24 24,24 12,24 0, 24 0,24 0,12`
7+
8+
const svgCheckerboard = `<defs>
9+
<pattern id="pattern-checker" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
10+
<rect x="0" y="0" width="4" height="4" fill="#fff" />
11+
<rect x="4" y="0" width="4" height="4" fill="#000" />
12+
<rect x="0" y="4" width="4" height="4" fill="#000" />
13+
<rect x="4" y="4" width="4" height="4" fill="#fff" />
14+
</pattern>
15+
</defs>
16+
<path d="${squirclePath}" fill="url(#pattern-checker)" />`
17+
18+
export const makeColorTile = (value: string, size: number = 18) => {
19+
try {
20+
const [_r, _g, _b, alpha] = parseToRgba(value)
21+
const hasAlphaTransparency = alpha !== 1
22+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}">${
23+
hasAlphaTransparency ? svgCheckerboard : ''
24+
}<path d="${squirclePath}" fill="${value}" /></svg>`
25+
const image = `![Image](data:image/svg+xml;base64,${btoa(svg)})`
26+
return image
27+
} catch {
28+
return ''
29+
}
30+
}
31+
32+
export const makeTable = (rows: Record<string, string>[]) => {
33+
const header = rows[0]!
34+
const keys = Object.keys(header)
35+
const renderRow = (row: Record<string, string>) => {
36+
return `| ${keys.map((key) => row[key]).join(' | ')} |`
37+
}
38+
const renderSplitter = () => {
39+
return `| ${keys.map(() => '---').join(' | ')} |`
40+
}
41+
42+
return `${renderRow(header)}\n${renderSplitter()}\n${rows.slice(1).map(renderRow).join('\n')}`
43+
}

packages/language-server/src/tokens/render-token-color-preview.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import { svgToMarkdownLink } from './utils'
66
import { Roboto } from './render-font-size-preview'
77

88
// https://og-playground.vercel.app/?share=5ZTNTsMwDMdfJbLELdI2xHYIgwMfb4DEJZe2cdtAmlRJSqmqvjtNw8ZEGUzihMglsRP_9bMtp4fMCAQGWyFfuCbE-U7hVd-HMyElyqL0jHBYLZdnHGh0t1L4cuYV0tUq6YI_V_i69wfjTlrMvDQ63GZGNZXmEK6HgevrcNgBfEY4rhuVGVm920GKkEnsUG4uGANvEifdR3RYldSPu9TWB5l9U4o5Rlhpkj0X1jRa3BplbIgqLOKY8_5RpCVk8RvgL6BOy-Yk5FQ1eJR4uxiB_0XnOlTKtH-rdRbFj52L-yXXQMHUYTgdsB6m4QZ2vt5QiJDANhcUBKZNASxPlEMKWJkn-dDV4e_w7WSNMrnR_r5KUQDztsGBgk_S8UU5ldBYJWB4Aw
9+
// TODO use raw svg ? or precompile and just replace the color
910
export const renderTokenColorPreview = async (ctx: PandaContext, token: Token) => {
1011
const colors = [] as [tokenName: string, value: string][]
1112

1213
// Only bother displaying a preview for multiple colors
1314
if (token.extensions.conditions) {
1415
Object.entries(token.extensions.conditions).forEach(([conditionName, value]) => {
16+
if (!ctx.conditions.get(conditionName) && conditionName !== 'base') return
1517
const [tokenRef] = ctx.tokens.getReferences(value)
18+
if (!tokenRef) return
1619

1720
colors.push([conditionName, tokenRef.value])
1821
})

packages/language-server/src/tokens/setup-tokens-helpers.ts

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@pandacss/types'
1212
import { CallExpression, Identifier, JsxOpeningElement, JsxSelfClosingElement, Node, SourceFile, ts } from 'ts-morph'
1313

14+
import { type PandaVSCodeSettings } from '@pandacss/extension-shared'
1415
import {
1516
BoxNodeArray,
1617
BoxNodeLiteral,
@@ -28,15 +29,17 @@ import {
2829
type Unboxed,
2930
} from '@pandacss/extractor'
3031
import { type PandaContext } from '@pandacss/node'
31-
import { walkObject } from '@pandacss/shared'
3232
import { type ParserResult } from '@pandacss/parser'
33+
import { walkObject } from '@pandacss/shared'
3334
import { type Token } from '@pandacss/token-dictionary'
3435
import { Bool } from 'lil-fp'
35-
import { type PandaVSCodeSettings } from '@pandacss/extension-shared'
3636
import { match } from 'ts-pattern'
3737
import { color2kToVsCodeColor } from './color2k-to-vscode-color'
3838
import { expandTokenFn, extractTokenPaths } from './expand-token-fn'
3939
import { isColor } from './is-color'
40+
import { makeColorTile, makeTable } from './metadata'
41+
import { getSortText } from './sort-text'
42+
import { traverse } from './traverse'
4043
import { getMarkdownCss, isObjectLike, nodeRangeToVsCodeRange, printTokenValue } from './utils'
4144

4245
type ClosestMatch = {
@@ -48,12 +51,14 @@ type ClosestTokenMatch = ClosestMatch & {
4851
kind: 'token'
4952
token: Token
5053
propValue: PrimitiveType
54+
shorthand: string
5155
}
5256

5357
type ClosestConditionMatch = ClosestMatch & {
5458
kind: 'condition'
5559
condition: RawCondition
5660
propValue: Unboxed['raw']
61+
shorthand: never
5762
}
5863
type ClosestToken = ClosestTokenMatch | ClosestConditionMatch
5964

@@ -165,14 +170,14 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
165170
const propNode = box.isArray(boxNode)
166171
? boxNode.value.find((node) => box.isMap(node) && getNestedBoxProp(node, paths))
167172
: getNestedBoxProp(boxNode, paths)
168-
if (!box.isLiteral(propNode)) return
173+
if (!box.isLiteral(propNode) || !prop) return
169174

170175
const propName = ctx.utility.resolveShorthand(prop)
171176
const token = getTokenFromPropValue(ctx, propName, value)
172177
if (!token) return
173178

174179
const range = nodeRangeToVsCodeRange(propNode.getRange())
175-
onToken?.({ kind: 'token', token, range, propName, propValue: value, propNode })
180+
onToken?.({ kind: 'token', token, range, propName, propValue: value, propNode, shorthand: prop })
176181
})
177182
})
178183
}
@@ -296,7 +301,7 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
296301
const findClosestToken = <Return>(
297302
node: Node,
298303
stack: Node[],
299-
onFoundToken: (args: Pick<ClosestToken, 'propName' | 'propNode'>) => Return,
304+
onFoundToken: (args: Pick<ClosestToken, 'propName' | 'propNode' | 'shorthand'>) => Return,
300305
) => {
301306
const ctx = setup.getContext()
302307
if (!ctx) return
@@ -317,7 +322,7 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
317322

318323
const propName = ctx.utility.resolveShorthand(name)
319324

320-
return onFoundToken({ propName, propNode })
325+
return onFoundToken({ propName, propNode, shorthand: name })
321326
},
322327
)
323328
.when(
@@ -333,7 +338,7 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
333338

334339
const propName = ctx.utility.resolveShorthand(name)
335340

336-
return onFoundToken({ propName, propNode: attrBox })
341+
return onFoundToken({ propName, propNode: attrBox, shorthand: name })
337342
},
338343
)
339344
.otherwise(() => {
@@ -411,10 +416,71 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
411416
const settings = await setup.getPandaSettings()
412417
const { node, stack } = match
413418

414-
return findClosestToken(node, stack, ({ propName, propNode }) => {
415-
if (!box.isLiteral(propNode)) return undefined
416-
return getCompletionFor(ctx, propName, propNode, settings)
417-
})
419+
try {
420+
return await findClosestToken(node, stack, ({ propName, propNode, shorthand }) => {
421+
if (!box.isLiteral(propNode)) return undefined
422+
return getCompletionFor({ ctx, propName, propNode, settings, shorthand })
423+
})
424+
} catch (err) {
425+
console.error(err)
426+
console.trace()
427+
return
428+
}
429+
}
430+
431+
const getCompletionDetails = async (item: CompletionItem) => {
432+
const ctx = setup.getContext()
433+
if (!ctx) return
434+
435+
const settings = await setup.getPandaSettings()
436+
const { propName, token, shorthand } = (item.data ?? {}) as { propName: string; token: Token; shorthand: string }
437+
const markdownCss = getMarkdownCss(ctx, { [propName]: token.value }, settings)
438+
439+
const markdown = [markdownCss.withCss]
440+
if (shorthand !== propName) {
441+
markdown.push(`\`${shorthand}\` is shorthand for \`${propName}\``)
442+
}
443+
444+
const conditions = token.extensions.conditions ?? { base: token.value }
445+
if (conditions) {
446+
const separator = '[___]'
447+
const table = [{ color: ' ', theme: 'Condition', value: 'Value' }]
448+
449+
const tab = '&nbsp;&nbsp;&nbsp;&nbsp;'
450+
traverse(
451+
conditions,
452+
({ key: cond, value, depth }) => {
453+
if (!ctx.conditions.get(cond) && cond !== 'base') return
454+
455+
const indent = depth > 0 ? tab.repeat(depth) + '├ ' : ''
456+
457+
if (typeof value === 'object') {
458+
table.push({
459+
color: '',
460+
theme: `${indent}**${cond}**`,
461+
value: '─────',
462+
})
463+
return
464+
}
465+
466+
const [tokenRef] = ctx.tokens.getReferences(value)
467+
const color = tokenRef?.value ?? value
468+
if (!color) return
469+
470+
table.push({
471+
color: makeColorTile(color),
472+
theme: `${indent}**${cond}**`,
473+
value: `\`${color}\``,
474+
})
475+
},
476+
{ separator },
477+
)
478+
479+
markdown.push(makeTable(table))
480+
markdown.push(`\n${tab}`)
481+
}
482+
483+
item.documentation = { kind: 'markdown', value: markdown.join('\n') }
418484
}
419485

420486
return {
@@ -425,6 +491,7 @@ export function setupTokensHelpers(setup: PandaExtensionSetup) {
425491
getClosestToken,
426492
getClosestInstance,
427493
getClosestCompletionList,
494+
getCompletionDetails,
428495
}
429496
}
430497

@@ -516,15 +583,19 @@ const getTokenFromPropValue = (ctx: PandaContext, prop: string, value: string):
516583
return token
517584
}
518585

519-
const completionCache = new Map<string, CompletionItem[]>()
520-
const itemCache = new Map<string, CompletionItem>()
521-
522-
const getCompletionFor = (
523-
ctx: PandaContext,
524-
propName: string,
525-
propNode: BoxNodeLiteral,
526-
settings: PandaVSCodeSettings,
527-
) => {
586+
const getCompletionFor = ({
587+
ctx,
588+
propName,
589+
shorthand,
590+
propNode,
591+
settings,
592+
}: {
593+
ctx: PandaContext
594+
propName: string
595+
shorthand?: string
596+
propNode: BoxNodeLiteral
597+
settings: PandaVSCodeSettings
598+
}) => {
528599
const propValue = propNode.value
529600

530601
let str = String(propValue)
@@ -552,10 +623,6 @@ const getCompletionFor = (
552623
category = split[0]
553624
}
554625

555-
const cachePath = propName + '.' + str
556-
const cachedList = completionCache.get(cachePath)
557-
if (cachedList) return cachedList
558-
559626
// token(colors.red.300) -> category = "colors"
560627
// color="red.300" -> no category, need to find it
561628
if (!category) {
@@ -574,21 +641,15 @@ const getCompletionFor = (
574641
values.forEach((token, name) => {
575642
if (str && !name.includes(str)) return
576643

577-
const tokenPath = token.name
578-
const cachedItem = itemCache.get(tokenPath)
579-
if (cachedItem) {
580-
items.push(cachedItem)
581-
return
582-
}
583-
584644
const isColor = token.extensions.category === 'colors'
645+
585646
const completionItem = {
647+
data: { propName, token, shorthand },
586648
label: name,
587649
kind: isColor ? CompletionItemKind.Color : CompletionItemKind.EnumMember,
588-
documentation: { kind: 'markdown', value: getMarkdownCss(ctx, { [propName]: token.value }, settings).withCss },
589650
labelDetails: { description: printTokenValue(token, settings), detail: ` ${token.extensions.varRef}` },
590-
sortText: '-' + name,
591-
preselect: true,
651+
sortText: '-' + getSortText(name),
652+
preselect: false,
592653
} as CompletionItem
593654

594655
if (isColor) {
@@ -597,10 +658,8 @@ const getCompletionFor = (
597658
}
598659

599660
items.push(completionItem)
600-
itemCache.set(tokenPath, completionItem)
601661
})
602662

603-
completionCache.set(cachePath, items)
604663
return items
605664
}
606665

@@ -611,6 +670,6 @@ const getFirstAncestorMatching = <Ancestor extends Node>(
611670
) => {
612671
for (let i = stack.length - 1; i >= 0; i--) {
613672
const parent = stack[i]
614-
if (callback(parent, i)) return parent
673+
if (parent && callback(parent, i)) return parent
615674
}
616675
}

0 commit comments

Comments
 (0)