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