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,595 @@
|
|
|
1
|
+
import * as vscode from 'vscode'
|
|
2
|
+
import { DOMQL_ALL_KEYS } from '../data/domqlKeys'
|
|
3
|
+
import { DOM_EVENTS, DOMQL_LIFECYCLE_EVENTS } from '../data/events'
|
|
4
|
+
import { ALL_CSS_PROPS } from '../data/cssProperties'
|
|
5
|
+
import { ALL_COMPONENTS } from '../data/components'
|
|
6
|
+
import { ELEMENT_METHODS, STATE_METHODS, HTML_ATTRIBUTES } from '../data/elementMethods'
|
|
7
|
+
import {
|
|
8
|
+
SPACING_SCALE, SPACING_TOKENS, TYPOGRAPHY_TOKENS, TIMING_TOKENS,
|
|
9
|
+
SEQUENCE_CONFIGS, FONT_SIZE_SCALE, COLOR_TOKENS, COLOR_TOKEN_MAP, COLOR_MODIFIERS,
|
|
10
|
+
GRADIENT_TOKENS, THEME_TOKENS, THEME_MODIFIERS, ICON_NAMES,
|
|
11
|
+
MEDIA_TOKENS, HTML_TAGS, CSS_VALUE_ENUMS,
|
|
12
|
+
COLOR_PROPERTIES, SPACING_PROPERTIES, FONT_SIZE_PROPERTIES,
|
|
13
|
+
INPUT_TYPES, TARGET_VALUES, REL_VALUES, AUTOCOMPLETE_VALUES,
|
|
14
|
+
BOOLEAN_VALUES, LOADING_VALUES
|
|
15
|
+
} from '../data/designSystemValues'
|
|
16
|
+
import { scanWorkspaceComponents } from './workspaceScanner'
|
|
17
|
+
|
|
18
|
+
// Patterns that indicate a file uses DOMQL / Symbols.app
|
|
19
|
+
const DOMQL_IMPORT_RE = /from\s+['"](@domql\/|domql|@symbo\.ls\/|smbls)/
|
|
20
|
+
const DOMQL_SIGNATURE_RE = /\b(extends|childExtends|childExtendsRecursive|onRender|onStateUpdate|onInit)\s*:/
|
|
21
|
+
// Design system patterns: flow, theme, round, boxSize, align + PascalCase component keys
|
|
22
|
+
const DESIGN_SYSTEM_RE = /\b(flow|theme|round|boxSize|childExtend|widthRange|heightRange)\s*:\s*['"`]/
|
|
23
|
+
const COMPONENT_EXPORT_RE = /export\s+(?:const|let|var)\s+[A-Z][a-zA-Z0-9]+\s*=\s*\{/
|
|
24
|
+
|
|
25
|
+
export function isDomqlFile(text: string, detectByImports: boolean): boolean {
|
|
26
|
+
if (DOMQL_IMPORT_RE.test(text)) return true
|
|
27
|
+
if (!detectByImports) return true
|
|
28
|
+
if (DOMQL_SIGNATURE_RE.test(text)) return true
|
|
29
|
+
if (DESIGN_SYSTEM_RE.test(text)) return true
|
|
30
|
+
if (COMPONENT_EXPORT_RE.test(text)) return true
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type DomqlContext =
|
|
35
|
+
| 'element-key'
|
|
36
|
+
| 'element-value'
|
|
37
|
+
| 'attr-key'
|
|
38
|
+
| 'attr-value'
|
|
39
|
+
| 'state-key'
|
|
40
|
+
| 'on-key'
|
|
41
|
+
| 'define-key'
|
|
42
|
+
| 'el-method'
|
|
43
|
+
| 'state-method'
|
|
44
|
+
| 'call-arg'
|
|
45
|
+
| 'none'
|
|
46
|
+
|
|
47
|
+
interface ContextInfo {
|
|
48
|
+
type: DomqlContext
|
|
49
|
+
propertyName?: string // the key whose value we're completing
|
|
50
|
+
enclosingKey?: string // parent object key (attr, state, on, etc.)
|
|
51
|
+
enclosingTag?: string // detected tag for the element
|
|
52
|
+
inString?: boolean // whether cursor is already inside quotes
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Walk backwards from `offset` and find the property key that owns the `{`
|
|
57
|
+
* immediately enclosing the cursor position.
|
|
58
|
+
*/
|
|
59
|
+
function findEnclosingKey(text: string, offset: number): string | null {
|
|
60
|
+
let depth = 0
|
|
61
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
62
|
+
const ch = text[i]
|
|
63
|
+
if (ch === '}' || ch === ']') {
|
|
64
|
+
depth++
|
|
65
|
+
} else if (ch === '{' || ch === '[') {
|
|
66
|
+
if (depth === 0) {
|
|
67
|
+
const before = text.substring(0, i).trimEnd()
|
|
68
|
+
const m = before.match(/(\w+)\s*:\s*$/)
|
|
69
|
+
return m ? m[1] : null
|
|
70
|
+
}
|
|
71
|
+
depth--
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns true when the cursor is at a key position (no colon yet on this property). */
|
|
78
|
+
function isAtKeyPosition(linePrefix: string): boolean {
|
|
79
|
+
const afterDelim = linePrefix.split(/[,{]/).pop() ?? ''
|
|
80
|
+
return !/:/.test(afterDelim)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Extract the property name before the colon when cursor is in value position */
|
|
84
|
+
function getPropertyNameBeforeColon(linePrefix: string): string | null {
|
|
85
|
+
// Match: ` propertyName: ` or ` propertyName: 'partial` or ` propertyName: "partial`
|
|
86
|
+
const m = linePrefix.match(/(\w+)\s*:\s*['"]?[^,{}]*$/)
|
|
87
|
+
return m ? m[1] : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Try to find the `tag` property in the current element scope */
|
|
91
|
+
function findTagInScope(text: string, offset: number): string | null {
|
|
92
|
+
// Find the enclosing `{` at element level
|
|
93
|
+
let depth = 0
|
|
94
|
+
let braceStart = -1
|
|
95
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
96
|
+
const ch = text[i]
|
|
97
|
+
if (ch === '}') depth++
|
|
98
|
+
else if (ch === '{') {
|
|
99
|
+
if (depth === 0) { braceStart = i; break }
|
|
100
|
+
depth--
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (braceStart === -1) return null
|
|
104
|
+
|
|
105
|
+
// Search within this scope for tag: "..."
|
|
106
|
+
const scope = text.substring(braceStart, Math.min(offset + 500, text.length))
|
|
107
|
+
const tagMatch = scope.match(/tag\s*:\s*['"](\w+)['"]/)
|
|
108
|
+
return tagMatch ? tagMatch[1] : null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Check if we're inside el.call("...") */
|
|
112
|
+
function isInsideCallArgs(linePrefix: string): boolean {
|
|
113
|
+
return /\.call\(\s*['"][^'"]*$/.test(linePrefix)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Check if cursor is inside a string value (after an opening quote with no closing quote) */
|
|
117
|
+
function isInsideStringValue(linePrefix: string): boolean {
|
|
118
|
+
// Count unescaped quotes — odd count means we're inside a string
|
|
119
|
+
const singleQuotes = (linePrefix.match(/(?<![\\])'/g) || []).length
|
|
120
|
+
const doubleQuotes = (linePrefix.match(/(?<![\\])"/g) || []).length
|
|
121
|
+
return singleQuotes % 2 === 1 || doubleQuotes % 2 === 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Extract the property name when cursor is inside a string: `propName: 'cursor|` */
|
|
125
|
+
function getPropertyForStringValue(linePrefix: string): string | null {
|
|
126
|
+
// Match: word : (optional space) quote ... cursor (no closing quote)
|
|
127
|
+
const m = linePrefix.match(/(\w+)\s*:\s*['"][^'"]*$/)
|
|
128
|
+
return m ? m[1] : null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function detectContext(
|
|
132
|
+
document: vscode.TextDocument,
|
|
133
|
+
position: vscode.Position
|
|
134
|
+
): ContextInfo {
|
|
135
|
+
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
|
136
|
+
|
|
137
|
+
// el.call("...") argument
|
|
138
|
+
if (isInsideCallArgs(linePrefix)) return { type: 'call-arg' }
|
|
139
|
+
|
|
140
|
+
// Method access: el. or state.
|
|
141
|
+
if (/\bel\.\s*$/.test(linePrefix)) return { type: 'el-method' }
|
|
142
|
+
if (/\bstate\.\s*$/.test(linePrefix)) return { type: 'state-method' }
|
|
143
|
+
|
|
144
|
+
const fullText = document.getText()
|
|
145
|
+
const config = vscode.workspace.getConfiguration('symbolsApp')
|
|
146
|
+
if (!isDomqlFile(fullText, config.get('detectByImports', true))) return { type: 'none' }
|
|
147
|
+
|
|
148
|
+
const offset = document.offsetAt(position)
|
|
149
|
+
const enclosingKey = findEnclosingKey(fullText, offset)
|
|
150
|
+
const tag = findTagInScope(fullText, offset) ?? undefined
|
|
151
|
+
|
|
152
|
+
// Inside a string value — provide string-level completions
|
|
153
|
+
if (isInsideStringValue(linePrefix)) {
|
|
154
|
+
const prop = getPropertyForStringValue(linePrefix)
|
|
155
|
+
if (prop) {
|
|
156
|
+
if (enclosingKey === 'attr') {
|
|
157
|
+
return { type: 'attr-value', propertyName: prop, enclosingTag: tag, inString: true }
|
|
158
|
+
}
|
|
159
|
+
return { type: 'element-value', propertyName: prop, enclosingKey: enclosingKey ?? undefined, enclosingTag: tag, inString: true }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if we're in value position (after colon)
|
|
164
|
+
const atKeyPos = isAtKeyPosition(linePrefix)
|
|
165
|
+
const propertyName = !atKeyPos ? getPropertyNameBeforeColon(linePrefix) : null
|
|
166
|
+
|
|
167
|
+
if (enclosingKey === 'attr') {
|
|
168
|
+
if (atKeyPos) return { type: 'attr-key', enclosingTag: tag }
|
|
169
|
+
return { type: 'attr-value', propertyName: propertyName ?? undefined, enclosingTag: tag }
|
|
170
|
+
}
|
|
171
|
+
if (enclosingKey === 'state') return { type: 'state-key' }
|
|
172
|
+
if (enclosingKey === 'on') return { type: 'on-key' }
|
|
173
|
+
if (enclosingKey === 'define') return { type: 'define-key' }
|
|
174
|
+
|
|
175
|
+
if (!atKeyPos && propertyName) {
|
|
176
|
+
return { type: 'element-value', propertyName, enclosingKey: enclosingKey ?? undefined, enclosingTag: tag }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (atKeyPos) return { type: 'element-key' }
|
|
180
|
+
return { type: 'none' }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Completion item builders
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
function mkItem(
|
|
188
|
+
label: string,
|
|
189
|
+
kind: vscode.CompletionItemKind,
|
|
190
|
+
detail: string,
|
|
191
|
+
docs: string,
|
|
192
|
+
snippet?: string,
|
|
193
|
+
sort = '5'
|
|
194
|
+
): vscode.CompletionItem {
|
|
195
|
+
const item = new vscode.CompletionItem(label, kind)
|
|
196
|
+
item.detail = detail
|
|
197
|
+
const md = new vscode.MarkdownString(docs)
|
|
198
|
+
md.isTrusted = true
|
|
199
|
+
item.documentation = md
|
|
200
|
+
if (snippet) item.insertText = new vscode.SnippetString(snippet)
|
|
201
|
+
item.sortText = sort + label
|
|
202
|
+
return item
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function mkValueItem(
|
|
206
|
+
label: string,
|
|
207
|
+
detail: string,
|
|
208
|
+
docs?: string,
|
|
209
|
+
sort = '1',
|
|
210
|
+
inString = false
|
|
211
|
+
): vscode.CompletionItem {
|
|
212
|
+
const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Value)
|
|
213
|
+
item.detail = detail
|
|
214
|
+
if (docs) {
|
|
215
|
+
const md = new vscode.MarkdownString(docs)
|
|
216
|
+
md.isTrusted = true
|
|
217
|
+
item.documentation = md
|
|
218
|
+
}
|
|
219
|
+
item.sortText = sort + label
|
|
220
|
+
// Ensure items show up inside strings
|
|
221
|
+
item.filterText = label
|
|
222
|
+
item.range = undefined
|
|
223
|
+
// Design system values are always strings — wrap in quotes when not already inside one
|
|
224
|
+
if (!inString) {
|
|
225
|
+
item.insertText = new vscode.SnippetString(`'${label}'`)
|
|
226
|
+
}
|
|
227
|
+
return item
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getElementKeyCompletions(): vscode.CompletionItem[] {
|
|
231
|
+
const config = vscode.workspace.getConfiguration('symbolsApp')
|
|
232
|
+
const items: vscode.CompletionItem[] = []
|
|
233
|
+
|
|
234
|
+
// 1 - DOMQL built-in keys
|
|
235
|
+
for (const k of DOMQL_ALL_KEYS) {
|
|
236
|
+
items.push(mkItem(k.label, vscode.CompletionItemKind.Property, k.detail, k.documentation, k.snippet, '1'))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2 - Lifecycle events
|
|
240
|
+
for (const ev of DOMQL_LIFECYCLE_EVENTS) {
|
|
241
|
+
items.push(mkItem(ev.label, vscode.CompletionItemKind.Event, ev.detail, ev.documentation, ev.snippet, '2'))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 3 - DOM events
|
|
245
|
+
for (const ev of DOM_EVENTS) {
|
|
246
|
+
items.push(mkItem(ev.label, vscode.CompletionItemKind.Event, ev.detail, ev.documentation, ev.snippet, '3'))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 4 - Built-in components / atoms (PascalCase children)
|
|
250
|
+
for (const c of ALL_COMPONENTS) {
|
|
251
|
+
items.push(mkItem(c.label, vscode.CompletionItemKind.Class, c.detail, c.documentation, c.snippet, '4'))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 5 - CSS properties
|
|
255
|
+
if (config.get('completeCssProps', true)) {
|
|
256
|
+
for (const p of ALL_CSS_PROPS) {
|
|
257
|
+
items.push(mkItem(p.label, vscode.CompletionItemKind.Property, p.detail, p.documentation ?? '', undefined, '5'))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return items
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getAttrKeyCompletions(tag?: string): vscode.CompletionItem[] {
|
|
265
|
+
return HTML_ATTRIBUTES.map(attr => {
|
|
266
|
+
const item = new vscode.CompletionItem(attr, vscode.CompletionItemKind.Property)
|
|
267
|
+
item.detail = `HTML attribute: ${attr}`
|
|
268
|
+
const needsQuotes = attr.includes('-')
|
|
269
|
+
item.insertText = new vscode.SnippetString(
|
|
270
|
+
needsQuotes ? `"${attr}": \${1:},` : `${attr}: \${1:},`
|
|
271
|
+
)
|
|
272
|
+
return item
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getOnKeyCompletions(): vscode.CompletionItem[] {
|
|
277
|
+
return [...DOM_EVENTS, ...DOMQL_LIFECYCLE_EVENTS].map(ev => {
|
|
278
|
+
const raw = ev.label.charAt(2).toLowerCase() + ev.label.slice(3)
|
|
279
|
+
const item = new vscode.CompletionItem(raw, vscode.CompletionItemKind.Event)
|
|
280
|
+
item.detail = `on.${raw} (v2 — prefer top-level ${ev.label})`
|
|
281
|
+
const md = new vscode.MarkdownString(`**v2 style** — prefer \`${ev.label}\` in v3.\n\n${ev.documentation}`)
|
|
282
|
+
md.isTrusted = true
|
|
283
|
+
item.documentation = md
|
|
284
|
+
const sig = ev.isDomqlLifecycle ? `${raw}: (el, state) => {\n \${1:}\n},` : `${raw}: (event, el, state) => {\n \${1:}\n},`
|
|
285
|
+
item.insertText = new vscode.SnippetString(sig)
|
|
286
|
+
return item
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getElementMethodCompletions(): vscode.CompletionItem[] {
|
|
291
|
+
return ELEMENT_METHODS.map(m => {
|
|
292
|
+
const item = new vscode.CompletionItem(m.label, vscode.CompletionItemKind.Method)
|
|
293
|
+
item.detail = m.detail
|
|
294
|
+
const md = new vscode.MarkdownString(m.documentation)
|
|
295
|
+
md.isTrusted = true
|
|
296
|
+
item.documentation = md
|
|
297
|
+
item.insertText = new vscode.SnippetString(m.snippet)
|
|
298
|
+
return item
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getStateMethodCompletions(): vscode.CompletionItem[] {
|
|
303
|
+
return STATE_METHODS.map(m => {
|
|
304
|
+
const item = new vscode.CompletionItem(m.label, vscode.CompletionItemKind.Method)
|
|
305
|
+
item.detail = m.detail
|
|
306
|
+
const md = new vscode.MarkdownString(m.documentation)
|
|
307
|
+
md.isTrusted = true
|
|
308
|
+
item.documentation = md
|
|
309
|
+
item.insertText = new vscode.SnippetString(m.snippet)
|
|
310
|
+
return item
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getStateKeyCompletions(): vscode.CompletionItem[] {
|
|
315
|
+
const common: [string, string, string][] = [
|
|
316
|
+
['loading', 'false', 'Loading flag'],
|
|
317
|
+
['error', 'null', 'Error message or null'],
|
|
318
|
+
['data', 'null', 'Fetched data'],
|
|
319
|
+
['open', 'false', 'Open/closed toggle'],
|
|
320
|
+
['active', 'null', 'Currently active item key'],
|
|
321
|
+
['selected', 'null', 'Currently selected value'],
|
|
322
|
+
['count', '0', 'Numeric counter'],
|
|
323
|
+
['items', '[]', 'Array of items'],
|
|
324
|
+
['value', "''", 'Input value'],
|
|
325
|
+
]
|
|
326
|
+
return common.map(([key, def, desc]) => {
|
|
327
|
+
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Property)
|
|
328
|
+
item.detail = `${key}: ${def}`
|
|
329
|
+
item.documentation = new vscode.MarkdownString(desc)
|
|
330
|
+
item.insertText = new vscode.SnippetString(`${key}: \${1:${def}},`)
|
|
331
|
+
return item
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Value completions — the smart part
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
function getColorCompletions(inString = false): vscode.CompletionItem[] {
|
|
340
|
+
const items: vscode.CompletionItem[] = []
|
|
341
|
+
|
|
342
|
+
for (const c of COLOR_TOKEN_MAP) {
|
|
343
|
+
const desc = c.description || ''
|
|
344
|
+
const detail = c.hex ? `${c.hex}` : (c.description || 'Color token')
|
|
345
|
+
const docs = c.hex
|
|
346
|
+
? `\`${c.label}\` → \`${c.hex}\`\n\nModifiers: \`${c.label}.5\` (opacity), \`${c.label}+16\` (lighten), \`${c.label}-16\` (darken), \`${c.label}=50\` (set lightness)`
|
|
347
|
+
: `${desc}\n\nModifiers: \`${c.label}.5\` (opacity)`
|
|
348
|
+
items.push(mkValueItem(c.label, detail, docs, '1', inString))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const g of GRADIENT_TOKENS) {
|
|
352
|
+
items.push(mkValueItem(g, `Gradient: ${g}`, 'Design system gradient token', '2', inString))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return items
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getSpacingCompletions(inString = false): vscode.CompletionItem[] {
|
|
359
|
+
const cfg = SEQUENCE_CONFIGS.spacing
|
|
360
|
+
return SPACING_TOKENS.map((token, i) => {
|
|
361
|
+
const sort = String(i).padStart(2, '0')
|
|
362
|
+
return mkValueItem(token.label, `${token.label} → ${token.approxValue}`, `**Spacing** \`${token.label}\` ≈ **${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\nSub-steps: A1, A2 interpolate between A and B\n\nOperations: \`A+B\`, \`A-Z\`, \`A*2\`, \`-A\` (negative)`, sort, inString)
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getFontSizeCompletions(inString = false): vscode.CompletionItem[] {
|
|
367
|
+
const cfg = SEQUENCE_CONFIGS.typography
|
|
368
|
+
return TYPOGRAPHY_TOKENS.map((token, i) => {
|
|
369
|
+
const sort = String(i).padStart(2, '0')
|
|
370
|
+
return mkValueItem(token.label, `${token.label} → ${token.approxValue}`, `**Typography** \`${token.label}\` ≈ **${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`, sort, inString)
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function getThemeCompletions(inString = false): vscode.CompletionItem[] {
|
|
375
|
+
const items: vscode.CompletionItem[] = []
|
|
376
|
+
|
|
377
|
+
for (const t of THEME_TOKENS) {
|
|
378
|
+
items.push(mkValueItem(t, `Theme: ${t}`, `Apply design system theme.\n\nModifiers: \`"${t} .child"\`, \`"${t} .color-only"\``, '1', inString))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Theme with modifier combinations
|
|
382
|
+
for (const t of ['primary', 'secondary', 'card', 'dialog', 'label']) {
|
|
383
|
+
for (const mod of THEME_MODIFIERS) {
|
|
384
|
+
items.push(mkValueItem(`${t} ${mod}`, `Theme modifier: ${t} ${mod}`, `Theme \`${t}\` with modifier \`${mod}\``, '3', inString))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return items
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getIconCompletions(inString = false): vscode.CompletionItem[] {
|
|
392
|
+
return ICON_NAMES.map(name =>
|
|
393
|
+
mkValueItem(name, `Icon: ${name}`, `Default icon from design system`, '1', inString)
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getExtendsCompletions(workspaceComponents: string[], inString = false): vscode.CompletionItem[] {
|
|
398
|
+
const items: vscode.CompletionItem[] = []
|
|
399
|
+
|
|
400
|
+
// Built-in components first
|
|
401
|
+
for (const c of ALL_COMPONENTS) {
|
|
402
|
+
items.push(mkValueItem(c.label, c.detail, c.documentation, '1', inString))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Workspace components
|
|
406
|
+
for (const name of workspaceComponents) {
|
|
407
|
+
// Skip if already in built-in
|
|
408
|
+
if (ALL_COMPONENTS.some(c => c.label === name)) continue
|
|
409
|
+
items.push(mkValueItem(name, `Project component: ${name}`, 'Detected from workspace', '2', inString))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return items
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getTagCompletions(inString = false): vscode.CompletionItem[] {
|
|
416
|
+
return HTML_TAGS.map(tag =>
|
|
417
|
+
mkValueItem(tag, `HTML tag: <${tag}>`, undefined, '1', inString)
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getCssEnumCompletions(property: string, inString = false): vscode.CompletionItem[] {
|
|
422
|
+
const values = CSS_VALUE_ENUMS[property]
|
|
423
|
+
if (!values) return []
|
|
424
|
+
return values.map(v => mkValueItem(v, `${property}: ${v}`, undefined, '1', inString))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getAttrValueCompletions(attrName: string): vscode.CompletionItem[] {
|
|
428
|
+
switch (attrName) {
|
|
429
|
+
case 'type':
|
|
430
|
+
return INPUT_TYPES.map(t => mkValueItem(t, `type="${t}"`, undefined, '1'))
|
|
431
|
+
case 'target':
|
|
432
|
+
return TARGET_VALUES.map(t => mkValueItem(t, `target="${t}"`, undefined, '1'))
|
|
433
|
+
case 'rel':
|
|
434
|
+
return REL_VALUES.map(r => mkValueItem(r, `rel="${r}"`, undefined, '1'))
|
|
435
|
+
case 'autocomplete':
|
|
436
|
+
return AUTOCOMPLETE_VALUES.map(a => mkValueItem(a, `autocomplete="${a}"`, undefined, '1'))
|
|
437
|
+
case 'loading':
|
|
438
|
+
return LOADING_VALUES.map(l => mkValueItem(l, `loading="${l}"`, undefined, '1'))
|
|
439
|
+
case 'disabled': case 'checked': case 'required': case 'readonly':
|
|
440
|
+
case 'multiple': case 'hidden': case 'draggable': case 'contenteditable':
|
|
441
|
+
case 'spellcheck': case 'novalidate': case 'autofocus':
|
|
442
|
+
return BOOLEAN_VALUES.map(b => mkValueItem(b, `${attrName}="${b}"`, undefined, '1'))
|
|
443
|
+
case 'role':
|
|
444
|
+
// Common ARIA roles
|
|
445
|
+
return ['button', 'link', 'dialog', 'alert', 'navigation', 'menu', 'menuitem',
|
|
446
|
+
'tab', 'tablist', 'tabpanel', 'checkbox', 'radio', 'listbox', 'option',
|
|
447
|
+
'textbox', 'search', 'progressbar', 'slider', 'switch', 'tooltip', 'img',
|
|
448
|
+
'heading', 'list', 'listitem', 'group', 'region', 'banner', 'main',
|
|
449
|
+
'complementary', 'contentinfo', 'form', 'presentation', 'none'
|
|
450
|
+
].map(r => mkValueItem(r, `role="${r}"`, undefined, '1'))
|
|
451
|
+
case 'dir':
|
|
452
|
+
return ['ltr', 'rtl', 'auto'].map(d => mkValueItem(d, `dir="${d}"`, undefined, '1'))
|
|
453
|
+
case 'method':
|
|
454
|
+
return ['get', 'post', 'put', 'delete', 'patch'].map(m => mkValueItem(m, `method="${m}"`, undefined, '1'))
|
|
455
|
+
case 'enctype':
|
|
456
|
+
return ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']
|
|
457
|
+
.map(e => mkValueItem(e, `enctype="${e}"`, undefined, '1'))
|
|
458
|
+
default:
|
|
459
|
+
return []
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getCallArgCompletions(): vscode.CompletionItem[] {
|
|
464
|
+
// Common context function names
|
|
465
|
+
const fns = [
|
|
466
|
+
['exec', 'Execute a dynamic prop value'],
|
|
467
|
+
['fetchData', 'Fetch data from API'],
|
|
468
|
+
['router', 'Navigate to a route'],
|
|
469
|
+
['isString', 'Check if value is string'],
|
|
470
|
+
['isObject', 'Check if value is object'],
|
|
471
|
+
['isArray', 'Check if value is array'],
|
|
472
|
+
['isNumber', 'Check if value is number'],
|
|
473
|
+
['isFunction', 'Check if value is function'],
|
|
474
|
+
['isBoolean', 'Check if value is boolean'],
|
|
475
|
+
['isDefined', 'Check if value is defined'],
|
|
476
|
+
['isUndefined', 'Check if value is undefined'],
|
|
477
|
+
['isNull', 'Check if value is null'],
|
|
478
|
+
['isEmpty', 'Check if value is empty'],
|
|
479
|
+
['deepMerge', 'Deep merge objects'],
|
|
480
|
+
['deepClone', 'Deep clone an object'],
|
|
481
|
+
['getSystemTheme', 'Get current system color scheme'],
|
|
482
|
+
['setTheme', 'Set application theme'],
|
|
483
|
+
]
|
|
484
|
+
return fns.map(([name, desc]) =>
|
|
485
|
+
mkValueItem(name, desc, `Context function: \`el.call("${name}", ...args)\`\n\nResolution: utils → functions → methods → snippets`, '1')
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function getValueCompletions(ctx: ContextInfo): Promise<vscode.CompletionItem[]> {
|
|
490
|
+
const prop = ctx.propertyName
|
|
491
|
+
if (!prop) return []
|
|
492
|
+
const inStr = ctx.inString ?? false
|
|
493
|
+
|
|
494
|
+
// extends / childExtends / childExtendsRecursive → component names
|
|
495
|
+
if (prop === 'extends' || prop === 'childExtends' || prop === 'childExtendsRecursive' || prop === 'childExtend') {
|
|
496
|
+
const wsComponents = await scanWorkspaceComponents()
|
|
497
|
+
return getExtendsCompletions(wsComponents, inStr)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// tag → HTML tags
|
|
501
|
+
if (prop === 'tag') return getTagCompletions(inStr)
|
|
502
|
+
|
|
503
|
+
// theme → theme tokens
|
|
504
|
+
if (prop === 'theme') return getThemeCompletions(inStr)
|
|
505
|
+
|
|
506
|
+
// icon / name (inside Icon component) → icon names
|
|
507
|
+
if (prop === 'icon' || prop === 'name') return getIconCompletions(inStr)
|
|
508
|
+
|
|
509
|
+
// Color properties → color tokens
|
|
510
|
+
if (COLOR_PROPERTIES.has(prop)) return getColorCompletions(inStr)
|
|
511
|
+
|
|
512
|
+
// Spacing/size properties → spacing tokens
|
|
513
|
+
if (SPACING_PROPERTIES.has(prop)) return getSpacingCompletions(inStr)
|
|
514
|
+
|
|
515
|
+
// Font size properties → font size tokens
|
|
516
|
+
if (FONT_SIZE_PROPERTIES.has(prop)) return getFontSizeCompletions(inStr)
|
|
517
|
+
|
|
518
|
+
// CSS enum values (display, position, etc.)
|
|
519
|
+
const enumItems = getCssEnumCompletions(prop, inStr)
|
|
520
|
+
if (enumItems.length > 0) return enumItems
|
|
521
|
+
|
|
522
|
+
// Timing properties → timing tokens
|
|
523
|
+
if (prop === 'transition' || prop === 'transitionDuration' || prop === 'animationDuration') {
|
|
524
|
+
const cfg = SEQUENCE_CONFIGS.timing
|
|
525
|
+
const items = TIMING_TOKENS.map((t, i) => {
|
|
526
|
+
const sort = String(i).padStart(2, '0')
|
|
527
|
+
return mkValueItem(t.label, `${t.label} → ${t.approxValue}`, `**Timing** \`${t.label}\` ≈ **${t.approxValue}**\n\nBase: A = ${cfg.base}ms, ratio: ${cfg.ratio} (perfect fourth)`, sort, inStr)
|
|
528
|
+
})
|
|
529
|
+
if (prop === 'transition') {
|
|
530
|
+
items.push(mkValueItem('A defaultBezier', 'transition: A defaultBezier', 'Common transition with default easing', '99', inStr))
|
|
531
|
+
}
|
|
532
|
+
return items
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return []
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
// Provider
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
export class DomqlCompletionProvider implements vscode.CompletionItemProvider {
|
|
543
|
+
async provideCompletionItems(
|
|
544
|
+
document: vscode.TextDocument,
|
|
545
|
+
position: vscode.Position
|
|
546
|
+
): Promise<vscode.CompletionList | vscode.CompletionItem[]> {
|
|
547
|
+
if (!vscode.workspace.getConfiguration('symbolsApp').get('enable', true)) return []
|
|
548
|
+
|
|
549
|
+
const ctx = detectContext(document, position)
|
|
550
|
+
let items: vscode.CompletionItem[]
|
|
551
|
+
|
|
552
|
+
switch (ctx.type) {
|
|
553
|
+
case 'el-method': items = getElementMethodCompletions(); break
|
|
554
|
+
case 'state-method': items = getStateMethodCompletions(); break
|
|
555
|
+
case 'call-arg': items = getCallArgCompletions(); break
|
|
556
|
+
case 'attr-key': items = getAttrKeyCompletions(ctx.enclosingTag); break
|
|
557
|
+
case 'attr-value': items = ctx.propertyName ? getAttrValueCompletions(ctx.propertyName) : []; break
|
|
558
|
+
case 'on-key': items = getOnKeyCompletions(); break
|
|
559
|
+
case 'state-key': items = getStateKeyCompletions(); break
|
|
560
|
+
case 'define-key':
|
|
561
|
+
items = [mkItem(
|
|
562
|
+
'propName',
|
|
563
|
+
vscode.CompletionItemKind.Property,
|
|
564
|
+
'(param, el, state, context) => void',
|
|
565
|
+
'Custom property transformer — runs when this key appears on any element.',
|
|
566
|
+
'propName: (param, el, state) => {\n ${1:}\n},',
|
|
567
|
+
'1'
|
|
568
|
+
)]
|
|
569
|
+
break
|
|
570
|
+
case 'element-value': items = await getValueCompletions(ctx); break
|
|
571
|
+
case 'element-key': {
|
|
572
|
+
items = getElementKeyCompletions()
|
|
573
|
+
const wsComponents = await scanWorkspaceComponents()
|
|
574
|
+
for (const name of wsComponents) {
|
|
575
|
+
if (ALL_COMPONENTS.some(c => c.label === name)) continue
|
|
576
|
+
items.push(mkItem(
|
|
577
|
+
name,
|
|
578
|
+
vscode.CompletionItemKind.Class,
|
|
579
|
+
`Project component: ${name}`,
|
|
580
|
+
'Detected from workspace files',
|
|
581
|
+
`${name}: {\n \${1:}\n},`,
|
|
582
|
+
'4'
|
|
583
|
+
))
|
|
584
|
+
}
|
|
585
|
+
break
|
|
586
|
+
}
|
|
587
|
+
default: return []
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (items.length === 0) return []
|
|
591
|
+
|
|
592
|
+
// Use CompletionList with isIncomplete so VSCode shows items inside strings
|
|
593
|
+
return new vscode.CompletionList(items, true)
|
|
594
|
+
}
|
|
595
|
+
}
|