@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
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,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
|
+
}
|