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