@pandacss/language-server 0.0.0-dev-20230607205019
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE.md +21 -0
- package/Roboto-Medium.ttf +0 -0
- package/dist/index.js +46760 -0
- package/index.ts +1 -0
- package/package.json +49 -0
- package/src/builder-resolver.ts +82 -0
- package/src/capabilities.ts +25 -0
- package/src/features/color-hints.ts +68 -0
- package/src/features/completion.ts +34 -0
- package/src/features/diagnostics.ts +93 -0
- package/src/features/hover.ts +79 -0
- package/src/features/index.ts +5 -0
- package/src/features/inlay-hints.ts +61 -0
- package/src/index.ts +130 -0
- package/src/setup-builder.ts +252 -0
- package/src/tokens/color2k-to-vscode-color.ts +18 -0
- package/src/tokens/error.ts +3 -0
- package/src/tokens/expand-token-fn.ts +43 -0
- package/src/tokens/generate-keyframe-css.ts +28 -0
- package/src/tokens/is-color.ts +216 -0
- package/src/tokens/render-font-size-preview.ts +49 -0
- package/src/tokens/render-token-color-preview.ts +63 -0
- package/src/tokens/setup-tokens-helpers.ts +601 -0
- package/src/tokens/utils.ts +82 -0
- package/src/uri-to-path.ts +38 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { CompletionItem, CompletionItemKind, Position, Range } from 'vscode-languageserver'
|
|
2
|
+
import { TextDocument } from 'vscode-languageserver-textdocument'
|
|
3
|
+
import { type PandaExtensionSetup } from '../setup-builder'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Dict,
|
|
7
|
+
type ParserResultType,
|
|
8
|
+
type RawCondition,
|
|
9
|
+
type ResultItem,
|
|
10
|
+
type SystemStyleObject,
|
|
11
|
+
} from '@pandacss/types'
|
|
12
|
+
import { CallExpression, Identifier, JsxOpeningElement, JsxSelfClosingElement, Node, ts } from 'ts-morph'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
BoxNodeArray,
|
|
16
|
+
BoxNodeLiteral,
|
|
17
|
+
BoxNodeMap,
|
|
18
|
+
BoxNodeObject,
|
|
19
|
+
box,
|
|
20
|
+
extractCallExpressionArguments,
|
|
21
|
+
extractJsxAttribute,
|
|
22
|
+
extractJsxElementProps,
|
|
23
|
+
findIdentifierValueDeclaration,
|
|
24
|
+
maybeBoxNode,
|
|
25
|
+
unbox,
|
|
26
|
+
type BoxContext,
|
|
27
|
+
type PrimitiveType,
|
|
28
|
+
type Unboxed,
|
|
29
|
+
} from '@pandacss/extractor'
|
|
30
|
+
import { type PandaContext } from '@pandacss/node'
|
|
31
|
+
import { walkObject } from '@pandacss/shared'
|
|
32
|
+
import { type Token } from '@pandacss/token-dictionary'
|
|
33
|
+
import { Bool } from 'lil-fp'
|
|
34
|
+
import { type PandaVSCodeSettings } from '@pandacss/extension-shared'
|
|
35
|
+
import { match } from 'ts-pattern'
|
|
36
|
+
import { color2kToVsCodeColor } from './color2k-to-vscode-color'
|
|
37
|
+
import { expandTokenFn, extractTokenPaths } from './expand-token-fn'
|
|
38
|
+
import { isColor } from './is-color'
|
|
39
|
+
import { getMarkdownCss, isObjectLike, nodeRangeToVsCodeRange, printTokenValue } from './utils'
|
|
40
|
+
|
|
41
|
+
type ClosestMatch = {
|
|
42
|
+
range: Range
|
|
43
|
+
propName: string
|
|
44
|
+
propNode: BoxNodeWithValue
|
|
45
|
+
}
|
|
46
|
+
type ClosestTokenMatch = ClosestMatch & {
|
|
47
|
+
kind: 'token'
|
|
48
|
+
token: Token
|
|
49
|
+
propValue: PrimitiveType
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ClosestConditionMatch = ClosestMatch & {
|
|
53
|
+
kind: 'condition'
|
|
54
|
+
condition: RawCondition
|
|
55
|
+
propValue: Unboxed['raw']
|
|
56
|
+
}
|
|
57
|
+
type ClosestToken = ClosestTokenMatch | ClosestConditionMatch
|
|
58
|
+
|
|
59
|
+
type ClosestInstanceMatch = { name: string }
|
|
60
|
+
type ClosestStylesInstance = { kind: 'styles'; props: BoxNodeMap | BoxNodeObject }
|
|
61
|
+
type ClosestInstance = ClosestInstanceMatch & ClosestStylesInstance
|
|
62
|
+
|
|
63
|
+
type OnTokenCallback = (args: ClosestToken) => void
|
|
64
|
+
type BoxNodeWithValue = BoxNodeObject | BoxNodeLiteral | BoxNodeMap | BoxNodeArray
|
|
65
|
+
|
|
66
|
+
const extractableFns = ['css', 'cx'] as const
|
|
67
|
+
type ExtractableFnName = (typeof extractableFns)[number]
|
|
68
|
+
const canEvalFn = (name: string): name is ExtractableFnName => extractableFns.includes(name as any)
|
|
69
|
+
|
|
70
|
+
const mergeCx = (...args: any[]) =>
|
|
71
|
+
args.filter(Boolean).reduce((acc, curr) => {
|
|
72
|
+
if (typeof curr === 'object') return Object.assign(acc, curr)
|
|
73
|
+
|
|
74
|
+
return acc
|
|
75
|
+
}, {})
|
|
76
|
+
|
|
77
|
+
const isFunctionMadeFromDefineParts = (expr: Identifier) => {
|
|
78
|
+
const declaration = findIdentifierValueDeclaration(expr, [], boxCtx)
|
|
79
|
+
if (!Node.isVariableDeclaration(declaration)) return
|
|
80
|
+
|
|
81
|
+
const initializer = declaration.getInitializer()
|
|
82
|
+
if (!Node.isCallExpression(initializer)) return
|
|
83
|
+
|
|
84
|
+
const fromFunctionName = initializer.getExpression().getText()
|
|
85
|
+
return fromFunctionName === 'defineParts'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const boxCtx: BoxContext = {
|
|
89
|
+
flags: { skipTraverseFiles: true },
|
|
90
|
+
getEvaluateOptions: (node) => {
|
|
91
|
+
if (!Node.isCallExpression(node)) return
|
|
92
|
+
const expr = node.getExpression()
|
|
93
|
+
|
|
94
|
+
if (!Node.isIdentifier(expr)) return
|
|
95
|
+
const name = expr.getText()
|
|
96
|
+
|
|
97
|
+
// TODO - check for import alias ? kinda overkill for now
|
|
98
|
+
if (!canEvalFn(name as string) && !isFunctionMadeFromDefineParts(expr)) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
environment: {
|
|
104
|
+
extra: {
|
|
105
|
+
cx: mergeCx,
|
|
106
|
+
css: (styles: SystemStyleObject) => styles,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
} as any
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isNodeJsx = Bool.or(Node.isJsxSelfClosingElement, Node.isJsxOpeningElement)
|
|
114
|
+
const getNestedBoxProp = (map: BoxNodeMap, path: string[]) => {
|
|
115
|
+
return path.reduce((acc, curr) => {
|
|
116
|
+
if (box.isMap(acc)) return acc.value.get(curr)
|
|
117
|
+
if (box.isObject(acc)) return acc.value[curr]
|
|
118
|
+
|
|
119
|
+
return acc
|
|
120
|
+
}, map as any)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function setupTokensHelpers(setup: PandaExtensionSetup): any {
|
|
124
|
+
function getSourceFile(doc: TextDocument) {
|
|
125
|
+
const ctx = setup.getContext()
|
|
126
|
+
if (!ctx) return
|
|
127
|
+
|
|
128
|
+
return ctx.project.getSourceFile(doc.uri)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the local component list of local tokens.
|
|
133
|
+
*/
|
|
134
|
+
function parseSourceFile(doc: TextDocument) {
|
|
135
|
+
const ctx = setup.getContext()
|
|
136
|
+
if (!ctx) return
|
|
137
|
+
|
|
138
|
+
const project = ctx.project
|
|
139
|
+
|
|
140
|
+
project.addSourceFile(doc.uri, doc.getText())
|
|
141
|
+
return project.parseSourceFile(doc.uri)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const createResultTokensGetter = (onToken: OnTokenCallback) => {
|
|
145
|
+
const ctx = setup.getContext()
|
|
146
|
+
if (!ctx)
|
|
147
|
+
return () => {
|
|
148
|
+
//
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (result: ResultItem) => {
|
|
152
|
+
const boxNode = result.box
|
|
153
|
+
|
|
154
|
+
result.data.forEach((styles) => {
|
|
155
|
+
const keys = Object.keys(styles)
|
|
156
|
+
if (!keys.length) return
|
|
157
|
+
|
|
158
|
+
walkObject(styles, (value, paths) => {
|
|
159
|
+
// if value doesn't exist
|
|
160
|
+
if (value == null) return
|
|
161
|
+
|
|
162
|
+
const [prop, ..._allConditions] = ctx.conditions.shift(paths)
|
|
163
|
+
const propNode = getNestedBoxProp(boxNode, paths)
|
|
164
|
+
if (!box.isLiteral(propNode)) return
|
|
165
|
+
|
|
166
|
+
const propName = ctx.utility.resolveShorthand(prop)
|
|
167
|
+
const token = getTokenFromPropValue(ctx, propName, value)
|
|
168
|
+
if (!token) return
|
|
169
|
+
|
|
170
|
+
const range = nodeRangeToVsCodeRange(propNode.getRange())
|
|
171
|
+
onToken?.({ kind: 'token', token, range, propName, propValue: value, propNode })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get all the tokens from the document and call a callback on it.
|
|
179
|
+
*/
|
|
180
|
+
function getFileTokens(_doc: TextDocument, parserResult: ParserResultType, onToken: OnTokenCallback) {
|
|
181
|
+
const ctx = setup.getContext()
|
|
182
|
+
if (!ctx) return
|
|
183
|
+
|
|
184
|
+
const onResult = createResultTokensGetter(onToken)
|
|
185
|
+
|
|
186
|
+
parserResult.css.forEach(onResult)
|
|
187
|
+
parserResult.jsx.forEach(onResult)
|
|
188
|
+
parserResult.cva.forEach((item) =>
|
|
189
|
+
item.data.forEach(({ base }) =>
|
|
190
|
+
onResult(Object.assign({}, item, { box: item.box.value.get('base'), data: [base] })),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const getNodeAtPosition = (doc: TextDocument, position: Position) => {
|
|
196
|
+
const ctx = setup.getContext()
|
|
197
|
+
if (!ctx) return
|
|
198
|
+
|
|
199
|
+
const sourceFile = getSourceFile(doc)
|
|
200
|
+
if (!sourceFile) return
|
|
201
|
+
|
|
202
|
+
const charIndex = ts.getPositionOfLineAndCharacter(sourceFile.compilerNode, position.line, position.character)
|
|
203
|
+
return getDescendantAtPos(sourceFile, charIndex)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const findClosestInstance = <Return>(
|
|
207
|
+
node: Node,
|
|
208
|
+
stack: Node[],
|
|
209
|
+
onFoundInstance: (args: ClosestInstance) => Return,
|
|
210
|
+
) => {
|
|
211
|
+
const ctx = setup.getContext()
|
|
212
|
+
if (!ctx) return
|
|
213
|
+
|
|
214
|
+
let current: Node | undefined = node
|
|
215
|
+
|
|
216
|
+
return match(node)
|
|
217
|
+
.when(
|
|
218
|
+
() => {
|
|
219
|
+
const callExpression = getFirstAncestorMatching(stack, (node): node is CallExpression => {
|
|
220
|
+
if (!Node.isCallExpression(node)) return false
|
|
221
|
+
const expr = node.getExpression()
|
|
222
|
+
|
|
223
|
+
// TODO - check for import alias ? kinda overkill for now
|
|
224
|
+
if (Node.isIdentifier(expr) && !canEvalFn(expr.getText())) return false
|
|
225
|
+
|
|
226
|
+
return true
|
|
227
|
+
})
|
|
228
|
+
if (!callExpression) return
|
|
229
|
+
|
|
230
|
+
current = callExpression
|
|
231
|
+
return current
|
|
232
|
+
},
|
|
233
|
+
() => {
|
|
234
|
+
const callExpression = current as CallExpression
|
|
235
|
+
const name = callExpression.getExpression().getText() as ExtractableFnName
|
|
236
|
+
|
|
237
|
+
if (name === 'cx') {
|
|
238
|
+
const list = extractCallExpressionArguments(
|
|
239
|
+
callExpression,
|
|
240
|
+
boxCtx,
|
|
241
|
+
() => true,
|
|
242
|
+
() => true,
|
|
243
|
+
)
|
|
244
|
+
const styles = mergeCx(
|
|
245
|
+
...list.value
|
|
246
|
+
.filter((node) => isObjectLike(node))
|
|
247
|
+
.map((item) => (item as BoxNodeMap | BoxNodeObject).value),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return onFoundInstance({
|
|
251
|
+
kind: 'styles',
|
|
252
|
+
name,
|
|
253
|
+
props: box.object(styles, callExpression, stack),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const list = extractCallExpressionArguments(
|
|
258
|
+
callExpression,
|
|
259
|
+
boxCtx,
|
|
260
|
+
() => true,
|
|
261
|
+
(args) => args.index === 0,
|
|
262
|
+
)
|
|
263
|
+
const config = list.value[0]
|
|
264
|
+
if (!isObjectLike(config)) return
|
|
265
|
+
|
|
266
|
+
if (name === 'css' || name === 'defineStyles') {
|
|
267
|
+
return onFoundInstance({ kind: 'styles', name, props: config })
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
.when(
|
|
272
|
+
() => {
|
|
273
|
+
current = getFirstAncestorMatching(stack, isNodeJsx)
|
|
274
|
+
return current
|
|
275
|
+
},
|
|
276
|
+
() => {
|
|
277
|
+
const componentNode = current as JsxSelfClosingElement | JsxOpeningElement
|
|
278
|
+
const { name, props } = extractJsxElementProps(componentNode, boxCtx)
|
|
279
|
+
if (!props.size) return
|
|
280
|
+
|
|
281
|
+
return onFoundInstance({ kind: 'styles', name, props: box.map(props, componentNode, stack) })
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
.otherwise(() => {
|
|
285
|
+
//
|
|
286
|
+
return undefined
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const findClosestToken = <Return>(
|
|
291
|
+
node: Node,
|
|
292
|
+
stack: Node[],
|
|
293
|
+
onFoundToken: (args: Pick<ClosestToken, 'propName' | 'propNode'>) => Return,
|
|
294
|
+
) => {
|
|
295
|
+
const ctx = setup.getContext()
|
|
296
|
+
if (!ctx) return
|
|
297
|
+
|
|
298
|
+
return match(node)
|
|
299
|
+
.when(
|
|
300
|
+
() => getFirstAncestorMatching(stack, Node.isPropertyAssignment),
|
|
301
|
+
() => {
|
|
302
|
+
const propAssignment = getFirstAncestorMatching(stack, Node.isPropertyAssignment)!
|
|
303
|
+
const name = propAssignment.getName()
|
|
304
|
+
|
|
305
|
+
const objectLiteral = getFirstAncestorMatching(stack, Node.isObjectLiteralExpression)!
|
|
306
|
+
const maybeBox = maybeBoxNode(objectLiteral, [], boxCtx, (args) => args.propName === name)
|
|
307
|
+
if (!box.isMap(maybeBox)) return
|
|
308
|
+
|
|
309
|
+
const propNode = maybeBox.value.get(name)
|
|
310
|
+
if (!box.hasValue(propNode)) return
|
|
311
|
+
|
|
312
|
+
const propName = ctx.utility.resolveShorthand(name)
|
|
313
|
+
|
|
314
|
+
return onFoundToken({ propName, propNode })
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
.when(
|
|
318
|
+
() => getFirstAncestorMatching(stack, Node.isJsxAttribute),
|
|
319
|
+
() => {
|
|
320
|
+
const attrNode = getFirstAncestorMatching(stack, Node.isJsxAttribute)!
|
|
321
|
+
|
|
322
|
+
const nameNode = attrNode.getNameNode()
|
|
323
|
+
const name = nameNode.getText()
|
|
324
|
+
|
|
325
|
+
const attrBox = extractJsxAttribute(attrNode, boxCtx)
|
|
326
|
+
if (!box.hasValue(attrBox)) return
|
|
327
|
+
|
|
328
|
+
const propName = ctx.utility.resolveShorthand(name)
|
|
329
|
+
|
|
330
|
+
return onFoundToken({ propName, propNode: attrBox })
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
.otherwise(() => {
|
|
334
|
+
//
|
|
335
|
+
return undefined
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const getClosestToken = (doc: TextDocument, position: Position): ClosestToken | undefined => {
|
|
340
|
+
const ctx = setup.getContext()
|
|
341
|
+
if (!ctx) return
|
|
342
|
+
|
|
343
|
+
const match = getNodeAtPosition(doc, position)
|
|
344
|
+
if (!match) return
|
|
345
|
+
|
|
346
|
+
const { node, stack } = match
|
|
347
|
+
|
|
348
|
+
return findClosestToken(node, stack, ({ propName, propNode }) => {
|
|
349
|
+
if (box.isLiteral(propNode)) {
|
|
350
|
+
const propValue = propNode.value
|
|
351
|
+
const maybeToken = getTokenFromPropValue(ctx, propName, String(propValue))
|
|
352
|
+
if (!maybeToken) return
|
|
353
|
+
|
|
354
|
+
const range = nodeRangeToVsCodeRange(propNode.getRange())
|
|
355
|
+
return { kind: 'token', token: maybeToken, range, propName, propValue, propNode } as ClosestTokenMatch
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (box.isMap(propNode) && ctx.conditions.isCondition(propName)) {
|
|
359
|
+
const objectBox = maybeBoxNode(propNode.getNode(), [], boxCtx, () => true)
|
|
360
|
+
if (!objectBox) return
|
|
361
|
+
|
|
362
|
+
if (box.isMap(objectBox)) {
|
|
363
|
+
const propValue = unbox(propNode).raw
|
|
364
|
+
const condition = ctx.conditions.getRaw(propName)
|
|
365
|
+
|
|
366
|
+
const range = nodeRangeToVsCodeRange(propNode.getRange())
|
|
367
|
+
return { kind: 'condition', condition, range, propName, propValue, propNode } as ClosestConditionMatch
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const getClosestInstance = (doc: TextDocument, position: Position) => {
|
|
374
|
+
const ctx = setup.getContext()
|
|
375
|
+
if (!ctx) return
|
|
376
|
+
|
|
377
|
+
const match = getNodeAtPosition(doc, position)
|
|
378
|
+
if (!match) return
|
|
379
|
+
|
|
380
|
+
const { node, stack } = match
|
|
381
|
+
|
|
382
|
+
return findClosestInstance(node, stack, (instance) => {
|
|
383
|
+
if (instance.kind === 'styles') {
|
|
384
|
+
const { name, props } = instance
|
|
385
|
+
|
|
386
|
+
const unboxed = unbox(props)
|
|
387
|
+
const { className, css, ...rest } = unboxed.raw
|
|
388
|
+
return {
|
|
389
|
+
kind: 'styles',
|
|
390
|
+
name,
|
|
391
|
+
props,
|
|
392
|
+
styles: Object.assign({}, className, css, rest),
|
|
393
|
+
} as ClosestStylesInstance & { styles: Dict }
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const getClosestCompletionList = async (doc: TextDocument, position: Position) => {
|
|
399
|
+
const ctx = setup.getContext()
|
|
400
|
+
if (!ctx) return
|
|
401
|
+
|
|
402
|
+
const match = getNodeAtPosition(doc, position)
|
|
403
|
+
if (!match) return
|
|
404
|
+
|
|
405
|
+
const settings = await setup.getPandaSettings()
|
|
406
|
+
const { node, stack } = match
|
|
407
|
+
|
|
408
|
+
return findClosestToken(node, stack, ({ propName, propNode }) => {
|
|
409
|
+
if (!box.isLiteral(propNode)) return undefined
|
|
410
|
+
return getCompletionFor(ctx, propName, propNode, settings)
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
getSourceFile,
|
|
416
|
+
parseSourceFile,
|
|
417
|
+
getFileTokens,
|
|
418
|
+
findClosestToken,
|
|
419
|
+
getClosestToken,
|
|
420
|
+
getClosestInstance,
|
|
421
|
+
getClosestCompletionList,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const getDescendantAtPos = (from: Node, pos: number) => {
|
|
426
|
+
let node: Node | undefined = from
|
|
427
|
+
const stack: Node[] = [from]
|
|
428
|
+
|
|
429
|
+
// eslint-disable-next-line no-constant-condition
|
|
430
|
+
while (true) {
|
|
431
|
+
const nextNode: Node | undefined = node.getChildAtPos(pos)
|
|
432
|
+
if (nextNode == null) return { node, stack }
|
|
433
|
+
else {
|
|
434
|
+
node = nextNode
|
|
435
|
+
stack.push(node)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const getColorExtensions = (value: string, kind: string) => {
|
|
441
|
+
const vscodeColor = color2kToVsCodeColor(value)
|
|
442
|
+
if (!vscodeColor) return
|
|
443
|
+
|
|
444
|
+
return { vscodeColor, kind }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const getTokenFromPropValue = (ctx: PandaContext, prop: string, value: string): Token | undefined => {
|
|
448
|
+
const utility = ctx.config.utilities?.[prop]
|
|
449
|
+
|
|
450
|
+
const category = typeof utility?.values === 'string' && utility?.values
|
|
451
|
+
if (!category) return
|
|
452
|
+
|
|
453
|
+
const tokenPath = [category, value].join('.')
|
|
454
|
+
const token = ctx.tokens.getByName(tokenPath)
|
|
455
|
+
|
|
456
|
+
// arbitrary value like
|
|
457
|
+
// display: "block", zIndex: 1, ...
|
|
458
|
+
if (!token) {
|
|
459
|
+
// any color
|
|
460
|
+
// color: "blue", color: "#000", color: "rgb(0, 0, 0)", ...
|
|
461
|
+
if (isColor(value)) {
|
|
462
|
+
const extensions = getColorExtensions(value, 'native-color')
|
|
463
|
+
if (!extensions) return
|
|
464
|
+
|
|
465
|
+
return { value, name: value, path: tokenPath, type: 'color', extensions } as unknown as Token
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// border="1px solid token(colors.gray.300)"
|
|
469
|
+
if (typeof value === 'string' && value.includes('token(')) {
|
|
470
|
+
const matches = expandTokenFn(value, ctx.tokens.getByName)
|
|
471
|
+
|
|
472
|
+
// wrong token path
|
|
473
|
+
if (!matches.length) {
|
|
474
|
+
return {
|
|
475
|
+
value,
|
|
476
|
+
name: value,
|
|
477
|
+
path: tokenPath,
|
|
478
|
+
type: 'color',
|
|
479
|
+
extensions: { kind: 'invalid-token-path' },
|
|
480
|
+
} as unknown as Token
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// TODO: handle multiple tokens like : "token(colors.gray.300), token(colors.gray.500)"
|
|
484
|
+
const first = matches?.[0]?.token
|
|
485
|
+
if (!first) return
|
|
486
|
+
|
|
487
|
+
if (isColor(first.value)) {
|
|
488
|
+
const extensions = getColorExtensions(first.value, 'semantic-color')
|
|
489
|
+
if (!extensions) return
|
|
490
|
+
|
|
491
|
+
return first.setExtensions(extensions)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return first
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// known theme token
|
|
501
|
+
// px: "2", fontSize: "xl", ...
|
|
502
|
+
// color: "blue.300"
|
|
503
|
+
if (isColor(token.value)) {
|
|
504
|
+
const extensions = getColorExtensions(token.value, 'color')
|
|
505
|
+
if (!extensions) return
|
|
506
|
+
|
|
507
|
+
return token.setExtensions(extensions)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return token
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const completionCache = new Map<string, CompletionItem[]>()
|
|
514
|
+
const itemCache = new Map<string, CompletionItem>()
|
|
515
|
+
|
|
516
|
+
const getCompletionFor = (
|
|
517
|
+
ctx: PandaContext,
|
|
518
|
+
propName: string,
|
|
519
|
+
propNode: BoxNodeLiteral,
|
|
520
|
+
settings: PandaVSCodeSettings,
|
|
521
|
+
) => {
|
|
522
|
+
const propValue = propNode.value
|
|
523
|
+
|
|
524
|
+
let str = String(propValue)
|
|
525
|
+
|
|
526
|
+
// also provide completion in string such as: token('colors.blue.300')
|
|
527
|
+
if (settings['completions.token-fn.enabled'] && str.includes('token(')) {
|
|
528
|
+
const matches = extractTokenPaths(str)
|
|
529
|
+
const tokenPath = matches[0] ?? ''
|
|
530
|
+
const split = tokenPath.split('.')
|
|
531
|
+
|
|
532
|
+
// provide completion for token category when token() is empty or partial
|
|
533
|
+
if (split.length <= 1) {
|
|
534
|
+
return Array.from(ctx.tokens.categoryMap.keys()).map((category) => {
|
|
535
|
+
return {
|
|
536
|
+
label: category,
|
|
537
|
+
kind: CompletionItemKind.EnumMember,
|
|
538
|
+
sortText: '-' + category,
|
|
539
|
+
preselect: true,
|
|
540
|
+
} as CompletionItem
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
str = tokenPath.split('.').slice(1).join('.')
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const cachePath = propName + '.' + str
|
|
548
|
+
const cachedList = completionCache.get(cachePath)
|
|
549
|
+
if (cachedList) return cachedList
|
|
550
|
+
|
|
551
|
+
const utility = ctx.config.utilities?.[propName]
|
|
552
|
+
const category = typeof utility?.values === 'string' && utility?.values
|
|
553
|
+
if (!category) return []
|
|
554
|
+
|
|
555
|
+
const values = ctx.tokens.categoryMap.get(category)
|
|
556
|
+
if (!values) return []
|
|
557
|
+
|
|
558
|
+
const items = [] as CompletionItem[]
|
|
559
|
+
values.forEach((token, name) => {
|
|
560
|
+
if (!name.includes(str)) return
|
|
561
|
+
|
|
562
|
+
const tokenPath = token.name
|
|
563
|
+
const cachedItem = itemCache.get(tokenPath)
|
|
564
|
+
if (cachedItem) {
|
|
565
|
+
items.push(cachedItem)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const isColor = token.extensions.category === 'colors'
|
|
570
|
+
const completionItem = {
|
|
571
|
+
label: name,
|
|
572
|
+
kind: isColor ? CompletionItemKind.Color : CompletionItemKind.EnumMember,
|
|
573
|
+
documentation: { kind: 'markdown', value: getMarkdownCss(ctx, { [propName]: token.value }, settings).withCss },
|
|
574
|
+
labelDetails: { description: printTokenValue(token, settings), detail: ` ${token.extensions.varRef}` },
|
|
575
|
+
sortText: '-' + name,
|
|
576
|
+
preselect: true,
|
|
577
|
+
} as CompletionItem
|
|
578
|
+
|
|
579
|
+
if (isColor) {
|
|
580
|
+
completionItem.detail = token.value
|
|
581
|
+
// TODO rgb conversion ?
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
items.push(completionItem)
|
|
585
|
+
itemCache.set(tokenPath, completionItem)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
completionCache.set(cachePath, items)
|
|
589
|
+
return items
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// quick index based loop
|
|
593
|
+
const getFirstAncestorMatching = <Ancestor extends Node>(
|
|
594
|
+
stack: Node[],
|
|
595
|
+
callback: (parent: Node, index: number) => parent is Ancestor,
|
|
596
|
+
) => {
|
|
597
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
598
|
+
const parent = stack[i]
|
|
599
|
+
if (callback(parent, i)) return parent
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { box, type NodeRange } from '@pandacss/extractor'
|
|
2
|
+
import { Bool } from 'lil-fp'
|
|
3
|
+
import { Range } from 'vscode-languageserver'
|
|
4
|
+
|
|
5
|
+
import { type SystemStyleObject } from '@pandacss/types'
|
|
6
|
+
|
|
7
|
+
import { AtomicRule, optimizeCss } from '@pandacss/core'
|
|
8
|
+
import { type PandaContext } from '@pandacss/node'
|
|
9
|
+
import { mapObject, toPx } from '@pandacss/shared'
|
|
10
|
+
import * as base64 from 'base-64'
|
|
11
|
+
|
|
12
|
+
import postcss from 'postcss'
|
|
13
|
+
import parserCSS from 'prettier/parser-postcss'
|
|
14
|
+
import prettier from 'prettier/standalone'
|
|
15
|
+
import * as utf8 from 'utf8'
|
|
16
|
+
import { match } from 'ts-pattern'
|
|
17
|
+
|
|
18
|
+
import { type PandaVSCodeSettings } from '@pandacss/extension-shared'
|
|
19
|
+
import { type Token } from '@pandacss/token-dictionary'
|
|
20
|
+
|
|
21
|
+
export const isObjectLike = Bool.or(box.isObject, box.isMap)
|
|
22
|
+
|
|
23
|
+
export const nodeRangeToVsCodeRange = (range: NodeRange) =>
|
|
24
|
+
Range.create(
|
|
25
|
+
{ line: range.startLineNumber - 1, character: range.startColumn - 1 },
|
|
26
|
+
{ line: range.endLineNumber - 1, character: range.endColumn - 1 },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const helpers = { map: mapObject }
|
|
30
|
+
export const makeSheetCtx = (ctx: PandaContext) => ({
|
|
31
|
+
root: postcss.root(),
|
|
32
|
+
conditions: ctx.conditions,
|
|
33
|
+
utility: ctx.utility,
|
|
34
|
+
hash: ctx.config.hash,
|
|
35
|
+
helpers,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function getPrettiedCSS(css: string) {
|
|
39
|
+
return prettier.format(css, {
|
|
40
|
+
parser: 'css',
|
|
41
|
+
plugins: [parserCSS],
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DisplayOptions = {
|
|
46
|
+
mode?: PandaVSCodeSettings['hovers.display.mode']
|
|
47
|
+
forceHash?: PandaVSCodeSettings['hovers.display.force-hash']
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const getMarkdownCss = (ctx: PandaContext, styles: SystemStyleObject, settings: PandaVSCodeSettings) => {
|
|
51
|
+
const mode = settings['hovers.display.mode']
|
|
52
|
+
const forceHash = settings['hovers.display.force-hash']
|
|
53
|
+
|
|
54
|
+
const hash = ctx.config.hash
|
|
55
|
+
if (forceHash) {
|
|
56
|
+
ctx.config.hash = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rule = new AtomicRule(makeSheetCtx(ctx))
|
|
60
|
+
rule.process({ styles })
|
|
61
|
+
|
|
62
|
+
const css = match(mode ?? 'optimized')
|
|
63
|
+
.with('nested', () => rule.toCss())
|
|
64
|
+
.with('optimized', () => optimizeCss(rule.toCss()))
|
|
65
|
+
.with('minified', () => optimizeCss(rule.toCss(), { minify: true }))
|
|
66
|
+
.run()
|
|
67
|
+
const raw = getPrettiedCSS(css)
|
|
68
|
+
const withCss = '```css' + '\n' + raw + '\n' + '```'
|
|
69
|
+
|
|
70
|
+
// restore hash
|
|
71
|
+
ctx.config.hash = hash
|
|
72
|
+
|
|
73
|
+
return { raw, withCss }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const printTokenValue = (token: Token, settings: PandaVSCodeSettings) =>
|
|
77
|
+
`${token.value}${settings['rem-to-px.enabled'] && token.value.endsWith('rem') ? ` (${toPx(token.value)})` : ''}`
|
|
78
|
+
|
|
79
|
+
export const svgToMarkdownLink = (svg: string) => {
|
|
80
|
+
const dataUri = 'data:image/svg+xml;charset=UTF-8;base64,' + base64.encode(utf8.encode(svg))
|
|
81
|
+
return ``
|
|
82
|
+
}
|