@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/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './src'
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@pandacss/language-server",
3
+ "displayName": "Panda CSS",
4
+ "description": "🐼 The official Panda language server",
5
+ "version": "0.0.0-dev-20230607205019",
6
+ "main": "./dist/index.js",
7
+ "repository": {
8
+ "url": "https://github.com/chakra-ui/panda",
9
+ "directory": "packages/extension/language-server"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "devDependencies": {
15
+ "@types/base-64": "^1.0.0",
16
+ "@types/node": "20.2.5",
17
+ "@types/utf8": "^3.0.1",
18
+ "base-64": "^1.0.0",
19
+ "color2k": "^2.0.2",
20
+ "lil-fp": "1.4.5",
21
+ "postcss": "^8.4.24",
22
+ "prettier": "^2.8.8",
23
+ "satori": "^0.10.1",
24
+ "ts-morph": "18.0.0",
25
+ "ts-pattern": "4.3.0",
26
+ "tsup": "6.7.0",
27
+ "typescript": "^5.1.3",
28
+ "utf8": "^3.0.0",
29
+ "vscode-languageserver": "^8.1.0",
30
+ "vscode-languageserver-textdocument": "^1.0.8",
31
+ "vscode-uri": "^3.0.7",
32
+ "@pandacss/core": "0.0.0-dev-20230607205019",
33
+ "@pandacss/extractor": "0.0.0-dev-20230607205019",
34
+ "@pandacss/node": "0.0.0-dev-20230607205019",
35
+ "@pandacss/shared": "0.0.0-dev-20230607205019",
36
+ "@pandacss/token-dictionary": "0.0.0-dev-20230607205019",
37
+ "@pandacss/types": "0.0.0-dev-20230607205019",
38
+ "@pandacss/extension-shared": "^0.0.0-dev-20230607205019"
39
+ },
40
+ "dependencies": {
41
+ "fast-glob": "^3.2.12"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format cjs --minify -d dist --clean --shims",
45
+ "dev": "tsup src/index.ts --watch --format cjs -d dist --shims",
46
+ "clean": "rm -rf dist",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }
@@ -0,0 +1,82 @@
1
+ import { Builder } from '@pandacss/node'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * - Calls builder.setup() for the closest config file to the given filepath
6
+ * - Keep track of all the config files and their builders
7
+ * - Share the setup promise so it can be awaited anywhere
8
+ */
9
+ export class BuilderResolver {
10
+ private builderByConfigDirpath = new Map<string, Builder>()
11
+ private synchronizingByConfigDirpath = new Map<string, Promise<void> | false>()
12
+
13
+ private configDirpathList = new Set<string>()
14
+ private configDirPathByFilepath = new Map<string, string>()
15
+ private configPathByDirpath = new Map<string, string>()
16
+ private _onSetup: ((args: { configPath: string }) => void) | undefined
17
+
18
+ findConfigDirpath<T>(filepath: string, onFound: (configDirPath: string, configPath: string) => T) {
19
+ const cachedDir = this.configDirPathByFilepath.get(filepath)
20
+ if (cachedDir) {
21
+ return onFound(cachedDir, this.configPathByDirpath.get(cachedDir)!)
22
+ }
23
+
24
+ const dirpath = path.dirname(filepath)
25
+
26
+ for (const configDirpath of this.configDirpathList) {
27
+ if (dirpath.startsWith(configDirpath)) {
28
+ this.configDirPathByFilepath.set(filepath, configDirpath)
29
+ return onFound(configDirpath, this.configPathByDirpath.get(configDirpath)!)
30
+ }
31
+ }
32
+ }
33
+
34
+ get(filepath: string) {
35
+ return this.findConfigDirpath(filepath, (configDirpath) => this.builderByConfigDirpath.get(configDirpath))
36
+ }
37
+
38
+ isContextSynchronizing(filepath: string) {
39
+ return this.findConfigDirpath(filepath, (configDirpath) => this.synchronizingByConfigDirpath.get(configDirpath))
40
+ }
41
+
42
+ create(configPath: string) {
43
+ const builder = new Builder()
44
+
45
+ const dirpath = path.dirname(configPath)
46
+ this.configDirPathByFilepath.set(configPath, dirpath)
47
+ this.builderByConfigDirpath.set(dirpath, builder)
48
+ this.configDirpathList.add(dirpath)
49
+ this.configPathByDirpath.set(dirpath, configPath)
50
+
51
+ return this
52
+ }
53
+
54
+ async setup(filepath: string) {
55
+ return this.findConfigDirpath(filepath, async (configDirpath, configPath) => {
56
+ const builder = this.builderByConfigDirpath.get(configDirpath)
57
+ if (!builder) {
58
+ return
59
+ }
60
+
61
+ const current = this.synchronizingByConfigDirpath.get(configDirpath)
62
+ if (current) {
63
+ return current
64
+ }
65
+
66
+ const synchronizing = builder.setup({ configPath })
67
+ this.synchronizingByConfigDirpath.set(configDirpath, synchronizing)
68
+
69
+ synchronizing.finally(() => {
70
+ this.synchronizingByConfigDirpath.set(configDirpath, false)
71
+
72
+ this._onSetup?.({ configPath })
73
+ })
74
+
75
+ return synchronizing
76
+ })
77
+ }
78
+
79
+ onSetup(callback: (args: { configPath: string }) => void) {
80
+ this._onSetup = callback
81
+ }
82
+ }
@@ -0,0 +1,25 @@
1
+ import { type ServerCapabilities, TextDocumentSyncKind } from 'vscode-languageserver'
2
+
3
+ export const serverCapabilities: ServerCapabilities = {
4
+ textDocumentSync: TextDocumentSyncKind.Incremental,
5
+ inlayHintProvider: {
6
+ resolveProvider: false,
7
+ },
8
+ workspace: {
9
+ workspaceFolders: {
10
+ supported: true,
11
+ changeNotifications: true,
12
+ },
13
+ },
14
+
15
+ completionProvider: {
16
+ resolveProvider: true,
17
+ completionItem: {
18
+ labelDetailsSupport: true,
19
+ },
20
+ },
21
+ definitionProvider: false,
22
+ hoverProvider: true,
23
+ colorProvider: true,
24
+ inlineValueProvider: true,
25
+ }
@@ -0,0 +1,68 @@
1
+ import { ColorInformation } from 'vscode-languageserver'
2
+ import type { PandaExtension } from '../index'
3
+ import { color2kToVsCodeColor } from '../tokens/color2k-to-vscode-color'
4
+ import { tryCatch } from 'lil-fp/func'
5
+ import { onError } from '../tokens/error'
6
+
7
+ export function registerColorHints(extension: PandaExtension) {
8
+ const { connection, documents, documentReady, parseSourceFile, getFileTokens, getPandaSettings } = extension
9
+
10
+ connection.onDocumentColor(
11
+ tryCatch(async (params) => {
12
+ const settings = await getPandaSettings()
13
+ if (!settings['color-hints.enabled']) return
14
+
15
+ await documentReady('🐼 onDocumentColor')
16
+
17
+ const doc = documents.get(params.textDocument.uri)
18
+ if (!doc) {
19
+ return []
20
+ }
21
+
22
+ const ctx = extension.getContext()
23
+ if (!ctx) return []
24
+
25
+ const parserResult = parseSourceFile(doc)
26
+ if (!parserResult) {
27
+ return []
28
+ }
29
+
30
+ const colors: ColorInformation[] = []
31
+
32
+ getFileTokens(doc, parserResult, (match) => {
33
+ const isColor = match.kind === 'token' && match.token.extensions?.vscodeColor
34
+ if (!isColor) return
35
+
36
+ // Add 1 color hint for each condition
37
+ if (match.token.extensions.conditions) {
38
+ if (settings['color-hints.semantic-tokens.enabled']) {
39
+ Object.values(match.token.extensions.conditions).forEach((value) => {
40
+ const [tokenRef] = ctx.tokens.getReferences(value)
41
+ const color = color2kToVsCodeColor(tokenRef.value)
42
+ if (!color) return
43
+
44
+ colors.push({ color, range: match.range as any })
45
+ })
46
+ }
47
+
48
+ return
49
+ }
50
+
51
+ colors.push({
52
+ color: match.token.extensions.vscodeColor,
53
+ range: match.range as any,
54
+ })
55
+ })
56
+
57
+ return colors
58
+ }, onError),
59
+ )
60
+
61
+ connection.onColorPresentation(() => {
62
+ return []
63
+ })
64
+
65
+ connection.onDidChangeConfiguration(async (_change) => {
66
+ connection.sendNotification('$/clear-colors')
67
+ })
68
+ }
@@ -0,0 +1,34 @@
1
+ import { tryCatch } from 'lil-fp/func'
2
+ import type { PandaExtension } from '../index'
3
+ import { onError } from '../tokens/error'
4
+
5
+ export function registerCompletion(extension: PandaExtension) {
6
+ const { connection, documents, documentReady, getClosestCompletionList, getPandaSettings } = extension
7
+
8
+ // This handler provides the initial list of the completion items.
9
+ connection.onCompletion(
10
+ tryCatch(async (params) => {
11
+ const isEnabled = await getPandaSettings('completions.enabled')
12
+ if (!isEnabled) return
13
+
14
+ await documentReady('✅ onCompletion')
15
+
16
+ const doc = documents.get(params.textDocument.uri)
17
+ if (!doc) {
18
+ return
19
+ }
20
+
21
+ // TODO recipe
22
+ // TODO token(xxx.yyy.zzz) paths in strings
23
+ const matches = await getClosestCompletionList(doc, params.position)
24
+ if (!matches?.length) {
25
+ return
26
+ }
27
+
28
+ return matches
29
+ }, onError),
30
+ )
31
+
32
+ // This handler resolves additional information for the item selected in the completion list.
33
+ connection.onCompletionResolve((item) => item)
34
+ }
@@ -0,0 +1,93 @@
1
+ import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'
2
+ import { TextDocument } from 'vscode-languageserver-textdocument'
3
+ import { type PandaExtension } from '../index'
4
+ import { tryCatch } from 'lil-fp/func'
5
+ import { onError } from '../tokens/error'
6
+
7
+ export function registerDiagnostics(context: PandaExtension) {
8
+ const {
9
+ connection,
10
+ debug,
11
+ documents,
12
+ documentReady,
13
+ loadPandaContext,
14
+ getContext,
15
+ parseSourceFile,
16
+ getFileTokens,
17
+ getPandaSettings,
18
+ } = context
19
+
20
+ const updateDocumentDiagnostics = tryCatch(async function (doc: TextDocument) {
21
+ const settings = await getPandaSettings()
22
+
23
+ if (!settings['diagnostics.enabled']) {
24
+ // this allows us to clear diagnostics
25
+ return connection.sendDiagnostics({
26
+ uri: doc.uri,
27
+ version: doc.version,
28
+ diagnostics: [],
29
+ })
30
+ }
31
+
32
+ debug(`Update diagnostics for ${doc.uri}`)
33
+
34
+ const diagnostics: Diagnostic[] = []
35
+ const parserResult = parseSourceFile(doc)
36
+
37
+ if (!parserResult) {
38
+ // this allows us to clear diagnostics
39
+ return connection.sendDiagnostics({
40
+ uri: doc.uri,
41
+ version: doc.version,
42
+ diagnostics: [],
43
+ })
44
+ }
45
+
46
+ getFileTokens(doc, parserResult, (match) => {
47
+ if (
48
+ match.kind === 'token' &&
49
+ match.token.extensions.kind === 'invalid-token-path' &&
50
+ settings['diagnostics.invalid-token-path']
51
+ ) {
52
+ diagnostics.push({
53
+ message: `🐼 Invalid token path`,
54
+ range: match.range,
55
+ severity: DiagnosticSeverity.Error,
56
+ code: match.token.name,
57
+ })
58
+ }
59
+ })
60
+
61
+ connection.sendDiagnostics({
62
+ uri: doc.uri,
63
+ version: doc.version,
64
+ diagnostics,
65
+ })
66
+ }, onError)
67
+
68
+ // Update diagnostics on document change
69
+ documents.onDidChangeContent(async (params) => {
70
+ await documentReady('🐼 diagnostics - onDidChangeContent')
71
+
72
+ // await when the server starts, then just get the context
73
+ if (!getContext()) {
74
+ await loadPandaContext(params.document.uri)
75
+ }
76
+
77
+ if (!getContext()) return
78
+
79
+ updateDocumentDiagnostics(params.document)
80
+ })
81
+
82
+ // Update diagnostics when watched file changes
83
+ connection.onDidChangeWatchedFiles(async (_change) => {
84
+ await documentReady('🐼 diagnostics - onDidChangeWatchedFiles')
85
+
86
+ const ctx = getContext()
87
+ if (!ctx) return
88
+
89
+ // Update all opened documents diagnostics
90
+ const docs = documents.all()
91
+ docs.forEach((doc) => updateDocumentDiagnostics(doc))
92
+ })
93
+ }
@@ -0,0 +1,79 @@
1
+ import { type PandaExtension } from '../index'
2
+ import { getMarkdownCss, nodeRangeToVsCodeRange, printTokenValue } from '../tokens/utils'
3
+ import { renderTokenColorPreview } from '../tokens/render-token-color-preview'
4
+ import { generateKeyframeCss } from '../tokens/generate-keyframe-css'
5
+ import { tryCatch } from 'lil-fp/func'
6
+ import { onError } from '../tokens/error'
7
+
8
+ export function registerHover(extension: PandaExtension) {
9
+ const { connection, documentReady, documents, getClosestToken, getClosestInstance, getPandaSettings } = extension
10
+
11
+ connection.onHover(
12
+ tryCatch(async (params) => {
13
+ const settings = await getPandaSettings()
14
+ if (!settings['hovers.enabled']) return
15
+
16
+ await documentReady('🐼 onHover')
17
+
18
+ const ctx = extension.getContext()
19
+ if (!ctx) return
20
+
21
+ const doc = documents.get(params.textDocument.uri)
22
+ if (!doc) {
23
+ return
24
+ }
25
+
26
+ if (settings['hovers.tokens.enabled']) {
27
+ // TODO recipe
28
+ const tokenMatch = getClosestToken(doc, params.position)
29
+ if (tokenMatch) {
30
+ if (tokenMatch.kind === 'token') {
31
+ const { token } = tokenMatch
32
+
33
+ const contents = [printTokenValue(token, settings)] as any[]
34
+ if (settings['hovers.tokens.css-preview.enabled']) {
35
+ const css = getMarkdownCss(ctx, { [tokenMatch.propName]: token.value }, settings).raw
36
+ contents.push({ language: 'css', value: css })
37
+ }
38
+
39
+ const category = token.extensions.category
40
+
41
+ if (category === 'colors' && settings['hovers.semantic-colors.enabled']) {
42
+ const preview = await renderTokenColorPreview(ctx, token)
43
+ if (preview) {
44
+ contents.push(preview)
45
+ }
46
+ }
47
+
48
+ if (category === 'animations' && token.extensions.prop) {
49
+ const keyframeCss = generateKeyframeCss(ctx, token.extensions.prop)
50
+ if (keyframeCss) {
51
+ contents.push({ language: 'css', value: keyframeCss })
52
+ }
53
+ }
54
+
55
+ return { contents, range: tokenMatch.range }
56
+ }
57
+
58
+ if (tokenMatch.kind === 'condition' && settings['hovers.conditions.enabled']) {
59
+ const { condition, propValue, propName } = tokenMatch
60
+ const css = getMarkdownCss(ctx, { [propName]: propValue }, settings).raw
61
+
62
+ return {
63
+ contents: [`🐼 \`${condition.value}\``, { language: 'css', value: css }],
64
+ range: tokenMatch.range,
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ if (settings['hovers.instances.enabled']) {
71
+ const instanceMatch = getClosestInstance(doc, params.position)
72
+ if (instanceMatch && instanceMatch.kind === 'styles') {
73
+ const range = nodeRangeToVsCodeRange(instanceMatch.props.getRange())
74
+ return { contents: getMarkdownCss(ctx, instanceMatch.styles, settings).withCss, range }
75
+ }
76
+ }
77
+ }, onError),
78
+ )
79
+ }
@@ -0,0 +1,5 @@
1
+ export * from './color-hints'
2
+ export * from './completion'
3
+ export * from './inlay-hints'
4
+ export * from './hover'
5
+ export * from './diagnostics'
@@ -0,0 +1,61 @@
1
+ import { InlayHint, InlayHintKind, type InlayHintParams } from 'vscode-languageserver'
2
+ import { type PandaExtension } from '..'
3
+ import { printTokenValue } from '../tokens/utils'
4
+ import { tryCatch } from 'lil-fp/func'
5
+ import { onError } from '../tokens/error'
6
+
7
+ export function registerInlayHints(context: PandaExtension) {
8
+ const {
9
+ connection,
10
+ documents,
11
+ documentReady,
12
+ loadPandaContext,
13
+ getContext,
14
+ parseSourceFile,
15
+ getFileTokens,
16
+ getPandaSettings,
17
+ } = context
18
+
19
+ connection.languages.inlayHint.on(
20
+ tryCatch(async (params: InlayHintParams) => {
21
+ const settings = await getPandaSettings()
22
+ if (!settings['inlay-hints.enabled']) return
23
+
24
+ await documentReady('🐼 inlay hints')
25
+
26
+ // await when the server starts, then just get the context
27
+ if (!getContext()) {
28
+ await loadPandaContext(params.textDocument.uri)
29
+ }
30
+
31
+ const doc = documents.get(params.textDocument.uri)
32
+ if (!doc) {
33
+ return []
34
+ }
35
+
36
+ const parserResult = parseSourceFile(doc)
37
+ if (!parserResult) return
38
+
39
+ const inlayHints = [] as InlayHint[]
40
+
41
+ getFileTokens(doc, parserResult, (match) => {
42
+ if (
43
+ match.kind === 'token' &&
44
+ match.token.extensions.kind !== 'color' &&
45
+ match.token.extensions.kind !== 'semantic-color' &&
46
+ match.token.extensions.kind !== 'native-color' &&
47
+ match.token.extensions.kind !== 'invalid-token-path'
48
+ ) {
49
+ inlayHints.push({
50
+ position: match.range.end,
51
+ label: printTokenValue(match.token, settings),
52
+ kind: InlayHintKind.Type,
53
+ paddingLeft: true,
54
+ })
55
+ }
56
+ })
57
+
58
+ return inlayHints
59
+ }, onError),
60
+ )
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,130 @@
1
+ import {
2
+ createConnection,
3
+ TextDocuments,
4
+ ProposedFeatures,
5
+ type ServerRequestHandler,
6
+ } from 'vscode-languageserver/node'
7
+ import { TextDocument } from 'vscode-languageserver-textdocument'
8
+ import { setupBuilder } from './setup-builder'
9
+ import { setupTokensHelpers } from './tokens/setup-tokens-helpers'
10
+ import * as features from './features'
11
+
12
+ // Create a connection for the server, using Node's IPC as a transport.
13
+ // Also include all preview / proposed LSP features.
14
+ const original = createConnection(ProposedFeatures.all)
15
+
16
+ // Wrap the connection in a proxy to be able to register multiple handlers for the same event
17
+ // ex: onDidChangeWatchedFiles
18
+ const connection = createConnectionProxy() as typeof original
19
+
20
+ // Create a simple text document manager.
21
+ const documents = new TextDocuments(TextDocument)
22
+
23
+ let isDebug = false
24
+
25
+ // Deferred promise to wait for extension to have resolved all the workspaces configs
26
+ let resolveDeferred: (value?: unknown) => void
27
+ const deferred = new Promise((resolve) => {
28
+ resolveDeferred = resolve
29
+ })
30
+
31
+ // Initialize extension.
32
+ const setup = setupBuilder(connection, documents, {
33
+ onDidChangeConfiguration: (settings) => {
34
+ isDebug = settings['debug.enabled'] ?? false
35
+ },
36
+ onReady() {
37
+ resolveDeferred()
38
+ },
39
+ })
40
+
41
+ // Most of the tokens retrieval logic is shared between features
42
+ const tokensHelpers = setupTokensHelpers(setup)
43
+
44
+ /**
45
+ * Debug message helper
46
+ */
47
+ function debug(message: string) {
48
+ isDebug && connection.console.log(message)
49
+ }
50
+
51
+ /**
52
+ * 1 - wait for the extension to have resolved all the workspaces configs before doing anything
53
+ * 2 - Check await until tokens are synchronized if a synchronization process is happening.
54
+ *
55
+ * should be used AFTER checking that the feature is enabled through settings
56
+ * and BEFORE getting the current context
57
+ */
58
+ async function documentReady(step: string) {
59
+ // wait for the extension to have resolved all the workspaces configs before doing anything
60
+ await deferred
61
+
62
+ try {
63
+ if (setup.isSynchronizing()) {
64
+ await setup.isSynchronizing()
65
+ }
66
+
67
+ debug(step)
68
+ } catch (error) {
69
+ connection.console.error('error on step ' + step)
70
+ connection.console.error(error as any)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extension context to be injected into features.
76
+ */
77
+ const extension = {
78
+ connection,
79
+ documents,
80
+ debug,
81
+ documentReady,
82
+ ...setup,
83
+ ...tokensHelpers,
84
+ } as const
85
+
86
+ /**
87
+ * Register each language server feature with extension context.
88
+ * wait for the extension to have resolved all the workspaces configs before doing anything
89
+ */
90
+ Object.entries(features).forEach(([key, registerFeature]) => {
91
+ debug(`🐼 Feature: ${key}`)
92
+ registerFeature(extension)
93
+ })
94
+
95
+ export type PandaExtension = typeof extension
96
+
97
+ type EventKeys<T> = {
98
+ [K in keyof T]: T[K] extends (...args: any[]) => any ? (K extends `on${string}` ? K : never) : never
99
+ }[keyof T]
100
+ type ConnectionEvents = EventKeys<typeof original>
101
+
102
+ function createConnectionProxy(): Pick<typeof original, ConnectionEvents> {
103
+ const ignoredEvents = ['onNotification', 'onRequest', 'onInitialize']
104
+ const eventMap = new Map<string, any[]>()
105
+
106
+ return new Proxy(original, {
107
+ get(target, prop) {
108
+ if (typeof prop === 'string' && prop.startsWith('on') && !ignoredEvents.includes(prop)) {
109
+ const handlers = eventMap.get(prop) ?? []
110
+ eventMap.set(prop, handlers)
111
+
112
+ // @ts-ignore
113
+ target[prop].call(target, (...args: any[]) => {
114
+ let result
115
+ handlers.forEach((handler) => {
116
+ result = handler(...args)
117
+ })
118
+
119
+ return result
120
+ })
121
+ return (handler: ServerRequestHandler<any, any, any, any>) => {
122
+ handlers.push(handler)
123
+ }
124
+ }
125
+
126
+ // @ts-ignore
127
+ return target[prop]
128
+ },
129
+ })
130
+ }