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,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TJS Autocompletion Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides context-aware completions for the TJS playground.
|
|
5
|
+
* Uses __tjs metadata for rich type information.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Completion {
|
|
9
|
+
/** The text to insert */
|
|
10
|
+
label: string
|
|
11
|
+
/** What kind of completion (function, variable, property, keyword) */
|
|
12
|
+
kind: 'function' | 'variable' | 'property' | 'keyword' | 'type'
|
|
13
|
+
/** Description shown in autocomplete popup */
|
|
14
|
+
detail?: string
|
|
15
|
+
/** Full documentation (markdown supported) */
|
|
16
|
+
documentation?: string
|
|
17
|
+
/** Text to insert (if different from label) */
|
|
18
|
+
insertText?: string
|
|
19
|
+
/** Snippet with tab stops: ${1:param} */
|
|
20
|
+
snippet?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CompletionContext {
|
|
24
|
+
/** Source code being edited */
|
|
25
|
+
source: string
|
|
26
|
+
/** Cursor position (character offset) */
|
|
27
|
+
position: number
|
|
28
|
+
/** Parsed __tjs metadata from current file */
|
|
29
|
+
metadata?: Record<string, TJSMetadata>
|
|
30
|
+
/** Imported module metadata */
|
|
31
|
+
imports?: Record<string, Record<string, TJSMetadata>>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TJSMetadata {
|
|
35
|
+
params?: Record<string, ParamInfo>
|
|
36
|
+
returns?: { type: string }
|
|
37
|
+
description?: string
|
|
38
|
+
typeParams?: Record<string, { constraint?: string; default?: string }>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ParamInfo {
|
|
42
|
+
type: string
|
|
43
|
+
required: boolean
|
|
44
|
+
default?: any
|
|
45
|
+
description?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TJS keywords
|
|
49
|
+
const TJS_KEYWORDS: Completion[] = [
|
|
50
|
+
{ label: 'function', kind: 'keyword', detail: 'Declare a function' },
|
|
51
|
+
{ label: 'const', kind: 'keyword', detail: 'Declare a constant' },
|
|
52
|
+
{ label: 'let', kind: 'keyword', detail: 'Declare a variable' },
|
|
53
|
+
{ label: 'if', kind: 'keyword', detail: 'Conditional statement' },
|
|
54
|
+
{ label: 'else', kind: 'keyword', detail: 'Else branch' },
|
|
55
|
+
{ label: 'while', kind: 'keyword', detail: 'While loop' },
|
|
56
|
+
{ label: 'for', kind: 'keyword', detail: 'For loop' },
|
|
57
|
+
{ label: 'return', kind: 'keyword', detail: 'Return from function' },
|
|
58
|
+
{ label: 'try', kind: 'keyword', detail: 'Try block' },
|
|
59
|
+
{ label: 'catch', kind: 'keyword', detail: 'Catch block' },
|
|
60
|
+
{ label: 'async', kind: 'keyword', detail: 'Async function' },
|
|
61
|
+
{ label: 'await', kind: 'keyword', detail: 'Await promise' },
|
|
62
|
+
{ label: 'import', kind: 'keyword', detail: 'Import module' },
|
|
63
|
+
{ label: 'export', kind: 'keyword', detail: 'Export declaration' },
|
|
64
|
+
{
|
|
65
|
+
label: 'test',
|
|
66
|
+
kind: 'keyword',
|
|
67
|
+
detail: 'Inline test block',
|
|
68
|
+
snippet: "test('${1:description}') {\n ${2:// test code}\n}",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: 'mock',
|
|
72
|
+
kind: 'keyword',
|
|
73
|
+
detail: 'Mock setup block',
|
|
74
|
+
snippet: 'mock {\n ${1:// setup code}\n}',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'unsafe',
|
|
78
|
+
kind: 'keyword',
|
|
79
|
+
detail: 'Skip type validation',
|
|
80
|
+
snippet: 'unsafe {\n ${1:// unvalidated code}\n}',
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
// TJS type examples
|
|
85
|
+
const TJS_TYPES: Completion[] = [
|
|
86
|
+
{ label: "''", kind: 'type', detail: 'String type', insertText: "''" },
|
|
87
|
+
{ label: '0', kind: 'type', detail: 'Number type', insertText: '0' },
|
|
88
|
+
{ label: 'true', kind: 'type', detail: 'Boolean type', insertText: 'true' },
|
|
89
|
+
{ label: 'null', kind: 'type', detail: 'Null type', insertText: 'null' },
|
|
90
|
+
{
|
|
91
|
+
label: 'undefined',
|
|
92
|
+
kind: 'type',
|
|
93
|
+
detail: 'Undefined type',
|
|
94
|
+
insertText: 'undefined',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "['']",
|
|
98
|
+
kind: 'type',
|
|
99
|
+
detail: 'Array of strings',
|
|
100
|
+
insertText: "['']",
|
|
101
|
+
},
|
|
102
|
+
{ label: '[0]', kind: 'type', detail: 'Array of numbers', insertText: '[0]' },
|
|
103
|
+
{ label: '{}', kind: 'type', detail: 'Object type', insertText: '{}' },
|
|
104
|
+
{ label: 'any', kind: 'type', detail: 'Any type', insertText: 'any' },
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
// Runtime globals
|
|
108
|
+
const RUNTIME_COMPLETIONS: Completion[] = [
|
|
109
|
+
{
|
|
110
|
+
label: 'isError',
|
|
111
|
+
kind: 'function',
|
|
112
|
+
detail: '(value: any) -> boolean',
|
|
113
|
+
documentation: 'Check if a value is a TJS error',
|
|
114
|
+
snippet: 'isError(${1:value})',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'error',
|
|
118
|
+
kind: 'function',
|
|
119
|
+
detail: '(message: string, details?: object) -> TJSError',
|
|
120
|
+
documentation: 'Create a TJS error object',
|
|
121
|
+
snippet: "error('${1:message}')",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: 'typeOf',
|
|
125
|
+
kind: 'function',
|
|
126
|
+
detail: '(value: any) -> string',
|
|
127
|
+
documentation: 'Get type name (fixed typeof - handles null, arrays)',
|
|
128
|
+
snippet: 'typeOf(${1:value})',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: 'wrap',
|
|
132
|
+
kind: 'function',
|
|
133
|
+
detail: '(fn: Function, meta: TJSMeta) -> Function',
|
|
134
|
+
documentation: 'Wrap function with runtime type validation',
|
|
135
|
+
snippet: 'wrap(${1:fn}, ${2:meta})',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
label: 'expect',
|
|
139
|
+
kind: 'function',
|
|
140
|
+
detail: '(actual: any) -> Matchers',
|
|
141
|
+
documentation: 'Test assertion (in test blocks)',
|
|
142
|
+
snippet: 'expect(${1:actual})',
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
// Common assertion matchers
|
|
147
|
+
const EXPECT_MATCHERS: Completion[] = [
|
|
148
|
+
{
|
|
149
|
+
label: 'toBe',
|
|
150
|
+
kind: 'function',
|
|
151
|
+
detail: '(expected: any) -> void',
|
|
152
|
+
documentation: 'Strict equality check (===)',
|
|
153
|
+
snippet: 'toBe(${1:expected})',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
label: 'toEqual',
|
|
157
|
+
kind: 'function',
|
|
158
|
+
detail: '(expected: any) -> void',
|
|
159
|
+
documentation: 'Deep equality check',
|
|
160
|
+
snippet: 'toEqual(${1:expected})',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: 'toContain',
|
|
164
|
+
kind: 'function',
|
|
165
|
+
detail: '(item: any) -> void',
|
|
166
|
+
documentation: 'Array/string contains check',
|
|
167
|
+
snippet: 'toContain(${1:item})',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: 'toThrow',
|
|
171
|
+
kind: 'function',
|
|
172
|
+
detail: '(message?: string) -> void',
|
|
173
|
+
documentation: 'Check that function throws',
|
|
174
|
+
snippet: 'toThrow(${1:message})',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
label: 'toBeTruthy',
|
|
178
|
+
kind: 'property',
|
|
179
|
+
detail: 'getter',
|
|
180
|
+
documentation: 'Check value is truthy',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
label: 'toBeFalsy',
|
|
184
|
+
kind: 'property',
|
|
185
|
+
detail: 'getter',
|
|
186
|
+
documentation: 'Check value is falsy',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
label: 'toBeNull',
|
|
190
|
+
kind: 'property',
|
|
191
|
+
detail: 'getter',
|
|
192
|
+
documentation: 'Check value is null',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
label: 'toBeUndefined',
|
|
196
|
+
kind: 'property',
|
|
197
|
+
detail: 'getter',
|
|
198
|
+
documentation: 'Check value is undefined',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
label: 'toBeGreaterThan',
|
|
202
|
+
kind: 'function',
|
|
203
|
+
detail: '(expected: number) -> void',
|
|
204
|
+
documentation: 'Check value is greater than expected',
|
|
205
|
+
snippet: 'toBeGreaterThan(${1:expected})',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: 'toBeLessThan',
|
|
209
|
+
kind: 'function',
|
|
210
|
+
detail: '(expected: number) -> void',
|
|
211
|
+
documentation: 'Check value is less than expected',
|
|
212
|
+
snippet: 'toBeLessThan(${1:expected})',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: 'toBeGreaterThanOrEqual',
|
|
216
|
+
kind: 'function',
|
|
217
|
+
detail: '(expected: number) -> void',
|
|
218
|
+
documentation: 'Check value is >= expected',
|
|
219
|
+
snippet: 'toBeGreaterThanOrEqual(${1:expected})',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: 'toBeLessThanOrEqual',
|
|
223
|
+
kind: 'function',
|
|
224
|
+
detail: '(expected: number) -> void',
|
|
225
|
+
documentation: 'Check value is <= expected',
|
|
226
|
+
snippet: 'toBeLessThanOrEqual(${1:expected})',
|
|
227
|
+
},
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the word being typed at the cursor position
|
|
232
|
+
*/
|
|
233
|
+
function getWordAtPosition(
|
|
234
|
+
source: string,
|
|
235
|
+
position: number
|
|
236
|
+
): { word: string; start: number } {
|
|
237
|
+
let start = position
|
|
238
|
+
while (start > 0 && /[\w$]/.test(source[start - 1])) {
|
|
239
|
+
start--
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
word: source.slice(start, position),
|
|
243
|
+
start,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get context around cursor (what's before the word)
|
|
249
|
+
*/
|
|
250
|
+
function getContextBefore(source: string, start: number): string {
|
|
251
|
+
// Look back for context (dot, opening paren, etc.)
|
|
252
|
+
let contextStart = start - 1
|
|
253
|
+
while (contextStart >= 0 && /\s/.test(source[contextStart])) {
|
|
254
|
+
contextStart--
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Get the previous 50 chars for context
|
|
258
|
+
const contextEnd = contextStart + 1
|
|
259
|
+
const contextBegin = Math.max(0, contextEnd - 50)
|
|
260
|
+
return source.slice(contextBegin, contextEnd)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if we're inside a test block
|
|
265
|
+
*/
|
|
266
|
+
function isInTestBlock(source: string, position: number): boolean {
|
|
267
|
+
// Simple heuristic: look for unmatched `test(` before position
|
|
268
|
+
const before = source.slice(0, position)
|
|
269
|
+
const testMatches = before.match(/test\s*\([^)]*\)\s*\{/g) || []
|
|
270
|
+
const closeBraces = (before.match(/\}/g) || []).length
|
|
271
|
+
|
|
272
|
+
// Very rough: if we have more test opens than closes, we're in a test
|
|
273
|
+
return testMatches.length > 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Extract function names from source
|
|
278
|
+
*/
|
|
279
|
+
function extractFunctions(source: string): Completion[] {
|
|
280
|
+
const completions: Completion[] = []
|
|
281
|
+
const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)/g
|
|
282
|
+
|
|
283
|
+
let match
|
|
284
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
285
|
+
const [, name, params] = match
|
|
286
|
+
completions.push({
|
|
287
|
+
label: name,
|
|
288
|
+
kind: 'function',
|
|
289
|
+
detail: `(${params})`,
|
|
290
|
+
snippet: `${name}(${params ? '${1}' : ''})`,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return completions
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Extract variable names from source
|
|
299
|
+
*/
|
|
300
|
+
function extractVariables(source: string, position: number): Completion[] {
|
|
301
|
+
const completions: Completion[] = []
|
|
302
|
+
const before = source.slice(0, position)
|
|
303
|
+
|
|
304
|
+
// Match const/let declarations
|
|
305
|
+
const varRegex = /(?:const|let)\s+(\w+)\s*=/g
|
|
306
|
+
let match
|
|
307
|
+
while ((match = varRegex.exec(before)) !== null) {
|
|
308
|
+
completions.push({
|
|
309
|
+
label: match[1],
|
|
310
|
+
kind: 'variable',
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return completions
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build completions from __tjs metadata
|
|
319
|
+
*/
|
|
320
|
+
function completionsFromMetadata(
|
|
321
|
+
metadata: Record<string, TJSMetadata>
|
|
322
|
+
): Completion[] {
|
|
323
|
+
const completions: Completion[] = []
|
|
324
|
+
|
|
325
|
+
for (const [name, meta] of Object.entries(metadata)) {
|
|
326
|
+
const params = meta.params ? Object.keys(meta.params).join(', ') : ''
|
|
327
|
+
const returnType = meta.returns?.type || 'void'
|
|
328
|
+
|
|
329
|
+
completions.push({
|
|
330
|
+
label: name,
|
|
331
|
+
kind: 'function',
|
|
332
|
+
detail: `(${params}) -> ${returnType}`,
|
|
333
|
+
documentation: meta.description,
|
|
334
|
+
snippet: params
|
|
335
|
+
? `${name}(${Object.keys(meta.params || {})
|
|
336
|
+
.map((p, i) => `\${${i + 1}:${p}}`)
|
|
337
|
+
.join(', ')})`
|
|
338
|
+
: `${name}()`,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return completions
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get autocompletions for the current context
|
|
347
|
+
*/
|
|
348
|
+
export function getCompletions(ctx: CompletionContext): Completion[] {
|
|
349
|
+
const { source, position, metadata, imports } = ctx
|
|
350
|
+
const { word, start } = getWordAtPosition(source, position)
|
|
351
|
+
const contextBefore = getContextBefore(source, start)
|
|
352
|
+
|
|
353
|
+
let completions: Completion[] = []
|
|
354
|
+
|
|
355
|
+
// After a dot - property/method access
|
|
356
|
+
if (contextBefore.endsWith('.')) {
|
|
357
|
+
// Check if it's expect().
|
|
358
|
+
if (/expect\s*\([^)]*\)\s*\.$/.test(contextBefore)) {
|
|
359
|
+
completions = [...EXPECT_MATCHERS]
|
|
360
|
+
}
|
|
361
|
+
// Could add more dot-completion contexts here
|
|
362
|
+
}
|
|
363
|
+
// After : in function params - type context
|
|
364
|
+
else if (/:\s*$/.test(contextBefore)) {
|
|
365
|
+
completions = [...TJS_TYPES]
|
|
366
|
+
}
|
|
367
|
+
// After -> return type
|
|
368
|
+
else if (/->\s*$/.test(contextBefore)) {
|
|
369
|
+
completions = [...TJS_TYPES]
|
|
370
|
+
}
|
|
371
|
+
// General completions
|
|
372
|
+
else {
|
|
373
|
+
completions = [
|
|
374
|
+
...TJS_KEYWORDS,
|
|
375
|
+
...RUNTIME_COMPLETIONS,
|
|
376
|
+
...extractFunctions(source),
|
|
377
|
+
...extractVariables(source, position),
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
// Add metadata-based completions
|
|
381
|
+
if (metadata) {
|
|
382
|
+
completions.push(...completionsFromMetadata(metadata))
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add import-based completions
|
|
386
|
+
if (imports) {
|
|
387
|
+
for (const [moduleName, moduleMeta] of Object.entries(imports)) {
|
|
388
|
+
completions.push(...completionsFromMetadata(moduleMeta))
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add test-specific completions if in test block
|
|
393
|
+
if (isInTestBlock(source, position)) {
|
|
394
|
+
completions.push({
|
|
395
|
+
label: 'expect',
|
|
396
|
+
kind: 'function',
|
|
397
|
+
detail: '(actual: any) -> Matchers',
|
|
398
|
+
documentation: 'Create test assertion',
|
|
399
|
+
snippet: 'expect(${1:actual}).toBe(${2:expected})',
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Filter by prefix
|
|
405
|
+
if (word) {
|
|
406
|
+
const lowerWord = word.toLowerCase()
|
|
407
|
+
completions = completions.filter((c) =>
|
|
408
|
+
c.label.toLowerCase().startsWith(lowerWord)
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sort: exact match first, then by kind, then alphabetically
|
|
413
|
+
completions.sort((a, b) => {
|
|
414
|
+
// Exact match first
|
|
415
|
+
if (a.label === word) return -1
|
|
416
|
+
if (b.label === word) return 1
|
|
417
|
+
|
|
418
|
+
// Then by kind priority
|
|
419
|
+
const kindPriority: Record<string, number> = {
|
|
420
|
+
function: 0,
|
|
421
|
+
variable: 1,
|
|
422
|
+
property: 2,
|
|
423
|
+
keyword: 3,
|
|
424
|
+
type: 4,
|
|
425
|
+
}
|
|
426
|
+
const aPriority = kindPriority[a.kind] ?? 5
|
|
427
|
+
const bPriority = kindPriority[b.kind] ?? 5
|
|
428
|
+
if (aPriority !== bPriority) return aPriority - bPriority
|
|
429
|
+
|
|
430
|
+
// Then alphabetically
|
|
431
|
+
return a.label.localeCompare(b.label)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
return completions
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get signature help for function calls
|
|
439
|
+
*/
|
|
440
|
+
export function getSignatureHelp(
|
|
441
|
+
ctx: CompletionContext
|
|
442
|
+
): { signature: string; activeParam: number; params: string[] } | null {
|
|
443
|
+
const { source, position, metadata } = ctx
|
|
444
|
+
|
|
445
|
+
// Find the function call we're in
|
|
446
|
+
const before = source.slice(0, position)
|
|
447
|
+
const callMatch = before.match(/(\w+)\s*\(([^)]*)$/)
|
|
448
|
+
|
|
449
|
+
if (!callMatch) return null
|
|
450
|
+
|
|
451
|
+
const [, funcName, argsSoFar] = callMatch
|
|
452
|
+
|
|
453
|
+
// Count commas to determine active parameter
|
|
454
|
+
const activeParam = (argsSoFar.match(/,/g) || []).length
|
|
455
|
+
|
|
456
|
+
// Look up function metadata
|
|
457
|
+
const meta = metadata?.[funcName]
|
|
458
|
+
if (meta?.params) {
|
|
459
|
+
const params = Object.entries(meta.params).map(([name, info]) => {
|
|
460
|
+
const req = info.required ? '' : '?'
|
|
461
|
+
return `${name}${req}: ${info.type}`
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
signature: `${funcName}(${params.join(', ')})`,
|
|
466
|
+
activeParam,
|
|
467
|
+
params,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check runtime functions
|
|
472
|
+
const runtimeFunc = RUNTIME_COMPLETIONS.find((c) => c.label === funcName)
|
|
473
|
+
if (runtimeFunc?.detail) {
|
|
474
|
+
return {
|
|
475
|
+
signature: `${funcName}${runtimeFunc.detail}`,
|
|
476
|
+
activeParam,
|
|
477
|
+
params: [],
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return null
|
|
482
|
+
}
|