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.
- package/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- 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 }
|