tjs-lang 0.2.0

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.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,1447 @@
1
+ /**
2
+ * CodeMirror 6 Language Support for AsyncJS
3
+ *
4
+ * This extends the JavaScript language with custom highlighting for AsyncJS:
5
+ * - Forbidden keywords (new, class, async, etc.) are marked as errors
6
+ * - Standard JS syntax highlighting otherwise
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * import { EditorState } from '@codemirror/state'
11
+ * import { EditorView, basicSetup } from 'codemirror'
12
+ * import { ajs } from 'tjs-lang/editors/codemirror/ajs-language'
13
+ *
14
+ * new EditorView({
15
+ * state: EditorState.create({
16
+ * doc: 'function agent(topic: "string") { ... }',
17
+ * extensions: [basicSetup, ajs()]
18
+ * }),
19
+ * parent: document.body
20
+ * })
21
+ * ```
22
+ */
23
+
24
+ import { javascript } from '@codemirror/lang-javascript'
25
+ import {
26
+ HighlightStyle,
27
+ syntaxHighlighting,
28
+ LanguageSupport,
29
+ defaultHighlightStyle,
30
+ } from '@codemirror/language'
31
+ import { tags } from '@lezer/highlight'
32
+ import {
33
+ EditorView,
34
+ Decoration,
35
+ DecorationSet,
36
+ ViewPlugin,
37
+ ViewUpdate,
38
+ } from '@codemirror/view'
39
+ import { Extension, RangeSetBuilder } from '@codemirror/state'
40
+ import {
41
+ autocompletion,
42
+ CompletionContext as CMCompletionContext,
43
+ CompletionResult,
44
+ Completion as CMCompletion,
45
+ snippetCompletion,
46
+ } from '@codemirror/autocomplete'
47
+
48
+ import {
49
+ FORBIDDEN_KEYWORDS as FORBIDDEN_LIST,
50
+ FORBIDDEN_PATTERN,
51
+ } from '../ajs-syntax'
52
+ import { FORBIDDEN_KEYWORDS as TJS_FORBIDDEN_LIST } from '../tjs-syntax'
53
+
54
+ /**
55
+ * Forbidden keywords in AsyncJS - these will be highlighted as errors
56
+ */
57
+ const FORBIDDEN_KEYWORDS = new Set(FORBIDDEN_LIST)
58
+
59
+ /**
60
+ * Forbidden keywords in TJS - fewer restrictions than AJS
61
+ */
62
+ const TJS_FORBIDDEN_KEYWORDS = new Set(TJS_FORBIDDEN_LIST)
63
+
64
+ /**
65
+ * Decoration for forbidden keywords
66
+ */
67
+ const forbiddenMark = Decoration.mark({
68
+ class: 'cm-ajs-forbidden',
69
+ })
70
+
71
+ /**
72
+ * Decoration for TJS special syntax (try-without-catch, etc.)
73
+ */
74
+ const tjsSpecialMark = Decoration.mark({
75
+ class: 'cm-tjs-special',
76
+ })
77
+
78
+ /**
79
+ * Find all string and comment regions in the document
80
+ * Returns array of [start, end] ranges to skip
81
+ */
82
+ function findSkipRegions(doc: string): [number, number][] {
83
+ const regions: [number, number][] = []
84
+ const len = doc.length
85
+ let i = 0
86
+
87
+ while (i < len) {
88
+ const ch = doc[i]
89
+ const next = doc[i + 1]
90
+
91
+ // Single-line comment
92
+ if (ch === '/' && next === '/') {
93
+ const start = i
94
+ i += 2
95
+ while (i < len && doc[i] !== '\n') i++
96
+ regions.push([start, i])
97
+ continue
98
+ }
99
+
100
+ // Multi-line comment
101
+ if (ch === '/' && next === '*') {
102
+ const start = i
103
+ i += 2
104
+ while (i < len - 1 && !(doc[i] === '*' && doc[i + 1] === '/')) i++
105
+ i += 2 // skip */
106
+ regions.push([start, i])
107
+ continue
108
+ }
109
+
110
+ // Template literal - skip string parts but NOT ${...} expressions
111
+ if (ch === '`') {
112
+ let stringStart = i
113
+ i++
114
+ while (i < len) {
115
+ if (doc[i] === '\\') {
116
+ i += 2 // skip escaped char
117
+ continue
118
+ }
119
+ if (doc[i] === '`') {
120
+ // End of template - add final string region
121
+ regions.push([stringStart, i + 1])
122
+ i++
123
+ break
124
+ }
125
+ if (doc[i] === '$' && doc[i + 1] === '{') {
126
+ // Add string region before ${
127
+ regions.push([stringStart, i])
128
+ i += 2 // skip ${
129
+ // Skip the expression inside ${...} (don't add to regions - it's code!)
130
+ let braceDepth = 1
131
+ while (i < len && braceDepth > 0) {
132
+ if (doc[i] === '{') braceDepth++
133
+ else if (doc[i] === '}') braceDepth--
134
+ if (braceDepth > 0) i++
135
+ }
136
+ i++ // skip closing }
137
+ stringStart = i // next string region starts here
138
+ continue
139
+ }
140
+ i++
141
+ }
142
+ continue
143
+ }
144
+
145
+ // Single or double quoted string
146
+ if (ch === '"' || ch === "'") {
147
+ const quote = ch
148
+ const start = i
149
+ i++
150
+ while (i < len) {
151
+ if (doc[i] === '\\') {
152
+ i += 2 // skip escaped char
153
+ continue
154
+ }
155
+ if (doc[i] === quote) {
156
+ i++
157
+ break
158
+ }
159
+ if (doc[i] === '\n') break // unterminated string
160
+ i++
161
+ }
162
+ regions.push([start, i])
163
+ continue
164
+ }
165
+
166
+ i++
167
+ }
168
+
169
+ return regions
170
+ }
171
+
172
+ /**
173
+ * Check if a position is inside any skip region
174
+ */
175
+ function isInSkipRegion(pos: number, regions: [number, number][]): boolean {
176
+ for (const [start, end] of regions) {
177
+ if (pos >= start && pos < end) return true
178
+ if (start > pos) break // regions are sorted, no need to check further
179
+ }
180
+ return false
181
+ }
182
+
183
+ /**
184
+ * Create a plugin that highlights forbidden keywords as errors
185
+ * (but not inside strings or comments)
186
+ */
187
+ function createForbiddenHighlighter(forbiddenSet: Set<string>) {
188
+ const pattern = new RegExp(`\\b(${[...forbiddenSet].join('|')})\\b`, 'g')
189
+
190
+ return ViewPlugin.fromClass(
191
+ class {
192
+ decorations: DecorationSet
193
+
194
+ constructor(view: EditorView) {
195
+ this.decorations = this.buildDecorations(view)
196
+ }
197
+
198
+ update(update: ViewUpdate) {
199
+ if (update.docChanged || update.viewportChanged) {
200
+ this.decorations = this.buildDecorations(update.view)
201
+ }
202
+ }
203
+
204
+ buildDecorations(view: EditorView): DecorationSet {
205
+ const builder = new RangeSetBuilder<Decoration>()
206
+ const doc = view.state.doc.toString()
207
+ const skipRegions = findSkipRegions(doc)
208
+
209
+ // Use fresh regex for each scan
210
+ const regex = new RegExp(pattern.source, 'g')
211
+
212
+ let match
213
+ while ((match = regex.exec(doc)) !== null) {
214
+ // Skip if inside string or comment
215
+ if (!isInSkipRegion(match.index, skipRegions)) {
216
+ builder.add(
217
+ match.index,
218
+ match.index + match[0].length,
219
+ forbiddenMark
220
+ )
221
+ }
222
+ }
223
+
224
+ return builder.finish()
225
+ }
226
+ },
227
+ {
228
+ decorations: (v) => v.decorations,
229
+ }
230
+ )
231
+ }
232
+
233
+ // AJS forbidden highlighter (stricter)
234
+ const forbiddenHighlighter = createForbiddenHighlighter(FORBIDDEN_KEYWORDS)
235
+
236
+ // TJS forbidden highlighter (more permissive - allows import/export, async/await, throw)
237
+ const tjsForbiddenHighlighter = createForbiddenHighlighter(
238
+ TJS_FORBIDDEN_KEYWORDS
239
+ )
240
+
241
+ /**
242
+ * Plugin that highlights try-without-catch as TJS special syntax
243
+ * (monadic error handling - returns error instead of throwing)
244
+ */
245
+ const tryWithoutCatchHighlighter = ViewPlugin.fromClass(
246
+ class {
247
+ decorations: DecorationSet
248
+
249
+ constructor(view: EditorView) {
250
+ this.decorations = this.buildDecorations(view)
251
+ }
252
+
253
+ update(update: ViewUpdate) {
254
+ if (update.docChanged || update.viewportChanged) {
255
+ this.decorations = this.buildDecorations(update.view)
256
+ }
257
+ }
258
+
259
+ buildDecorations(view: EditorView): DecorationSet {
260
+ const builder = new RangeSetBuilder<Decoration>()
261
+ const doc = view.state.doc.toString()
262
+ const skipRegions = findSkipRegions(doc)
263
+
264
+ // Find try blocks without catch or finally
265
+ // Pattern: try { ... } NOT followed by catch or finally
266
+ const tryPattern = /\btry\s*\{/g
267
+ let match
268
+
269
+ while ((match = tryPattern.exec(doc)) !== null) {
270
+ // Skip if inside string or comment
271
+ if (isInSkipRegion(match.index, skipRegions)) continue
272
+
273
+ // Find the matching closing brace
274
+ const braceStart = match.index + match[0].length - 1
275
+ let depth = 1
276
+ let j = braceStart + 1
277
+
278
+ while (j < doc.length && depth > 0) {
279
+ const char = doc[j]
280
+ if (char === '{') depth++
281
+ else if (char === '}') depth--
282
+ j++
283
+ }
284
+
285
+ if (depth !== 0) continue // Unbalanced
286
+
287
+ // Check what comes after the closing brace
288
+ const afterTry = doc.slice(j).match(/^\s*(catch|finally)\b/)
289
+
290
+ if (!afterTry) {
291
+ // No catch or finally - this is TJS monadic try
292
+ // Highlight just the 'try' keyword
293
+ const tryKeywordEnd = match.index + 3 // 'try'.length
294
+ builder.add(match.index, tryKeywordEnd, tjsSpecialMark)
295
+ }
296
+ }
297
+
298
+ return builder.finish()
299
+ }
300
+ },
301
+ {
302
+ decorations: (v) => v.decorations,
303
+ }
304
+ )
305
+
306
+ /**
307
+ * Theme for AsyncJS - styles forbidden keywords as errors
308
+ */
309
+ const ajsTheme = EditorView.theme({
310
+ '.cm-ajs-forbidden': {
311
+ color: '#dc2626',
312
+ textDecoration: 'wavy underline #dc2626',
313
+ backgroundColor: 'rgba(220, 38, 38, 0.1)',
314
+ },
315
+ '.cm-tjs-special': {
316
+ color: '#7c3aed',
317
+ fontWeight: 'bold',
318
+ backgroundColor: 'rgba(124, 58, 237, 0.1)',
319
+ },
320
+ })
321
+
322
+ /**
323
+ * Custom highlight style that could be used for additional AsyncJS-specific highlighting
324
+ */
325
+ const ajsHighlightStyle = HighlightStyle.define([
326
+ // Standard highlighting is inherited from JavaScript
327
+ // Add any AsyncJS-specific overrides here
328
+ ])
329
+
330
+ /**
331
+ * Autocomplete configuration
332
+ */
333
+ export interface AutocompleteConfig {
334
+ /** Function to get __tjs metadata from current source */
335
+ getMetadata?: () => Record<string, any> | undefined
336
+ /** Function to get imported module metadata */
337
+ getImports?: () => Record<string, Record<string, any>> | undefined
338
+ }
339
+
340
+ // TJS keywords with snippets
341
+ const TJS_COMPLETIONS: CMCompletion[] = [
342
+ { label: 'function', type: 'keyword', detail: 'Declare a function' },
343
+ { label: 'const', type: 'keyword', detail: 'Declare a constant' },
344
+ { label: 'let', type: 'keyword', detail: 'Declare a variable' },
345
+ { label: 'if', type: 'keyword', detail: 'Conditional statement' },
346
+ { label: 'else', type: 'keyword', detail: 'Else branch' },
347
+ { label: 'while', type: 'keyword', detail: 'While loop' },
348
+ { label: 'for', type: 'keyword', detail: 'For loop' },
349
+ { label: 'return', type: 'keyword', detail: 'Return from function' },
350
+ { label: 'try', type: 'keyword', detail: 'Try block' },
351
+ { label: 'catch', type: 'keyword', detail: 'Catch block' },
352
+ { label: 'import', type: 'keyword', detail: 'Import module' },
353
+ { label: 'export', type: 'keyword', detail: 'Export declaration' },
354
+ snippetCompletion("test('${description}') {\n\t${}\n}", {
355
+ label: 'test',
356
+ type: 'keyword',
357
+ detail: 'Inline test block',
358
+ }),
359
+ snippetCompletion('mock {\n\t${}\n}', {
360
+ label: 'mock',
361
+ type: 'keyword',
362
+ detail: 'Mock setup block',
363
+ }),
364
+ snippetCompletion('unsafe {\n\t${}\n}', {
365
+ label: 'unsafe',
366
+ type: 'keyword',
367
+ detail: 'Skip type validation',
368
+ }),
369
+ ]
370
+
371
+ // Type examples for after : or ->
372
+ const TJS_TYPES: CMCompletion[] = [
373
+ { label: "''", type: 'type', detail: 'String type' },
374
+ { label: '0', type: 'type', detail: 'Number type' },
375
+ { label: 'true', type: 'type', detail: 'Boolean type' },
376
+ { label: 'null', type: 'type', detail: 'Null type' },
377
+ { label: 'undefined', type: 'type', detail: 'Undefined type' },
378
+ { label: "['']", type: 'type', detail: 'Array of strings' },
379
+ { label: '[0]', type: 'type', detail: 'Array of numbers' },
380
+ { label: '{}', type: 'type', detail: 'Object type' },
381
+ { label: 'any', type: 'type', detail: 'Any type' },
382
+ ]
383
+
384
+ // Runtime functions
385
+ const RUNTIME_COMPLETIONS: CMCompletion[] = [
386
+ snippetCompletion('isError(${value})', {
387
+ label: 'isError',
388
+ type: 'function',
389
+ detail: '(value: any) -> boolean',
390
+ info: 'Check if a value is a TJS error',
391
+ }),
392
+ snippetCompletion("error('${message}')", {
393
+ label: 'error',
394
+ type: 'function',
395
+ detail: '(message: string) -> TJSError',
396
+ info: 'Create a TJS error object',
397
+ }),
398
+ snippetCompletion('typeOf(${value})', {
399
+ label: 'typeOf',
400
+ type: 'function',
401
+ detail: '(value: any) -> string',
402
+ info: 'Get type name (fixed typeof)',
403
+ }),
404
+ snippetCompletion('expect(${actual})', {
405
+ label: 'expect',
406
+ type: 'function',
407
+ detail: '(actual: any) -> Matchers',
408
+ info: 'Test assertion',
409
+ }),
410
+ ]
411
+
412
+ // JavaScript global objects and constructors
413
+ const GLOBAL_COMPLETIONS: CMCompletion[] = [
414
+ // Console
415
+ {
416
+ label: 'console',
417
+ type: 'variable',
418
+ detail: 'Console object',
419
+ info: 'Logging and debugging',
420
+ },
421
+
422
+ // Math
423
+ {
424
+ label: 'Math',
425
+ type: 'variable',
426
+ detail: 'Math object',
427
+ info: 'Mathematical functions and constants',
428
+ },
429
+
430
+ // JSON
431
+ {
432
+ label: 'JSON',
433
+ type: 'variable',
434
+ detail: 'JSON object',
435
+ info: 'JSON parse and stringify',
436
+ },
437
+
438
+ // Constructors / types
439
+ {
440
+ label: 'Array',
441
+ type: 'class',
442
+ detail: 'Array constructor',
443
+ info: 'Create arrays',
444
+ },
445
+ {
446
+ label: 'Object',
447
+ type: 'class',
448
+ detail: 'Object constructor',
449
+ info: 'Object utilities',
450
+ },
451
+ {
452
+ label: 'String',
453
+ type: 'class',
454
+ detail: 'String constructor',
455
+ info: 'String utilities',
456
+ },
457
+ {
458
+ label: 'Number',
459
+ type: 'class',
460
+ detail: 'Number constructor',
461
+ info: 'Number utilities',
462
+ },
463
+ { label: 'Boolean', type: 'class', detail: 'Boolean constructor' },
464
+ {
465
+ label: 'Date',
466
+ type: 'class',
467
+ detail: 'Date constructor',
468
+ info: 'Date and time',
469
+ },
470
+ {
471
+ label: 'RegExp',
472
+ type: 'class',
473
+ detail: 'RegExp constructor',
474
+ info: 'Regular expressions',
475
+ },
476
+ {
477
+ label: 'Map',
478
+ type: 'class',
479
+ detail: 'Map constructor',
480
+ info: 'Key-value collection',
481
+ },
482
+ {
483
+ label: 'Set',
484
+ type: 'class',
485
+ detail: 'Set constructor',
486
+ info: 'Unique value collection',
487
+ },
488
+ { label: 'WeakMap', type: 'class', detail: 'WeakMap constructor' },
489
+ { label: 'WeakSet', type: 'class', detail: 'WeakSet constructor' },
490
+ { label: 'Symbol', type: 'class', detail: 'Symbol constructor' },
491
+ { label: 'BigInt', type: 'class', detail: 'BigInt constructor' },
492
+
493
+ // Error types
494
+ { label: 'Error', type: 'class', detail: 'Error constructor' },
495
+ { label: 'TypeError', type: 'class', detail: 'TypeError constructor' },
496
+ { label: 'RangeError', type: 'class', detail: 'RangeError constructor' },
497
+ { label: 'SyntaxError', type: 'class', detail: 'SyntaxError constructor' },
498
+ {
499
+ label: 'ReferenceError',
500
+ type: 'class',
501
+ detail: 'ReferenceError constructor',
502
+ },
503
+
504
+ // Typed arrays
505
+ { label: 'ArrayBuffer', type: 'class', detail: 'ArrayBuffer constructor' },
506
+ { label: 'Uint8Array', type: 'class', detail: 'Uint8Array constructor' },
507
+ { label: 'Int8Array', type: 'class', detail: 'Int8Array constructor' },
508
+ { label: 'Uint16Array', type: 'class', detail: 'Uint16Array constructor' },
509
+ { label: 'Int16Array', type: 'class', detail: 'Int16Array constructor' },
510
+ { label: 'Uint32Array', type: 'class', detail: 'Uint32Array constructor' },
511
+ { label: 'Int32Array', type: 'class', detail: 'Int32Array constructor' },
512
+ { label: 'Float32Array', type: 'class', detail: 'Float32Array constructor' },
513
+ { label: 'Float64Array', type: 'class', detail: 'Float64Array constructor' },
514
+
515
+ // Promises (though async/await is forbidden, Promise itself may be useful)
516
+ { label: 'Promise', type: 'class', detail: 'Promise constructor' },
517
+
518
+ // Global functions
519
+ { label: 'parseInt', type: 'function', detail: '(string, radix?) -> number' },
520
+ { label: 'parseFloat', type: 'function', detail: '(string) -> number' },
521
+ { label: 'isNaN', type: 'function', detail: '(value) -> boolean' },
522
+ { label: 'isFinite', type: 'function', detail: '(value) -> boolean' },
523
+ { label: 'encodeURI', type: 'function', detail: '(uri) -> string' },
524
+ { label: 'decodeURI', type: 'function', detail: '(encodedURI) -> string' },
525
+ {
526
+ label: 'encodeURIComponent',
527
+ type: 'function',
528
+ detail: '(component) -> string',
529
+ },
530
+ {
531
+ label: 'decodeURIComponent',
532
+ type: 'function',
533
+ detail: '(encoded) -> string',
534
+ },
535
+
536
+ // Global values
537
+ { label: 'undefined', type: 'keyword', detail: 'Undefined value' },
538
+ { label: 'null', type: 'keyword', detail: 'Null value' },
539
+ { label: 'NaN', type: 'keyword', detail: 'Not a Number' },
540
+ { label: 'Infinity', type: 'keyword', detail: 'Positive infinity' },
541
+ { label: 'globalThis', type: 'variable', detail: 'Global object' },
542
+ ]
543
+
544
+ // Expect matchers (after expect().
545
+ const EXPECT_MATCHERS: CMCompletion[] = [
546
+ snippetCompletion('toBe(${expected})', {
547
+ label: 'toBe',
548
+ type: 'method',
549
+ detail: '(expected: any)',
550
+ info: 'Strict equality (===)',
551
+ }),
552
+ snippetCompletion('toEqual(${expected})', {
553
+ label: 'toEqual',
554
+ type: 'method',
555
+ detail: '(expected: any)',
556
+ info: 'Deep equality',
557
+ }),
558
+ snippetCompletion('toContain(${item})', {
559
+ label: 'toContain',
560
+ type: 'method',
561
+ detail: '(item: any)',
562
+ info: 'Array/string contains',
563
+ }),
564
+ { label: 'toThrow', type: 'method', detail: '()', info: 'Throws an error' },
565
+ { label: 'toBeTruthy', type: 'method', detail: '()', info: 'Is truthy' },
566
+ { label: 'toBeFalsy', type: 'method', detail: '()', info: 'Is falsy' },
567
+ { label: 'toBeNull', type: 'method', detail: '()', info: 'Is null' },
568
+ {
569
+ label: 'toBeUndefined',
570
+ type: 'method',
571
+ detail: '()',
572
+ info: 'Is undefined',
573
+ },
574
+ ]
575
+
576
+ /**
577
+ * Extract function declarations from source
578
+ */
579
+ function extractFunctions(source: string): CMCompletion[] {
580
+ const completions: CMCompletion[] = []
581
+ const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)/g
582
+ let match
583
+ while ((match = funcRegex.exec(source)) !== null) {
584
+ const [, name, params] = match
585
+ completions.push(
586
+ snippetCompletion(`${name}(${params ? '${1}' : ''})`, {
587
+ label: name,
588
+ type: 'function',
589
+ detail: `(${params})`,
590
+ })
591
+ )
592
+ }
593
+ return completions
594
+ }
595
+
596
+ /**
597
+ * Extract variable declarations from source before cursor
598
+ */
599
+ function extractVariables(source: string, position: number): CMCompletion[] {
600
+ const completions: CMCompletion[] = []
601
+ const before = source.slice(0, position)
602
+ const varRegex = /(?:const|let)\s+(\w+)\s*=/g
603
+ let match
604
+ while ((match = varRegex.exec(before)) !== null) {
605
+ completions.push({
606
+ label: match[1],
607
+ type: 'variable',
608
+ })
609
+ }
610
+ return completions
611
+ }
612
+
613
+ /**
614
+ * Curated property completions for common globals
615
+ * These are hand-picked for usefulness with proper type signatures
616
+ */
617
+ const CURATED_PROPERTIES: Record<string, CMCompletion[]> = {
618
+ console: [
619
+ snippetCompletion('log(${1:message})', {
620
+ label: 'log',
621
+ type: 'method',
622
+ detail: '(...args: any[]) -> void',
623
+ info: 'Log to console',
624
+ }),
625
+ snippetCompletion('error(${1:message})', {
626
+ label: 'error',
627
+ type: 'method',
628
+ detail: '(...args: any[]) -> void',
629
+ info: 'Log error',
630
+ }),
631
+ snippetCompletion('warn(${1:message})', {
632
+ label: 'warn',
633
+ type: 'method',
634
+ detail: '(...args: any[]) -> void',
635
+ info: 'Log warning',
636
+ }),
637
+ snippetCompletion('info(${1:message})', {
638
+ label: 'info',
639
+ type: 'method',
640
+ detail: '(...args: any[]) -> void',
641
+ info: 'Log info',
642
+ }),
643
+ snippetCompletion('debug(${1:message})', {
644
+ label: 'debug',
645
+ type: 'method',
646
+ detail: '(...args: any[]) -> void',
647
+ info: 'Log debug',
648
+ }),
649
+ snippetCompletion('table(${1:data})', {
650
+ label: 'table',
651
+ type: 'method',
652
+ detail: '(data: any) -> void',
653
+ info: 'Display as table',
654
+ }),
655
+ snippetCompletion("time('${1:label}')", {
656
+ label: 'time',
657
+ type: 'method',
658
+ detail: '(label: string) -> void',
659
+ info: 'Start timer',
660
+ }),
661
+ snippetCompletion("timeEnd('${1:label}')", {
662
+ label: 'timeEnd',
663
+ type: 'method',
664
+ detail: '(label: string) -> void',
665
+ info: 'End timer',
666
+ }),
667
+ snippetCompletion("group('${1:label}')", {
668
+ label: 'group',
669
+ type: 'method',
670
+ detail: '(label?: string) -> void',
671
+ info: 'Start group',
672
+ }),
673
+ {
674
+ label: 'groupEnd',
675
+ type: 'method',
676
+ detail: '() -> void',
677
+ info: 'End group',
678
+ },
679
+ {
680
+ label: 'clear',
681
+ type: 'method',
682
+ detail: '() -> void',
683
+ info: 'Clear console',
684
+ },
685
+ ],
686
+ Math: [
687
+ // Common operations
688
+ snippetCompletion('floor(${1:x})', {
689
+ label: 'floor',
690
+ type: 'method',
691
+ detail: '(x: number) -> number',
692
+ info: 'Round down',
693
+ }),
694
+ snippetCompletion('ceil(${1:x})', {
695
+ label: 'ceil',
696
+ type: 'method',
697
+ detail: '(x: number) -> number',
698
+ info: 'Round up',
699
+ }),
700
+ snippetCompletion('round(${1:x})', {
701
+ label: 'round',
702
+ type: 'method',
703
+ detail: '(x: number) -> number',
704
+ info: 'Round to nearest',
705
+ }),
706
+ snippetCompletion('trunc(${1:x})', {
707
+ label: 'trunc',
708
+ type: 'method',
709
+ detail: '(x: number) -> number',
710
+ info: 'Remove decimals',
711
+ }),
712
+ snippetCompletion('abs(${1:x})', {
713
+ label: 'abs',
714
+ type: 'method',
715
+ detail: '(x: number) -> number',
716
+ info: 'Absolute value',
717
+ }),
718
+ snippetCompletion('sign(${1:x})', {
719
+ label: 'sign',
720
+ type: 'method',
721
+ detail: '(x: number) -> number',
722
+ info: 'Sign of number (-1, 0, 1)',
723
+ }),
724
+ // Min/max
725
+ snippetCompletion('min(${1:a}, ${2:b})', {
726
+ label: 'min',
727
+ type: 'method',
728
+ detail: '(...values: number[]) -> number',
729
+ info: 'Minimum value',
730
+ }),
731
+ snippetCompletion('max(${1:a}, ${2:b})', {
732
+ label: 'max',
733
+ type: 'method',
734
+ detail: '(...values: number[]) -> number',
735
+ info: 'Maximum value',
736
+ }),
737
+ snippetCompletion('clamp(${1:x}, ${2:min}, ${3:max})', {
738
+ label: 'clamp',
739
+ type: 'method',
740
+ detail: '(x, min, max) -> number',
741
+ info: 'Clamp to range (ES2024)',
742
+ }),
743
+ // Powers and roots
744
+ snippetCompletion('pow(${1:base}, ${2:exp})', {
745
+ label: 'pow',
746
+ type: 'method',
747
+ detail: '(base, exp) -> number',
748
+ info: 'Power',
749
+ }),
750
+ snippetCompletion('sqrt(${1:x})', {
751
+ label: 'sqrt',
752
+ type: 'method',
753
+ detail: '(x: number) -> number',
754
+ info: 'Square root',
755
+ }),
756
+ snippetCompletion('cbrt(${1:x})', {
757
+ label: 'cbrt',
758
+ type: 'method',
759
+ detail: '(x: number) -> number',
760
+ info: 'Cube root',
761
+ }),
762
+ snippetCompletion('hypot(${1:a}, ${2:b})', {
763
+ label: 'hypot',
764
+ type: 'method',
765
+ detail: '(...values: number[]) -> number',
766
+ info: 'Hypotenuse',
767
+ }),
768
+ // Logarithms
769
+ snippetCompletion('log(${1:x})', {
770
+ label: 'log',
771
+ type: 'method',
772
+ detail: '(x: number) -> number',
773
+ info: 'Natural log',
774
+ }),
775
+ snippetCompletion('log10(${1:x})', {
776
+ label: 'log10',
777
+ type: 'method',
778
+ detail: '(x: number) -> number',
779
+ info: 'Base 10 log',
780
+ }),
781
+ snippetCompletion('log2(${1:x})', {
782
+ label: 'log2',
783
+ type: 'method',
784
+ detail: '(x: number) -> number',
785
+ info: 'Base 2 log',
786
+ }),
787
+ snippetCompletion('exp(${1:x})', {
788
+ label: 'exp',
789
+ type: 'method',
790
+ detail: '(x: number) -> number',
791
+ info: 'e^x',
792
+ }),
793
+ // Trig
794
+ snippetCompletion('sin(${1:x})', {
795
+ label: 'sin',
796
+ type: 'method',
797
+ detail: '(radians: number) -> number',
798
+ }),
799
+ snippetCompletion('cos(${1:x})', {
800
+ label: 'cos',
801
+ type: 'method',
802
+ detail: '(radians: number) -> number',
803
+ }),
804
+ snippetCompletion('tan(${1:x})', {
805
+ label: 'tan',
806
+ type: 'method',
807
+ detail: '(radians: number) -> number',
808
+ }),
809
+ snippetCompletion('atan2(${1:y}, ${2:x})', {
810
+ label: 'atan2',
811
+ type: 'method',
812
+ detail: '(y, x) -> number',
813
+ info: 'Angle in radians',
814
+ }),
815
+ // Random
816
+ {
817
+ label: 'random',
818
+ type: 'method',
819
+ detail: '() -> number',
820
+ info: 'Random 0-1',
821
+ },
822
+ // Constants
823
+ { label: 'PI', type: 'property', detail: 'number', info: '3.14159...' },
824
+ { label: 'E', type: 'property', detail: 'number', info: '2.71828...' },
825
+ ],
826
+ JSON: [
827
+ snippetCompletion('parse(${1:text})', {
828
+ label: 'parse',
829
+ type: 'method',
830
+ detail: '(text: string) -> any',
831
+ info: 'Parse JSON string',
832
+ }),
833
+ snippetCompletion('stringify(${1:value})', {
834
+ label: 'stringify',
835
+ type: 'method',
836
+ detail: '(value: any, replacer?, space?) -> string',
837
+ info: 'Convert to JSON',
838
+ }),
839
+ ],
840
+ Object: [
841
+ snippetCompletion('keys(${1:obj})', {
842
+ label: 'keys',
843
+ type: 'method',
844
+ detail: '(obj: object) -> string[]',
845
+ info: 'Get property names',
846
+ }),
847
+ snippetCompletion('values(${1:obj})', {
848
+ label: 'values',
849
+ type: 'method',
850
+ detail: '(obj: object) -> any[]',
851
+ info: 'Get property values',
852
+ }),
853
+ snippetCompletion('entries(${1:obj})', {
854
+ label: 'entries',
855
+ type: 'method',
856
+ detail: '(obj: object) -> [string, any][]',
857
+ info: 'Get key-value pairs',
858
+ }),
859
+ snippetCompletion('fromEntries(${1:entries})', {
860
+ label: 'fromEntries',
861
+ type: 'method',
862
+ detail: '(entries: [string, any][]) -> object',
863
+ info: 'Create from entries',
864
+ }),
865
+ snippetCompletion('assign(${1:target}, ${2:source})', {
866
+ label: 'assign',
867
+ type: 'method',
868
+ detail: '(target, ...sources) -> object',
869
+ info: 'Copy properties',
870
+ }),
871
+ snippetCompletion('hasOwn(${1:obj}, ${2:prop})', {
872
+ label: 'hasOwn',
873
+ type: 'method',
874
+ detail: '(obj, prop: string) -> boolean',
875
+ info: 'Has own property',
876
+ }),
877
+ snippetCompletion('freeze(${1:obj})', {
878
+ label: 'freeze',
879
+ type: 'method',
880
+ detail: '(obj: T) -> T',
881
+ info: 'Make immutable',
882
+ }),
883
+ ],
884
+ Array: [
885
+ snippetCompletion('isArray(${1:value})', {
886
+ label: 'isArray',
887
+ type: 'method',
888
+ detail: '(value: any) -> boolean',
889
+ info: 'Check if array',
890
+ }),
891
+ snippetCompletion('from(${1:iterable})', {
892
+ label: 'from',
893
+ type: 'method',
894
+ detail: '(iterable, mapFn?) -> any[]',
895
+ info: 'Create from iterable',
896
+ }),
897
+ snippetCompletion('of(${1:items})', {
898
+ label: 'of',
899
+ type: 'method',
900
+ detail: '(...items) -> any[]',
901
+ info: 'Create from arguments',
902
+ }),
903
+ ],
904
+ String: [
905
+ snippetCompletion('fromCharCode(${1:code})', {
906
+ label: 'fromCharCode',
907
+ type: 'method',
908
+ detail: '(...codes: number[]) -> string',
909
+ }),
910
+ snippetCompletion('fromCodePoint(${1:code})', {
911
+ label: 'fromCodePoint',
912
+ type: 'method',
913
+ detail: '(...codes: number[]) -> string',
914
+ }),
915
+ ],
916
+ Number: [
917
+ snippetCompletion('isFinite(${1:value})', {
918
+ label: 'isFinite',
919
+ type: 'method',
920
+ detail: '(value: any) -> boolean',
921
+ }),
922
+ snippetCompletion('isInteger(${1:value})', {
923
+ label: 'isInteger',
924
+ type: 'method',
925
+ detail: '(value: any) -> boolean',
926
+ }),
927
+ snippetCompletion('isNaN(${1:value})', {
928
+ label: 'isNaN',
929
+ type: 'method',
930
+ detail: '(value: any) -> boolean',
931
+ }),
932
+ snippetCompletion('parseFloat(${1:string})', {
933
+ label: 'parseFloat',
934
+ type: 'method',
935
+ detail: '(string: string) -> number',
936
+ }),
937
+ snippetCompletion('parseInt(${1:string})', {
938
+ label: 'parseInt',
939
+ type: 'method',
940
+ detail: '(string: string, radix?) -> number',
941
+ }),
942
+ {
943
+ label: 'MAX_SAFE_INTEGER',
944
+ type: 'property',
945
+ detail: 'number',
946
+ info: '2^53 - 1',
947
+ },
948
+ {
949
+ label: 'MIN_SAFE_INTEGER',
950
+ type: 'property',
951
+ detail: 'number',
952
+ info: '-(2^53 - 1)',
953
+ },
954
+ {
955
+ label: 'EPSILON',
956
+ type: 'property',
957
+ detail: 'number',
958
+ info: 'Smallest difference',
959
+ },
960
+ ],
961
+ Date: [
962
+ {
963
+ label: 'now',
964
+ type: 'method',
965
+ detail: '() -> number',
966
+ info: 'Current timestamp',
967
+ },
968
+ snippetCompletion('parse(${1:dateString})', {
969
+ label: 'parse',
970
+ type: 'method',
971
+ detail: '(dateString: string) -> number',
972
+ }),
973
+ snippetCompletion('UTC(${1:year}, ${2:month})', {
974
+ label: 'UTC',
975
+ type: 'method',
976
+ detail: '(year, month, ...) -> number',
977
+ }),
978
+ ],
979
+ Promise: [
980
+ snippetCompletion('resolve(${1:value})', {
981
+ label: 'resolve',
982
+ type: 'method',
983
+ detail: '(value: T) -> Promise<T>',
984
+ }),
985
+ snippetCompletion('reject(${1:reason})', {
986
+ label: 'reject',
987
+ type: 'method',
988
+ detail: '(reason: any) -> Promise<never>',
989
+ }),
990
+ snippetCompletion('all(${1:promises})', {
991
+ label: 'all',
992
+ type: 'method',
993
+ detail: '(promises: Promise[]) -> Promise<any[]>',
994
+ info: 'Wait for all',
995
+ }),
996
+ snippetCompletion('allSettled(${1:promises})', {
997
+ label: 'allSettled',
998
+ type: 'method',
999
+ detail: '(promises: Promise[]) -> Promise<Result[]>',
1000
+ info: 'Wait for all to settle',
1001
+ }),
1002
+ snippetCompletion('race(${1:promises})', {
1003
+ label: 'race',
1004
+ type: 'method',
1005
+ detail: '(promises: Promise[]) -> Promise<any>',
1006
+ info: 'First to resolve/reject',
1007
+ }),
1008
+ snippetCompletion('any(${1:promises})', {
1009
+ label: 'any',
1010
+ type: 'method',
1011
+ detail: '(promises: Promise[]) -> Promise<any>',
1012
+ info: 'First to resolve',
1013
+ }),
1014
+ ],
1015
+ }
1016
+
1017
+ /**
1018
+ * Known global objects that can be introspected for property completion
1019
+ * Falls back to runtime introspection if not in CURATED_PROPERTIES
1020
+ */
1021
+ const INTROSPECTABLE_GLOBALS: Record<string, any> = {
1022
+ // Core JS globals (always available)
1023
+ console,
1024
+ Math,
1025
+ JSON,
1026
+ Object,
1027
+ Array,
1028
+ String,
1029
+ Number,
1030
+ Boolean,
1031
+ Date,
1032
+ RegExp,
1033
+ Map,
1034
+ Set,
1035
+ WeakMap,
1036
+ WeakSet,
1037
+ Promise,
1038
+ Reflect,
1039
+ Proxy,
1040
+ Symbol,
1041
+ Error,
1042
+ TypeError,
1043
+ RangeError,
1044
+ SyntaxError,
1045
+ ReferenceError,
1046
+ ArrayBuffer,
1047
+ Uint8Array,
1048
+ Int8Array,
1049
+ Uint16Array,
1050
+ Int16Array,
1051
+ Uint32Array,
1052
+ Int32Array,
1053
+ Float32Array,
1054
+ Float64Array,
1055
+ Intl,
1056
+ }
1057
+
1058
+ // Add browser globals when available (isomorphic - works in Node too)
1059
+ if (typeof globalThis !== 'undefined') {
1060
+ // Browser APIs
1061
+ if (typeof crypto !== 'undefined') INTROSPECTABLE_GLOBALS.crypto = crypto
1062
+ if (typeof navigator !== 'undefined')
1063
+ INTROSPECTABLE_GLOBALS.navigator = navigator
1064
+ if (typeof localStorage !== 'undefined')
1065
+ INTROSPECTABLE_GLOBALS.localStorage = localStorage
1066
+ if (typeof sessionStorage !== 'undefined')
1067
+ INTROSPECTABLE_GLOBALS.sessionStorage = sessionStorage
1068
+ if (typeof fetch !== 'undefined') INTROSPECTABLE_GLOBALS.fetch = fetch
1069
+ if (typeof URL !== 'undefined') INTROSPECTABLE_GLOBALS.URL = URL
1070
+ if (typeof URLSearchParams !== 'undefined')
1071
+ INTROSPECTABLE_GLOBALS.URLSearchParams = URLSearchParams
1072
+ if (typeof Headers !== 'undefined') INTROSPECTABLE_GLOBALS.Headers = Headers
1073
+ if (typeof Request !== 'undefined') INTROSPECTABLE_GLOBALS.Request = Request
1074
+ if (typeof Response !== 'undefined')
1075
+ INTROSPECTABLE_GLOBALS.Response = Response
1076
+ if (typeof FormData !== 'undefined')
1077
+ INTROSPECTABLE_GLOBALS.FormData = FormData
1078
+ if (typeof Blob !== 'undefined') INTROSPECTABLE_GLOBALS.Blob = Blob
1079
+ if (typeof File !== 'undefined') INTROSPECTABLE_GLOBALS.File = File
1080
+ if (typeof FileReader !== 'undefined')
1081
+ INTROSPECTABLE_GLOBALS.FileReader = FileReader
1082
+ if (typeof AbortController !== 'undefined')
1083
+ INTROSPECTABLE_GLOBALS.AbortController = AbortController
1084
+ if (typeof TextEncoder !== 'undefined')
1085
+ INTROSPECTABLE_GLOBALS.TextEncoder = TextEncoder
1086
+ if (typeof TextDecoder !== 'undefined')
1087
+ INTROSPECTABLE_GLOBALS.TextDecoder = TextDecoder
1088
+
1089
+ // DOM classes (for instanceof checks and static methods)
1090
+ if (typeof Element !== 'undefined') INTROSPECTABLE_GLOBALS.Element = Element
1091
+ if (typeof HTMLElement !== 'undefined')
1092
+ INTROSPECTABLE_GLOBALS.HTMLElement = HTMLElement
1093
+ if (typeof Document !== 'undefined')
1094
+ INTROSPECTABLE_GLOBALS.Document = Document
1095
+ if (typeof Node !== 'undefined') INTROSPECTABLE_GLOBALS.Node = Node
1096
+ if (typeof Event !== 'undefined') INTROSPECTABLE_GLOBALS.Event = Event
1097
+ if (typeof CustomEvent !== 'undefined')
1098
+ INTROSPECTABLE_GLOBALS.CustomEvent = CustomEvent
1099
+ if (typeof MutationObserver !== 'undefined')
1100
+ INTROSPECTABLE_GLOBALS.MutationObserver = MutationObserver
1101
+ if (typeof ResizeObserver !== 'undefined')
1102
+ INTROSPECTABLE_GLOBALS.ResizeObserver = ResizeObserver
1103
+ if (typeof IntersectionObserver !== 'undefined')
1104
+ INTROSPECTABLE_GLOBALS.IntersectionObserver = IntersectionObserver
1105
+
1106
+ // Canvas/WebGL
1107
+ if (typeof CanvasRenderingContext2D !== 'undefined')
1108
+ INTROSPECTABLE_GLOBALS.CanvasRenderingContext2D = CanvasRenderingContext2D
1109
+ if (typeof ImageData !== 'undefined')
1110
+ INTROSPECTABLE_GLOBALS.ImageData = ImageData
1111
+
1112
+ // Audio
1113
+ if (typeof AudioContext !== 'undefined')
1114
+ INTROSPECTABLE_GLOBALS.AudioContext = AudioContext
1115
+
1116
+ // Performance
1117
+ if (typeof performance !== 'undefined')
1118
+ INTROSPECTABLE_GLOBALS.performance = performance
1119
+ if (typeof PerformanceObserver !== 'undefined')
1120
+ INTROSPECTABLE_GLOBALS.PerformanceObserver = PerformanceObserver
1121
+
1122
+ // Global objects (singletons)
1123
+ if (typeof document !== 'undefined')
1124
+ INTROSPECTABLE_GLOBALS.document = document
1125
+ if (typeof window !== 'undefined') INTROSPECTABLE_GLOBALS.window = window
1126
+ }
1127
+
1128
+ /**
1129
+ * Get completions for properties of an object
1130
+ * Uses curated list if available, falls back to runtime introspection
1131
+ */
1132
+ function getPropertyCompletions(objName: string): CMCompletion[] {
1133
+ // Prefer curated completions with proper type info
1134
+ if (CURATED_PROPERTIES[objName]) {
1135
+ return CURATED_PROPERTIES[objName]
1136
+ }
1137
+
1138
+ // Fall back to runtime introspection for uncurated objects
1139
+ const obj = INTROSPECTABLE_GLOBALS[objName]
1140
+ if (!obj) return []
1141
+
1142
+ const completions: CMCompletion[] = []
1143
+ const seen = new Set<string>()
1144
+
1145
+ // Get own properties and prototype chain
1146
+ let current = obj
1147
+ while (current && current !== Object.prototype) {
1148
+ for (const key of Object.getOwnPropertyNames(current)) {
1149
+ // Skip constructor, private-ish names, and already seen
1150
+ if (key === 'constructor' || key.startsWith('_') || seen.has(key)) {
1151
+ continue
1152
+ }
1153
+ seen.add(key)
1154
+
1155
+ try {
1156
+ const descriptor = Object.getOwnPropertyDescriptor(current, key)
1157
+ const value =
1158
+ descriptor?.value ?? (descriptor?.get ? '[getter]' : undefined)
1159
+ const valueType = typeof value
1160
+
1161
+ if (valueType === 'function') {
1162
+ // Try to get function signature from length
1163
+ const fn = value as Function
1164
+ const paramCount = fn.length
1165
+ const params =
1166
+ paramCount > 0
1167
+ ? Array.from(
1168
+ { length: paramCount },
1169
+ (_, i) => `arg${i + 1}`
1170
+ ).join(', ')
1171
+ : ''
1172
+
1173
+ completions.push(
1174
+ snippetCompletion(`${key}(${paramCount > 0 ? '${1}' : ''})`, {
1175
+ label: key,
1176
+ type: 'method',
1177
+ detail: `(${params})`,
1178
+ boost: key.startsWith('to') ? -1 : 0, // Demote toString, etc.
1179
+ })
1180
+ )
1181
+ } else {
1182
+ // Property or constant
1183
+ const type =
1184
+ valueType === 'number'
1185
+ ? 'property'
1186
+ : valueType === 'string'
1187
+ ? 'property'
1188
+ : valueType === 'boolean'
1189
+ ? 'property'
1190
+ : 'property'
1191
+
1192
+ completions.push({
1193
+ label: key,
1194
+ type,
1195
+ detail: valueType,
1196
+ })
1197
+ }
1198
+ } catch {
1199
+ // Some properties may throw on access - skip them
1200
+ }
1201
+ }
1202
+ current = Object.getPrototypeOf(current)
1203
+ }
1204
+
1205
+ return completions
1206
+ }
1207
+
1208
+ /**
1209
+ * Extract the object name before a dot from source
1210
+ * e.g., "console." -> "console", "Math.floor" -> "Math"
1211
+ */
1212
+ function getObjectBeforeDot(source: string, dotPos: number): string | null {
1213
+ // Look backwards from the dot to find the identifier
1214
+ const before = source.slice(0, dotPos)
1215
+ const match = before.match(/(\w+)\s*$/)
1216
+ return match ? match[1] : null
1217
+ }
1218
+
1219
+ /**
1220
+ * Get a placeholder value for a parameter based on its type info
1221
+ * Returns a sensible default that can be used in snippet placeholders
1222
+ */
1223
+ function getPlaceholderForParam(name: string, info: any): string {
1224
+ // First check for example value (from TJS colon syntax)
1225
+ if (info.example !== undefined && info.example !== null) {
1226
+ const ex = info.example
1227
+ if (typeof ex === 'string') return `'${ex}'`
1228
+ if (typeof ex === 'number' || typeof ex === 'boolean') return String(ex)
1229
+ if (Array.isArray(ex)) return JSON.stringify(ex)
1230
+ if (typeof ex === 'object') return JSON.stringify(ex)
1231
+ return String(ex)
1232
+ }
1233
+
1234
+ // Then check for explicit default value
1235
+ if (info.default !== undefined && info.default !== null) {
1236
+ const def = info.default
1237
+ if (typeof def === 'string') return `'${def}'`
1238
+ if (typeof def === 'number' || typeof def === 'boolean') return String(def)
1239
+ if (Array.isArray(def)) return JSON.stringify(def)
1240
+ if (typeof def === 'object') return JSON.stringify(def)
1241
+ return String(def)
1242
+ }
1243
+
1244
+ // Otherwise generate based on type
1245
+ const kind = info.type?.kind || info.type?.type || 'any'
1246
+ switch (kind) {
1247
+ case 'string':
1248
+ return `'${name}'`
1249
+ case 'number':
1250
+ return '0'
1251
+ case 'boolean':
1252
+ return 'true'
1253
+ case 'null':
1254
+ return 'null'
1255
+ case 'array':
1256
+ return '[]'
1257
+ case 'object':
1258
+ return '{}'
1259
+ default:
1260
+ return name
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * Create TJS/AJS completion source
1266
+ */
1267
+ function tjsCompletionSource(config: AutocompleteConfig = {}) {
1268
+ return (context: CMCompletionContext): CompletionResult | null => {
1269
+ // Get word at cursor
1270
+ const word = context.matchBefore(/[\w$]*/)
1271
+ if (!word) return null
1272
+
1273
+ const source = context.state.doc.toString()
1274
+ const pos = context.pos
1275
+
1276
+ // Don't complete inside strings or comments
1277
+ const skipRegions = findSkipRegions(source)
1278
+ if (isInSkipRegion(pos, skipRegions)) {
1279
+ return null
1280
+ }
1281
+
1282
+ // Check context before cursor
1283
+ const lineStart = context.state.doc.lineAt(pos).from
1284
+ const lineBefore = source.slice(lineStart, word.from)
1285
+ const charBefore = source.slice(Math.max(0, word.from - 1), word.from)
1286
+
1287
+ // Don't complete in the middle of a word unless explicit,
1288
+ // BUT always allow completion after a dot (for property access)
1289
+ if (word.from === word.to && !context.explicit && charBefore !== '.') {
1290
+ return null
1291
+ }
1292
+
1293
+ let options: CMCompletion[] = []
1294
+
1295
+ // After . - property completion
1296
+ if (charBefore === '.') {
1297
+ const before = source.slice(Math.max(0, word.from - 50), word.from)
1298
+
1299
+ // Check for expect() matchers first
1300
+ if (/expect\s*\([^)]*\)\s*\.$/.test(before)) {
1301
+ options = EXPECT_MATCHERS
1302
+ } else {
1303
+ // Try to get object name and introspect its properties
1304
+ const objName = getObjectBeforeDot(source, word.from - 1)
1305
+ if (objName) {
1306
+ options = getPropertyCompletions(objName)
1307
+ }
1308
+ }
1309
+ }
1310
+ // After : - type context
1311
+ else if (/:\s*$/.test(lineBefore)) {
1312
+ options = TJS_TYPES
1313
+ }
1314
+ // After -> - return type context
1315
+ else if (/->\s*$/.test(lineBefore)) {
1316
+ options = TJS_TYPES
1317
+ }
1318
+ // General completions
1319
+ else {
1320
+ options = [
1321
+ ...TJS_COMPLETIONS,
1322
+ ...RUNTIME_COMPLETIONS,
1323
+ ...GLOBAL_COMPLETIONS,
1324
+ ...extractFunctions(source),
1325
+ ...extractVariables(source, pos),
1326
+ ]
1327
+
1328
+ // Add metadata-based completions if available
1329
+ const metadata = config.getMetadata?.()
1330
+ if (metadata) {
1331
+ for (const [name, meta] of Object.entries(metadata)) {
1332
+ // Build parameter list with types for display
1333
+ const paramEntries = meta.params ? Object.entries(meta.params) : []
1334
+ const paramList = paramEntries
1335
+ .map(([pName, pInfo]: [string, any]) => {
1336
+ const pType = pInfo.type?.kind || pInfo.type?.type || 'any'
1337
+ const optional = !pInfo.required
1338
+ return optional ? `${pName}?: ${pType}` : `${pName}: ${pType}`
1339
+ })
1340
+ .join(', ')
1341
+
1342
+ // Build snippet with example values as placeholders
1343
+ const snippetParams = paramEntries
1344
+ .map(([pName, pInfo]: [string, any], i) => {
1345
+ // Use default value or generate placeholder based on type
1346
+ const placeholder = getPlaceholderForParam(pName, pInfo)
1347
+ return `\${${i + 1}:${placeholder}}`
1348
+ })
1349
+ .join(', ')
1350
+
1351
+ // Handle both { type: 'string' } and { kind: 'string' } formats
1352
+ const returnType = meta.returns?.type || meta.returns?.kind || 'void'
1353
+ options.push(
1354
+ snippetCompletion(`${name}(${snippetParams})`, {
1355
+ label: name,
1356
+ type: 'function',
1357
+ detail: `(${paramList}) -> ${returnType}`,
1358
+ info: meta.description,
1359
+ boost: 2, // Boost user-defined functions above globals
1360
+ })
1361
+ )
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+ if (options.length === 0) return null
1367
+
1368
+ return {
1369
+ from: word.from,
1370
+ options,
1371
+ validFor: /^[\w$]*$/,
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ /**
1377
+ * Create AsyncJS language support for CodeMirror 6
1378
+ *
1379
+ * @param config Optional configuration
1380
+ * @returns Extension array for CodeMirror
1381
+ */
1382
+ export function ajsEditorExtension(
1383
+ config: {
1384
+ jsx?: boolean
1385
+ typescript?: boolean
1386
+ autocomplete?: AutocompleteConfig
1387
+ } = {}
1388
+ ): Extension {
1389
+ return [
1390
+ javascript({ jsx: config.jsx, typescript: config.typescript }),
1391
+ syntaxHighlighting(defaultHighlightStyle),
1392
+ forbiddenHighlighter,
1393
+ tryWithoutCatchHighlighter,
1394
+ ajsTheme,
1395
+ syntaxHighlighting(ajsHighlightStyle),
1396
+ autocompletion({
1397
+ override: [tjsCompletionSource(config.autocomplete || {})],
1398
+ activateOnTyping: true,
1399
+ }),
1400
+ ]
1401
+ }
1402
+
1403
+ // Alias for backwards compatibility
1404
+ export { ajsEditorExtension as ajs }
1405
+
1406
+ /**
1407
+ * TJS editor extension - like AJS but with fewer restrictions
1408
+ * Allows: import/export, async/await, throw
1409
+ */
1410
+ export function tjsEditorExtension(
1411
+ config: {
1412
+ jsx?: boolean
1413
+ typescript?: boolean
1414
+ autocomplete?: AutocompleteConfig
1415
+ } = {}
1416
+ ): Extension {
1417
+ return [
1418
+ javascript({ jsx: config.jsx, typescript: config.typescript }),
1419
+ syntaxHighlighting(defaultHighlightStyle),
1420
+ tjsForbiddenHighlighter, // Use TJS forbidden list (more permissive)
1421
+ tryWithoutCatchHighlighter,
1422
+ ajsTheme,
1423
+ syntaxHighlighting(ajsHighlightStyle),
1424
+ autocompletion({
1425
+ override: [tjsCompletionSource(config.autocomplete || {})],
1426
+ activateOnTyping: true,
1427
+ }),
1428
+ ]
1429
+ }
1430
+
1431
+ /**
1432
+ * AsyncJS language support wrapped as LanguageSupport
1433
+ * Use this if you need access to the language object
1434
+ */
1435
+ export function ajsLanguage(
1436
+ config: { jsx?: boolean; typescript?: boolean } = {}
1437
+ ): LanguageSupport {
1438
+ const jsLang = javascript({ jsx: config.jsx, typescript: config.typescript })
1439
+ return new LanguageSupport(jsLang.language, [
1440
+ forbiddenHighlighter,
1441
+ tryWithoutCatchHighlighter,
1442
+ ajsTheme,
1443
+ syntaxHighlighting(ajsHighlightStyle),
1444
+ ])
1445
+ }
1446
+
1447
+ export { FORBIDDEN_KEYWORDS }