Skip to content

Commit 2042dc4

Browse files
committed
refactor(lsp): using class
1 parent 9b2b9c3 commit 2042dc4

21 files changed

+1323
-1235
lines changed

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export class BuilderResolver {
1313
private configDirpathList = new Set<string>()
1414
private configDirPathByFilepath = new Map<string, string>()
1515
private configPathByDirpath = new Map<string, string>()
16-
private _onSetup: ((args: { configPath: string }) => void) | undefined
16+
17+
constructor(private onSetup: (args: { configPath: string; builder: Builder }) => void) {}
1718

1819
findConfigDirpath<T>(filepath: string, onFound: (configDirPath: string, configPath: string) => T) {
1920
const cachedDir = this.configDirPathByFilepath.get(filepath)
@@ -68,15 +69,10 @@ export class BuilderResolver {
6869

6970
synchronizing.finally(() => {
7071
this.synchronizingByConfigDirpath.set(configDirpath, false)
71-
72-
this._onSetup?.({ configPath })
72+
this.onSetup({ configPath, builder })
7373
})
7474

7575
return synchronizing
7676
})
7777
}
78-
79-
onSetup(callback: (args: { configPath: string }) => void) {
80-
this._onSetup = callback
81-
}
8278
}

packages/language-server/src/capabilities.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const TRIGGER_CHARACTERS = [
1111
'.',
1212
]
1313

