symbols-app-connect 3.2.8
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/README.md +106 -0
- package/eslint.config.js +6 -0
- package/icon.png +0 -0
- package/out/extension.js +2814 -0
- package/package.json +103 -0
- package/src/data/components.ts +182 -0
- package/src/data/cssProperties.ts +187 -0
- package/src/data/designSystemValues.ts +294 -0
- package/src/data/domqlKeys.ts +321 -0
- package/src/data/elementMethods.ts +385 -0
- package/src/data/events.ts +368 -0
- package/src/extension.ts +82 -0
- package/src/providers/completionProvider.ts +595 -0
- package/src/providers/definitionProvider.ts +201 -0
- package/src/providers/hoverProvider.ts +162 -0
- package/src/providers/workspaceScanner.ts +98 -0
- package/symbols-app-connect-3.2.4.vsix +0 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as vscode from 'vscode'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { isDomqlFile } from './completionProvider'
|
|
5
|
+
|
|
6
|
+
const PASCAL_CASE_RE = /^[A-Z][a-zA-Z0-9]+$/
|
|
7
|
+
|
|
8
|
+
/** Walk up from a file to find symbols.json, return its dir and location */
|
|
9
|
+
function findSymbolsConfig(fromPath: string): { root: string; dir: string } | null {
|
|
10
|
+
let current = path.dirname(fromPath)
|
|
11
|
+
while (true) {
|
|
12
|
+
const configPath = path.join(current, 'symbols.json')
|
|
13
|
+
if (fs.existsSync(configPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
16
|
+
return { root: current, dir: json.dir || './symbols' }
|
|
17
|
+
} catch {
|
|
18
|
+
return { root: current, dir: './symbols' }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const parent = path.dirname(current)
|
|
22
|
+
if (parent === current) break
|
|
23
|
+
current = parent
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DomqlDefinitionProvider implements vscode.DefinitionProvider {
|
|
29
|
+
async provideDefinition(
|
|
30
|
+
document: vscode.TextDocument,
|
|
31
|
+
position: vscode.Position
|
|
32
|
+
): Promise<vscode.Definition | null> {
|
|
33
|
+
if (!vscode.workspace.getConfiguration('symbolsApp').get('enable', true)) return null
|
|
34
|
+
|
|
35
|
+
const fullText = document.getText()
|
|
36
|
+
const config = vscode.workspace.getConfiguration('symbolsApp')
|
|
37
|
+
if (!isDomqlFile(fullText, config.get('detectByImports', true))) return null
|
|
38
|
+
|
|
39
|
+
const wordRange =
|
|
40
|
+
document.getWordRangeAtPosition(position, /[A-Z][a-zA-Z0-9]+/) ||
|
|
41
|
+
document.getWordRangeAtPosition(position, /[a-zA-Z][a-zA-Z0-9]+/)
|
|
42
|
+
if (!wordRange) return null
|
|
43
|
+
|
|
44
|
+
const word = document.getText(wordRange)
|
|
45
|
+
if (!PASCAL_CASE_RE.test(word)) return null
|
|
46
|
+
|
|
47
|
+
const line = document.lineAt(position).text
|
|
48
|
+
|
|
49
|
+
const insideString = isInsideQuotes(line, wordRange.start.character)
|
|
50
|
+
const isObjKey = line.substring(wordRange.end.character).trimStart().startsWith(':')
|
|
51
|
+
const textBefore = line.substring(0, wordRange.start.character)
|
|
52
|
+
const isDirectRef = /(?:extends|childExtends|childExtendsRecursive|childExtend)\s*:\s*$/.test(textBefore.trimEnd())
|
|
53
|
+
const isArrayRef = /(?:extends|childExtends)\s*:\s*\[/.test(textBefore)
|
|
54
|
+
|
|
55
|
+
if (!insideString && !isObjKey && !isDirectRef && !isArrayRef) return null
|
|
56
|
+
|
|
57
|
+
// 1. Fast: convention-based lookup from symbols.json
|
|
58
|
+
const conventionResult = this.findByConvention(word, document.uri.fsPath)
|
|
59
|
+
if (conventionResult) return conventionResult
|
|
60
|
+
|
|
61
|
+
// 2. Slow: workspace-wide search
|
|
62
|
+
return this.findByWorkspaceSearch(word, document.uri)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private findByConvention(name: string, filePath: string): vscode.Location | null {
|
|
66
|
+
const symConfig = findSymbolsConfig(filePath)
|
|
67
|
+
|
|
68
|
+
// Collect all candidate directories to search
|
|
69
|
+
const searchDirs: string[] = []
|
|
70
|
+
|
|
71
|
+
if (symConfig) {
|
|
72
|
+
const symbolsBase = path.resolve(symConfig.root, symConfig.dir)
|
|
73
|
+
searchDirs.push(
|
|
74
|
+
path.join(symbolsBase, 'components'),
|
|
75
|
+
symbolsBase,
|
|
76
|
+
path.join(symConfig.root, 'components'),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Also check relative to current file
|
|
81
|
+
const fileDir = path.dirname(filePath)
|
|
82
|
+
searchDirs.push(
|
|
83
|
+
path.join(fileDir, '..', 'components'), // sibling components dir
|
|
84
|
+
path.join(fileDir, 'components'),
|
|
85
|
+
fileDir,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Also check workspace folders
|
|
89
|
+
const folders = vscode.workspace.workspaceFolders
|
|
90
|
+
if (folders) {
|
|
91
|
+
for (const f of folders) {
|
|
92
|
+
searchDirs.push(
|
|
93
|
+
path.join(f.uri.fsPath, 'components'),
|
|
94
|
+
path.join(f.uri.fsPath, 'symbols', 'components'),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx']
|
|
100
|
+
|
|
101
|
+
for (const dir of searchDirs) {
|
|
102
|
+
// Try direct file: components/Name.js
|
|
103
|
+
for (const ext of extensions) {
|
|
104
|
+
const candidate = path.join(dir, `${name}${ext}`)
|
|
105
|
+
const loc = this.resolveFile(candidate, name)
|
|
106
|
+
if (loc) return loc
|
|
107
|
+
}
|
|
108
|
+
// Try directory: components/Name/index.js
|
|
109
|
+
for (const ext of extensions) {
|
|
110
|
+
const candidate = path.join(dir, name, `index${ext}`)
|
|
111
|
+
const loc = this.resolveFile(candidate, name)
|
|
112
|
+
if (loc) return loc
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private resolveFile(filePath: string, name: string): vscode.Location | null {
|
|
120
|
+
if (!fs.existsSync(filePath)) return null
|
|
121
|
+
try {
|
|
122
|
+
const text = fs.readFileSync(filePath, 'utf-8')
|
|
123
|
+
const lines = text.split('\n')
|
|
124
|
+
const pattern = new RegExp(`(?:export\\s+)?(?:const|let|var|function)\\s+${name}\\b`)
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
if (pattern.test(lines[i])) {
|
|
127
|
+
return new vscode.Location(vscode.Uri.file(filePath), new vscode.Position(i, 0))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// File matches by name, go to top
|
|
131
|
+
return new vscode.Location(vscode.Uri.file(filePath), new vscode.Position(0, 0))
|
|
132
|
+
} catch {
|
|
133
|
+
return new vscode.Location(vscode.Uri.file(filePath), new vscode.Position(0, 0))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async findByWorkspaceSearch(
|
|
138
|
+
name: string,
|
|
139
|
+
currentUri: vscode.Uri
|
|
140
|
+
): Promise<vscode.Location | null> {
|
|
141
|
+
try {
|
|
142
|
+
// Targeted filename search first
|
|
143
|
+
const nameFiles = await vscode.workspace.findFiles(
|
|
144
|
+
`**/${name}.{js,ts,jsx,tsx}`,
|
|
145
|
+
'{**/node_modules/**,**/dist/**,**/out/**,**/build/**}',
|
|
146
|
+
10
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for (const file of nameFiles) {
|
|
150
|
+
try {
|
|
151
|
+
const doc = await vscode.workspace.openTextDocument(file)
|
|
152
|
+
const text = doc.getText()
|
|
153
|
+
if (!text.includes(name)) continue
|
|
154
|
+
const lines = text.split('\n')
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
if (new RegExp(`(?:export\\s+)?(?:const|let|var|function)\\s+${name}\\s*[=({]`).test(lines[i])) {
|
|
157
|
+
return new vscode.Location(file, new vscode.Position(i, 0))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return new vscode.Location(file, new vscode.Position(0, 0))
|
|
161
|
+
} catch { /* skip */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Broader search
|
|
165
|
+
const files = await vscode.workspace.findFiles(
|
|
166
|
+
'**/*.{js,ts,jsx,tsx}',
|
|
167
|
+
'{**/node_modules/**,**/dist/**,**/out/**,**/build/**,.next/**}',
|
|
168
|
+
300
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
try {
|
|
173
|
+
const doc = await vscode.workspace.openTextDocument(file)
|
|
174
|
+
const text = doc.getText()
|
|
175
|
+
if (!text.includes(name)) continue
|
|
176
|
+
const lines = text.split('\n')
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
if (new RegExp(`^export\\s+(?:const|let|var)\\s+${name}\\s*=`).test(lines[i])) {
|
|
179
|
+
return new vscode.Location(file, new vscode.Position(i, 0))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch { /* skip */ }
|
|
183
|
+
}
|
|
184
|
+
} catch { /* failed */ }
|
|
185
|
+
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isInsideQuotes(line: string, charIndex: number): boolean {
|
|
191
|
+
let inSingle = false
|
|
192
|
+
let inDouble = false
|
|
193
|
+
for (let i = 0; i < charIndex; i++) {
|
|
194
|
+
const ch = line[i]
|
|
195
|
+
const prev = i > 0 ? line[i - 1] : ''
|
|
196
|
+
if (prev === '\\') continue
|
|
197
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle
|
|
198
|
+
if (ch === '"' && !inSingle) inDouble = !inDouble
|
|
199
|
+
}
|
|
200
|
+
return inSingle || inDouble
|
|
201
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as vscode from 'vscode'
|
|
2
|
+
import { DOMQL_ALL_KEYS } from '../data/domqlKeys'
|
|
3
|
+
import { ALL_EVENTS } from '../data/events'
|
|
4
|
+
import { ELEMENT_METHODS, STATE_METHODS } from '../data/elementMethods'
|
|
5
|
+
import { ALL_COMPONENTS } from '../data/components'
|
|
6
|
+
import { ALL_CSS_PROPS } from '../data/cssProperties'
|
|
7
|
+
import {
|
|
8
|
+
COLOR_TOKENS, COLOR_TOKEN_MAP, GRADIENT_TOKENS, THEME_TOKENS,
|
|
9
|
+
ICON_NAMES, SPACING_SCALE, SPACING_TOKENS, TYPOGRAPHY_TOKENS, TIMING_TOKENS,
|
|
10
|
+
SEQUENCE_CONFIGS, COLOR_PROPERTIES,
|
|
11
|
+
SPACING_PROPERTIES, FONT_SIZE_PROPERTIES
|
|
12
|
+
} from '../data/designSystemValues'
|
|
13
|
+
import { isDomqlFile } from './completionProvider'
|
|
14
|
+
|
|
15
|
+
// Build lookup maps once
|
|
16
|
+
const keyMap = new Map<string, string>()
|
|
17
|
+
|
|
18
|
+
for (const k of DOMQL_ALL_KEYS) {
|
|
19
|
+
keyMap.set(k.label, `**${k.detail}**\n\n${k.documentation}`)
|
|
20
|
+
}
|
|
21
|
+
for (const ev of ALL_EVENTS) {
|
|
22
|
+
keyMap.set(ev.label, `**${ev.detail}**\n\n${ev.documentation}`)
|
|
23
|
+
}
|
|
24
|
+
for (const m of ELEMENT_METHODS) {
|
|
25
|
+
keyMap.set(m.label, `**${m.detail}**\n\n${m.documentation}`)
|
|
26
|
+
}
|
|
27
|
+
for (const m of STATE_METHODS) {
|
|
28
|
+
keyMap.set(`state.${m.label}`, `**${m.detail}**\n\n${m.documentation}`)
|
|
29
|
+
}
|
|
30
|
+
for (const c of ALL_COMPONENTS) {
|
|
31
|
+
keyMap.set(c.label, `**${c.detail}**\n\n${c.documentation}`)
|
|
32
|
+
}
|
|
33
|
+
for (const p of ALL_CSS_PROPS) {
|
|
34
|
+
if (p.documentation) keyMap.set(p.label, `**${p.detail}**\n\n${p.documentation}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Design system value hover info
|
|
38
|
+
const valueHints = new Map<string, string>()
|
|
39
|
+
|
|
40
|
+
for (const c of COLOR_TOKEN_MAP) {
|
|
41
|
+
if (c.label !== 'inherit' && c.label !== 'none' && c.label !== 'currentColor') {
|
|
42
|
+
const hexInfo = c.hex ? ` → \`${c.hex}\`` : ''
|
|
43
|
+
const desc = c.description ? `\n\n${c.description}` : ''
|
|
44
|
+
valueHints.set(c.label, `**Color token:** \`${c.label}\`${hexInfo}${desc}\n\nModifiers: \`${c.label}.5\` (opacity), \`${c.label}+16\` (lighten), \`${c.label}-16\` (darken), \`${c.label}=50\` (set lightness)`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const g of GRADIENT_TOKENS) {
|
|
48
|
+
valueHints.set(g, `**Gradient token:** \`${g}\``)
|
|
49
|
+
}
|
|
50
|
+
for (const t of THEME_TOKENS) {
|
|
51
|
+
valueHints.set(t, `**Theme:** \`${t}\`\n\nUsage: \`theme: "${t}"\`\n\nModifiers: \`"${t} .child"\`, \`"${t} .color-only"\``)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Detect if the hovered word is in a value position and what property it belongs to */
|
|
55
|
+
function getPropertyContext(document: vscode.TextDocument, position: vscode.Position): string | null {
|
|
56
|
+
const line = document.lineAt(position).text
|
|
57
|
+
const colonIdx = line.indexOf(':')
|
|
58
|
+
if (colonIdx === -1 || position.character <= colonIdx) return null
|
|
59
|
+
const beforeColon = line.substring(0, colonIdx).trim()
|
|
60
|
+
const m = beforeColon.match(/(\w+)$/)
|
|
61
|
+
return m ? m[1] : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class DomqlHoverProvider implements vscode.HoverProvider {
|
|
65
|
+
provideHover(
|
|
66
|
+
document: vscode.TextDocument,
|
|
67
|
+
position: vscode.Position
|
|
68
|
+
): vscode.Hover | null {
|
|
69
|
+
if (!vscode.workspace.getConfiguration('symbolsApp').get('enable', true)) return null
|
|
70
|
+
|
|
71
|
+
const fullText = document.getText()
|
|
72
|
+
const config = vscode.workspace.getConfiguration('symbolsApp')
|
|
73
|
+
if (!isDomqlFile(fullText, config.get('detectByImports', true))) return null
|
|
74
|
+
|
|
75
|
+
const wordRange = document.getWordRangeAtPosition(position, /[\w.@:-]+/)
|
|
76
|
+
if (!wordRange) return null
|
|
77
|
+
|
|
78
|
+
const word = document.getText(wordRange)
|
|
79
|
+
|
|
80
|
+
// Check key docs
|
|
81
|
+
const docs = keyMap.get(word)
|
|
82
|
+
if (docs) {
|
|
83
|
+
const md = new vscode.MarkdownString(docs)
|
|
84
|
+
md.isTrusted = true
|
|
85
|
+
return new vscode.Hover(md, wordRange)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if it's a design system value in a value position
|
|
89
|
+
const prop = getPropertyContext(document, position)
|
|
90
|
+
if (prop) {
|
|
91
|
+
// Spacing tokens
|
|
92
|
+
if (SPACING_PROPERTIES.has(prop)) {
|
|
93
|
+
const token = SPACING_TOKENS.find(t => t.label === word)
|
|
94
|
+
if (token) {
|
|
95
|
+
const cfg = SEQUENCE_CONFIGS.spacing
|
|
96
|
+
const md = new vscode.MarkdownString(`**Spacing token:** \`${word}\` ≈ **${token.approxValue}**\n\nBase: A = ${cfg.base}px, ratio: ${cfg.ratio} (golden ratio)\n\nScale: W X Y Z **A** B C D E F G H\n\nOperations: \`A+B\`, \`A-Z\`, \`A*2\`, \`-A\` (negative)`)
|
|
97
|
+
md.isTrusted = true
|
|
98
|
+
return new vscode.Hover(md, wordRange)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Typography tokens
|
|
103
|
+
if (FONT_SIZE_PROPERTIES.has(prop)) {
|
|
104
|
+
const token = TYPOGRAPHY_TOKENS.find(t => t.label === word)
|
|
105
|
+
if (token) {
|
|
106
|
+
const cfg = SEQUENCE_CONFIGS.typography
|
|
107
|
+
const md = new vscode.MarkdownString(`**Typography token:** \`${word}\` ≈ **${token.approxValue}**\n\nBase: A = ${cfg.base}px, ratio: ${cfg.ratio} (major third)\n\nScale: X Y Z **A** B C D E F G H`)
|
|
108
|
+
md.isTrusted = true
|
|
109
|
+
return new vscode.Hover(md, wordRange)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Timing tokens
|
|
114
|
+
if (prop === 'transition' || prop === 'transitionDuration' || prop === 'animationDuration') {
|
|
115
|
+
const token = TIMING_TOKENS.find(t => t.label === word)
|
|
116
|
+
if (token) {
|
|
117
|
+
const cfg = SEQUENCE_CONFIGS.timing
|
|
118
|
+
const md = new vscode.MarkdownString(`**Timing token:** \`${word}\` ≈ **${token.approxValue}**\n\nBase: A = ${cfg.base}ms, ratio: ${cfg.ratio} (perfect fourth)`)
|
|
119
|
+
md.isTrusted = true
|
|
120
|
+
return new vscode.Hover(md, wordRange)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Color values
|
|
125
|
+
if (COLOR_PROPERTIES.has(prop) || prop === 'background') {
|
|
126
|
+
const hint = valueHints.get(word)
|
|
127
|
+
if (hint) {
|
|
128
|
+
const md = new vscode.MarkdownString(hint)
|
|
129
|
+
md.isTrusted = true
|
|
130
|
+
return new vscode.Hover(md, wordRange)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Theme values
|
|
135
|
+
if (prop === 'theme') {
|
|
136
|
+
const hint = valueHints.get(word)
|
|
137
|
+
if (hint) {
|
|
138
|
+
const md = new vscode.MarkdownString(hint)
|
|
139
|
+
md.isTrusted = true
|
|
140
|
+
return new vscode.Hover(md, wordRange)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Icon names
|
|
145
|
+
if ((prop === 'icon' || prop === 'name') && ICON_NAMES.includes(word)) {
|
|
146
|
+
const md = new vscode.MarkdownString(`**Icon:** \`${word}\`\n\nDefault icon from design system sprite`)
|
|
147
|
+
md.isTrusted = true
|
|
148
|
+
return new vscode.Hover(md, wordRange)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// General value hints (color/gradient/theme tokens anywhere)
|
|
153
|
+
const generalHint = valueHints.get(word)
|
|
154
|
+
if (generalHint) {
|
|
155
|
+
const md = new vscode.MarkdownString(generalHint)
|
|
156
|
+
md.isTrusted = true
|
|
157
|
+
return new vscode.Hover(md, wordRange)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as vscode from 'vscode'
|
|
2
|
+
|
|
3
|
+
export interface ComponentLocation {
|
|
4
|
+
uri: vscode.Uri
|
|
5
|
+
line: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ProjectCache {
|
|
9
|
+
components: Map<string, ComponentLocation>
|
|
10
|
+
lastScan: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SCAN_INTERVAL = 30_000
|
|
14
|
+
const PASCAL_CASE_RE = /^[A-Z][a-zA-Z0-9]+$/
|
|
15
|
+
|
|
16
|
+
let cache: ProjectCache = { components: new Map(), lastScan: 0 }
|
|
17
|
+
|
|
18
|
+
// Patterns that export DOMQL components
|
|
19
|
+
const EXPORT_RE = /export\s+(?:const|let|var|function)\s+([A-Z][a-zA-Z0-9]+)/g
|
|
20
|
+
const OBJECT_KEY_RE = /^\s+([A-Z][a-zA-Z0-9]+)\s*[:{]/gm
|
|
21
|
+
|
|
22
|
+
function extractComponentsWithLines(text: string): { name: string; line: number }[] {
|
|
23
|
+
const results: { name: string; line: number }[] = []
|
|
24
|
+
const seen = new Set<string>()
|
|
25
|
+
|
|
26
|
+
// Named exports: export const Button = { ... }
|
|
27
|
+
EXPORT_RE.lastIndex = 0
|
|
28
|
+
let m: RegExpExecArray | null
|
|
29
|
+
while ((m = EXPORT_RE.exec(text))) {
|
|
30
|
+
if (PASCAL_CASE_RE.test(m[1]) && !seen.has(m[1])) {
|
|
31
|
+
seen.add(m[1])
|
|
32
|
+
const line = text.substring(0, m.index).split('\n').length - 1
|
|
33
|
+
results.push({ name: m[1], line })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Object keys inside component definitions
|
|
38
|
+
OBJECT_KEY_RE.lastIndex = 0
|
|
39
|
+
while ((m = OBJECT_KEY_RE.exec(text))) {
|
|
40
|
+
if (PASCAL_CASE_RE.test(m[1]) && !seen.has(m[1])) {
|
|
41
|
+
seen.add(m[1])
|
|
42
|
+
const line = text.substring(0, m.index).split('\n').length - 1
|
|
43
|
+
results.push({ name: m[1], line })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return results
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function scanWorkspaceComponents(): Promise<string[]> {
|
|
51
|
+
await ensureScan()
|
|
52
|
+
return [...cache.components.keys()].sort()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getComponentLocation(name: string): Promise<ComponentLocation | undefined> {
|
|
56
|
+
await ensureScan()
|
|
57
|
+
return cache.components.get(name)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureScan(): Promise<void> {
|
|
61
|
+
const now = Date.now()
|
|
62
|
+
if (now - cache.lastScan < SCAN_INTERVAL && cache.components.size > 0) return
|
|
63
|
+
|
|
64
|
+
const components = new Map<string, ComponentLocation>()
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const files = await vscode.workspace.findFiles(
|
|
68
|
+
'**/*.{js,ts,jsx,tsx}',
|
|
69
|
+
'{**/node_modules/**,**/dist/**,**/out/**,**/build/**,.next/**}',
|
|
70
|
+
500
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
try {
|
|
75
|
+
const doc = await vscode.workspace.openTextDocument(file)
|
|
76
|
+
const text = doc.getText()
|
|
77
|
+
if (/extends\s*:|childExtends|from\s+['"](@domql|@symbo\.ls|smbls)/.test(text)) {
|
|
78
|
+
for (const { name, line } of extractComponentsWithLines(text)) {
|
|
79
|
+
// First definition found wins (don't overwrite)
|
|
80
|
+
if (!components.has(name)) {
|
|
81
|
+
components.set(name, { uri: file, line })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// skip unreadable files
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// workspace not available
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cache = { components, lastScan: now }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function invalidateCache(): void {
|
|
97
|
+
cache.lastScan = 0
|
|
98
|
+
}
|
|
Binary file
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"sourceMap": true,
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"outDir": "out",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"],
|
|
14
|
+
"exclude": ["node_modules", ".vscode-test"]
|
|
15
|
+
}
|