14-
export const serverCapabilities: ServerCapabilities = {
14+
export const getDefaultCapabilities = (): ServerCapabilities => ({
1515
textDocumentSync: TextDocumentSyncKind.Incremental,
1616
inlayHintProvider: {
1717
resolveProvider: false,
@@ -36,4 +36,4 @@ export const serverCapabilities: ServerCapabilities = {
3636
hoverProvider: true,
3737
colorProvider: true,
3838
inlineValueProvider: true,
39-
}
39+
})
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { CompletionItem, CompletionItemKind, Position } from 'vscode-languageserver'
2+
import { TextDocument } from 'vscode-languageserver-textdocument'
3+
4+
import { type PandaVSCodeSettings } from '@pandacss/extension-shared'
5+
import { BoxNodeLiteral, box } from '@pandacss/extractor'
6+
import { type PandaContext } from '@pandacss/node'
7+
import { type Token } from '@pandacss/token-dictionary'
8+
import { extractTokenPaths } from './tokens/expand-token-fn'
9+
import { makeColorTile, makeTable } from './tokens/render-markdown'
10+
import { getSortText } from './tokens/sort-text'
11+
import { traverse } from './tokens/traverse'
12+
import { getMarkdownCss, printTokenValue } from './tokens/utils'
13+
import type { GetContext } from './panda-language-server'
14+
import type { ProjectHelper } from './project-helper'
15+
import type { TokenFinder } from './token-finder'
16+
17+
export class CompletionProvider {
18+
constructor(
19+
private getContext: GetContext,
20+
private getPandaSettings: () => Promise<PandaVSCodeSettings>,
21+
private project: ProjectHelper,
22+
private tokenFinder: TokenFinder,
23+
) {}
24+
25+
async getClosestCompletionList(doc: TextDocument, position: Position) {
26+
const ctx = this.getContext()
27+
if (!ctx) return
28+
29+
const match = this.project.getNodeAtPosition(doc, position)
30+
if (!match) return
31+
32+
const settings = await this.getPandaSettings()
33+
const { node, stack } = match
34+
35+
try {
36+
return await this.tokenFinder.findClosestToken(node, stack, ({ propName, propNode, shorthand }) => {
37+
if (!box.isLiteral(propNode)) return undefined
38+
return getCompletionFor({ ctx, propName, propNode, settings, shorthand })
39+
})
40+
} catch (err) {
41+
console.error(err)
42+
console.trace()
43+
return
44+
}
45+
}
46+
47+
async getCompletionDetails(item: CompletionItem) {
48+
const ctx = this.getContext()
49+
if (!ctx) return
50+
51+
const settings = await this.getPandaSettings()
52+
const { propName, token, shorthand } = (item.data ?? {}) as { propName: string; token?: Token; shorthand: string }
53+
if (!token) return
54+
const markdownCss = getMarkdownCss(ctx, { [propName]: token.value }, settings)
55+
56+
const markdown = [markdownCss.withCss]
57+
if (shorthand !== propName) {
58+
markdown.push(`\`${shorthand}\` is shorthand for \`${propName}\``)
59+
}
60+
61+
const conditions = token.extensions.conditions ?? { base: token.value }
62+
if (conditions) {
63+
const separator = '[___]'
64+
const table = [{ color: ' ', theme: 'Condition', value: 'Value' }]
65+
66+
const tab = '&nbsp;&nbsp;&nbsp;&nbsp;'
67+
traverse(
68+
conditions,
69+
({ key: cond, value, depth }) => {
70+
if (!ctx.conditions.get(cond) && cond !== 'base') return
71+
72+
const indent = depth > 0 ? tab.repeat(depth) + '├ ' : ''
73+
74+
if (typeof value === 'object') {
75+
table.push({
76+
color: '',
77+
theme: `${indent}**${cond}**`,
78+
value: '─────',
79+
})
80+
return
81+
}
82+
83+
const [tokenRef] = ctx.tokens.getReferences(value)
84+
const color = tokenRef?.value ?? value
85+
if (!color) return
86+
87+
table.push({
88+
color: makeColorTile(color),
89+
theme: `${indent}**${cond}**`,
90+
value: `\`${color}\``,
91+
})
92+
},
93+
{ separator },
94+
)
95+
96+
markdown.push(makeTable(table))
97+
markdown.push(`\n${tab}`)
98+
}
99+
100+
item.documentation = { kind: 'markdown', value: markdown.join('\n') }
101+
}
102+
}
103+
104+
const getCompletionFor = ({
105+
ctx,
106+
propName,
107+
shorthand,
108+
propNode,
109+
settings,
110+
}: {
111+
ctx: PandaContext
112+
propName: string
113+
shorthand?: string
114+
propNode: BoxNodeLiteral
115+
settings: PandaVSCodeSettings
116+
}) => {
117+
const propValue = propNode.value
118+
119+
let str = String(propValue)
120+
let category: string | undefined
121+
122+
// also provide completion in string such as: token('colors.blue.300')
123+
if (settings['completions.token-fn.enabled'] && str.includes('token(')) {
124+
const matches = extractTokenPaths(str)
125+
const tokenPath = matches[0] ?? ''
126+
const split = tokenPath.split('.').filter(Boolean)
127+
128+
// provide completion for token category when token() is empty or partial
129+
if (split.length < 1) {
130+
return Array.from(ctx.tokens.categoryMap.keys()).map((category) => {
131+
return {
132+
label: category,
133+
kind: CompletionItemKind.EnumMember,
134+
sortText: '-' + category,
135+
preselect: true,
136+
} as CompletionItem
137+
})
138+
}
139+
140+
str = tokenPath.split('.').slice(1).join('.')
141+
category = split[0]
142+
}
143+
144+
// token(colors.red.300) -> category = "colors"
145+
// color="red.300" -> no category, need to find it
146+
let propValues: Record<string, string> | undefined
147+
if (!category) {
148+
const utility = ctx.config.utilities?.[propName]
149+
if (!utility?.values) return
150+
151+
// values: "spacing"
152+
if (typeof utility?.values === 'string') {
153+
category = utility.values
154+
} else if (typeof utility.values === 'function') {
155+
// values: (theme) => { ...theme("spacing") }
156+
const record = ctx.utility.getPropertyValues(utility)
157+
if (record) {
158+
if (record.type) category = record.type
159+
else propValues = record
160+
}
161+
}
162+
}
163+
164+
// values: { "1": "1px", "2": "2px", ... }
165+
if (propValues) {
166+
const items = [] as CompletionItem[]
167+
Object.entries(propValues).map(([name, value]) => {
168+
// margin: "2" -> ['var(--spacing-2)', 'var(--spacing-12)', 'var(--spacing-20)', ...]
169+
if (str && !name.includes(str)) return
170+
171+
const tokenPath = matchVar(value ?? '')?.replace('-', '.')
172+
const token = tokenPath && ctx.tokens.getByName(tokenPath)
173+
174+
items.push({
175+
data: { propName, token, shorthand },
176+
label: name,
177+
kind: CompletionItemKind.EnumMember,
178+
sortText: '-' + getSortText(name),
179+
preselect: false,
180+
})
181+
})
182+
183+
return items
184+
}
185+
186+
if (!category) return []
187+
188+
const categoryValues = ctx.tokens.categoryMap.get(category!)
189+
if (!categoryValues) return []
190+
191+
const items = [] as CompletionItem[]
192+
categoryValues.forEach((token, name) => {
193+
if (str && !name.includes(str)) return
194+
195+
const isColor = token.extensions.category === 'colors'
196+
197+
const completionItem = {
198+
data: { propName, token, shorthand },
199+
label: name,
200+
kind: isColor ? CompletionItemKind.Color : CompletionItemKind.EnumMember,
201+
labelDetails: { description: printTokenValue(token, settings), detail: ` ${token.extensions.varRef}` },
202+
sortText: '-' + getSortText(name),
203+
preselect: false,
204+
} as CompletionItem
205+
206+
if (isColor) {
207+
completionItem.detail = token.value
208+
// TODO rgb conversion ?
209+
}
210+
211+
items.push(completionItem)
212+
})
213+
214+
return items
215+
}
216+
const cssVarRegex = /var\(--([\w-.]+)\)/g
217+
const matchVar = (str: string) => {
218+
const match = cssVarRegex.exec(str)
219+
return match ? match[1] : null
220+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Deferred promise implementation\
3+
* Allow resolving a promise later
4+
*
5+
* @example
6+
* ```ts
7+
*
8+
const deferred = new Deferred();
9+
console.log("waiting 2 seconds...");
10+
setTimeout(() => {
11+
deferred.resolve("whoa!");
12+
}, 2000);
13+
14+
async function someAsyncFunction() {
15+
const value = await deferred;
16+
console.log(value);
17+
}
18+
19+
someAsyncFunction();
20+
// "waiting 2 seconds..."
21+
// "whoa!"
22+
```
23+
*/
24+
export class Deferred<T> {
25+
private _resolve!: (value: T | PromiseLike<T>) => void
26+
private _reject!: (reason?: any) => void
27+
public promise: Promise<T>
28+
29+
public then: Promise<T>['then']
30+
public catch: Promise<T>['catch']
31+
public finally: Promise<T>['finally']
32+
33+
constructor() {
34+
this.promise = new Promise<T>((resolve, reject) => {
35+
this._resolve = resolve
36+
this._reject = reject
37+
})
38+
39+
this.then = this.promise.then.bind(this.promise)
40+
this.catch = this.promise.catch.bind(this.promise)
41+
this.finally = this.promise.finally.bind(this.promise)
42+
}
43+
44+
resolve(value: T | PromiseLike<T>): void {
45+
this._resolve(value)
46+
}
47+
48+
reject(reason?: any): void {
49+
this._reject(reason)
50+
}
51+
52+
get [Symbol.toStringTag](): string {
53+
return this.constructor.name
54+
}
55+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { type SystemStyleObject } from '@pandacss/types'
2+
import { Identifier, Node } from 'ts-morph'
3+
4+
import {
5+
BoxNodeArray,
6+
BoxNodeLiteral,
7+
BoxNodeMap,
8+
BoxNodeObject,
9+
box,
10+
findIdentifierValueDeclaration,
11+
type BoxContext,
12+
} from '@pandacss/extractor'
13+
import { Bool } from 'lil-fp'
14+
15+
export type BoxNodeWithValue = BoxNodeObject | BoxNodeLiteral | BoxNodeMap | BoxNodeArray
16+
export type ExtractableFnName = (typeof extractableFns)[number]
17+
18+
const extractableFns = ['css', 'cx'] as const
19+
const canEvalFn = (name: string): name is ExtractableFnName => extractableFns.includes(name as any)
20+
21+
const mergeCx = (...args: any[]) =>
22+
args.filter(Boolean).reduce((acc, curr) => {
23+
if (typeof curr === 'object') return Object.assign(acc, curr)
24+
25+
return acc
26+
}, {})
27+
28+
const isFunctionMadeFromDefineParts = (expr: Identifier) => {
29+
const declaration = findIdentifierValueDeclaration(expr, [], boxCtx)
30+
if (!Node.isVariableDeclaration(declaration)) return
31+
32+
const initializer = declaration.getInitializer()
33+
if (!Node.isCallExpression(initializer)) return
34+
35+
const fromFunctionName = initializer.getExpression().getText()
36+
return fromFunctionName === 'defineParts'
37+
}
38+
39+
const boxCtx: BoxContext = {
40+
flags: { skipTraverseFiles: true },
41+
getEvaluateOptions: (node) => {
42+
if (!Node.isCallExpression(node)) return
43+
const expr = node.getExpression()
44+
45+
if (!Node.isIdentifier(expr)) return
46+
const name = expr.getText()
47+
48+
// TODO - check for import alias ? kinda overkill for now
49+
if (!canEvalFn(name as string) && !isFunctionMadeFromDefineParts(expr)) {
50+
return
51+
}
52+
53+
return {
54+
environment: {
55+
extra: {
56+
cx: mergeCx,
57+
css: (styles: SystemStyleObject) => styles,
58+
},
59+
},
60+
} as any
61+
},
62+
}
63+
64+
const isNodeJsx = Bool.or(Node.isJsxSelfClosingElement, Node.isJsxOpeningElement)
65+
const getNestedBoxProp = (map: BoxNodeMap, path: string[]) => {
66+
return path.reduce((acc, curr) => {
67+
if (box.isMap(acc)) return acc.value.get(curr)
68+
if (box.isObject(acc)) return acc.value[curr]
69+
70+
return acc
71+
}, map as any)
72+
}
73+
74+
export const extractor = {
75+
canEvalFn,
76+
mergeCx,
77+
boxCtx,
78+
isNodeJsx,
79+
getNestedBoxProp,
80+
}

0 commit comments

Comments
 (0)