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,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocomplete Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests autocomplete behavior with real TJS code in various states.
|
|
5
|
+
* Uses transpilation to get metadata, then verifies completions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'bun:test'
|
|
9
|
+
import { tjs } from '../../src/lang'
|
|
10
|
+
|
|
11
|
+
// Import the completion source internals for testing
|
|
12
|
+
// We'll test the logic directly rather than through CodeMirror
|
|
13
|
+
|
|
14
|
+
interface CompletionContext {
|
|
15
|
+
source: string
|
|
16
|
+
position: number // cursor position (use | in source to mark it)
|
|
17
|
+
metadata?: Record<string, any>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Completion {
|
|
21
|
+
label: string
|
|
22
|
+
type: string
|
|
23
|
+
detail?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper: parse source with | marking cursor position
|
|
27
|
+
function parseSource(sourceWithCursor: string): {
|
|
28
|
+
source: string
|
|
29
|
+
position: number
|
|
30
|
+
} {
|
|
31
|
+
const position = sourceWithCursor.indexOf('|')
|
|
32
|
+
if (position === -1) {
|
|
33
|
+
return { source: sourceWithCursor, position: sourceWithCursor.length }
|
|
34
|
+
}
|
|
35
|
+
const source =
|
|
36
|
+
sourceWithCursor.slice(0, position) + sourceWithCursor.slice(position + 1)
|
|
37
|
+
return { source, position }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Helper: transpile and extract metadata
|
|
41
|
+
function getMetadata(source: string): Record<string, any> | undefined {
|
|
42
|
+
try {
|
|
43
|
+
const result = tjs(source)
|
|
44
|
+
// tjs returns { code, types, metadata, ... }
|
|
45
|
+
return result?.metadata
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper: get object name before a dot
|
|
52
|
+
function getObjectBeforeDot(source: string, dotPos: number): string | null {
|
|
53
|
+
const before = source.slice(0, dotPos)
|
|
54
|
+
const match = before.match(/(\w+)\s*$/)
|
|
55
|
+
return match ? match[1] : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Curated property completions (simplified version of ajs-language.ts)
|
|
59
|
+
const PROPERTY_COMPLETIONS: Record<string, Completion[]> = {
|
|
60
|
+
console: [
|
|
61
|
+
{ label: 'log', type: 'method', detail: '(...args: any[]) -> void' },
|
|
62
|
+
{ label: 'error', type: 'method', detail: '(...args: any[]) -> void' },
|
|
63
|
+
{ label: 'warn', type: 'method', detail: '(...args: any[]) -> void' },
|
|
64
|
+
{ label: 'info', type: 'method', detail: '(...args: any[]) -> void' },
|
|
65
|
+
{ label: 'debug', type: 'method', detail: '(...args: any[]) -> void' },
|
|
66
|
+
{ label: 'table', type: 'method', detail: '(data: any) -> void' },
|
|
67
|
+
{ label: 'clear', type: 'method', detail: '() -> void' },
|
|
68
|
+
],
|
|
69
|
+
Math: [
|
|
70
|
+
{ label: 'floor', type: 'method', detail: '(x: number) -> number' },
|
|
71
|
+
{ label: 'ceil', type: 'method', detail: '(x: number) -> number' },
|
|
72
|
+
{ label: 'round', type: 'method', detail: '(x: number) -> number' },
|
|
73
|
+
{ label: 'abs', type: 'method', detail: '(x: number) -> number' },
|
|
74
|
+
{ label: 'min', type: 'method', detail: '(...values: number[]) -> number' },
|
|
75
|
+
{ label: 'max', type: 'method', detail: '(...values: number[]) -> number' },
|
|
76
|
+
{ label: 'sqrt', type: 'method', detail: '(x: number) -> number' },
|
|
77
|
+
{ label: 'pow', type: 'method', detail: '(base, exp) -> number' },
|
|
78
|
+
{ label: 'random', type: 'method', detail: '() -> number' },
|
|
79
|
+
{ label: 'PI', type: 'property', detail: 'number' },
|
|
80
|
+
{ label: 'E', type: 'property', detail: 'number' },
|
|
81
|
+
],
|
|
82
|
+
JSON: [
|
|
83
|
+
{ label: 'parse', type: 'method', detail: '(text: string) -> any' },
|
|
84
|
+
{ label: 'stringify', type: 'method', detail: '(value: any) -> string' },
|
|
85
|
+
],
|
|
86
|
+
Object: [
|
|
87
|
+
{ label: 'keys', type: 'method', detail: '(obj: object) -> string[]' },
|
|
88
|
+
{ label: 'values', type: 'method', detail: '(obj: object) -> any[]' },
|
|
89
|
+
{
|
|
90
|
+
label: 'entries',
|
|
91
|
+
type: 'method',
|
|
92
|
+
detail: '(obj: object) -> [string, any][]',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'assign',
|
|
96
|
+
type: 'method',
|
|
97
|
+
detail: '(target, ...sources) -> object',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
Array: [
|
|
101
|
+
{ label: 'isArray', type: 'method', detail: '(value: any) -> boolean' },
|
|
102
|
+
{ label: 'from', type: 'method', detail: '(iterable) -> any[]' },
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper: get property completions for an object
|
|
107
|
+
function getPropertyCompletions(objName: string): Completion[] {
|
|
108
|
+
return PROPERTY_COMPLETIONS[objName] || []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Simplified completion logic for testing (mirrors ajs-language.ts)
|
|
112
|
+
function getCompletions(ctx: CompletionContext): Completion[] {
|
|
113
|
+
const { source, position, metadata } = ctx
|
|
114
|
+
|
|
115
|
+
// Get word at cursor
|
|
116
|
+
let wordStart = position
|
|
117
|
+
while (wordStart > 0 && /[\w$]/.test(source[wordStart - 1])) {
|
|
118
|
+
wordStart--
|
|
119
|
+
}
|
|
120
|
+
const word = source.slice(wordStart, position)
|
|
121
|
+
|
|
122
|
+
// Get context before word
|
|
123
|
+
const charBefore = source.slice(Math.max(0, wordStart - 1), wordStart)
|
|
124
|
+
const lineStart = source.lastIndexOf('\n', wordStart - 1) + 1
|
|
125
|
+
const lineBefore = source.slice(lineStart, wordStart)
|
|
126
|
+
|
|
127
|
+
let completions: Completion[] = []
|
|
128
|
+
|
|
129
|
+
// After . - property access
|
|
130
|
+
if (charBefore === '.') {
|
|
131
|
+
const before = source.slice(Math.max(0, wordStart - 50), wordStart)
|
|
132
|
+
if (/expect\s*\([^)]*\)\s*\.$/.test(before)) {
|
|
133
|
+
completions = [
|
|
134
|
+
{ label: 'toBe', type: 'method', detail: '(expected: any)' },
|
|
135
|
+
{ label: 'toEqual', type: 'method', detail: '(expected: any)' },
|
|
136
|
+
{ label: 'toContain', type: 'method', detail: '(item: any)' },
|
|
137
|
+
{ label: 'toThrow', type: 'method', detail: '()' },
|
|
138
|
+
{ label: 'toBeTruthy', type: 'method', detail: '()' },
|
|
139
|
+
{ label: 'toBeFalsy', type: 'method', detail: '()' },
|
|
140
|
+
]
|
|
141
|
+
} else {
|
|
142
|
+
// Property completions for known globals
|
|
143
|
+
const objName = getObjectBeforeDot(source, wordStart - 1)
|
|
144
|
+
if (objName) {
|
|
145
|
+
completions = getPropertyCompletions(objName)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// After : - type context
|
|
150
|
+
else if (/:\s*$/.test(lineBefore)) {
|
|
151
|
+
completions = [
|
|
152
|
+
{ label: "''", type: 'type', detail: 'String type' },
|
|
153
|
+
{ label: '0', type: 'type', detail: 'Number type' },
|
|
154
|
+
{ label: 'true', type: 'type', detail: 'Boolean type' },
|
|
155
|
+
{ label: 'null', type: 'type', detail: 'Null type' },
|
|
156
|
+
{ label: '[]', type: 'type', detail: 'Array type' },
|
|
157
|
+
{ label: '{}', type: 'type', detail: 'Object type' },
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
// After -> - return type context
|
|
161
|
+
else if (/->\s*$/.test(lineBefore)) {
|
|
162
|
+
completions = [
|
|
163
|
+
{ label: "''", type: 'type', detail: 'String type' },
|
|
164
|
+
{ label: '0', type: 'type', detail: 'Number type' },
|
|
165
|
+
{ label: 'true', type: 'type', detail: 'Boolean type' },
|
|
166
|
+
{ label: '{}', type: 'type', detail: 'Object type' },
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
// General context
|
|
170
|
+
else {
|
|
171
|
+
// Keywords
|
|
172
|
+
completions = [
|
|
173
|
+
{ label: 'function', type: 'keyword' },
|
|
174
|
+
{ label: 'const', type: 'keyword' },
|
|
175
|
+
{ label: 'let', type: 'keyword' },
|
|
176
|
+
{ label: 'if', type: 'keyword' },
|
|
177
|
+
{ label: 'else', type: 'keyword' },
|
|
178
|
+
{ label: 'while', type: 'keyword' },
|
|
179
|
+
{ label: 'for', type: 'keyword' },
|
|
180
|
+
{ label: 'return', type: 'keyword' },
|
|
181
|
+
{ label: 'test', type: 'keyword' },
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
// Global objects
|
|
185
|
+
completions.push(
|
|
186
|
+
{ label: 'console', type: 'variable' },
|
|
187
|
+
{ label: 'Math', type: 'variable' },
|
|
188
|
+
{ label: 'JSON', type: 'variable' },
|
|
189
|
+
{ label: 'Array', type: 'class' },
|
|
190
|
+
{ label: 'Object', type: 'class' },
|
|
191
|
+
{ label: 'String', type: 'class' },
|
|
192
|
+
{ label: 'Number', type: 'class' },
|
|
193
|
+
{ label: 'Date', type: 'class' },
|
|
194
|
+
{ label: 'parseInt', type: 'function' },
|
|
195
|
+
{ label: 'parseFloat', type: 'function' }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Extract functions from source
|
|
199
|
+
const funcRegex = /function\s+(\w+)\s*\(([^)]*)\)/g
|
|
200
|
+
let match
|
|
201
|
+
while ((match = funcRegex.exec(source)) !== null) {
|
|
202
|
+
const [, name, params] = match
|
|
203
|
+
completions.push({ label: name, type: 'function', detail: `(${params})` })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract variables before cursor
|
|
207
|
+
const before = source.slice(0, position)
|
|
208
|
+
const varRegex = /(?:const|let)\s+(\w+)\s*=/g
|
|
209
|
+
while ((match = varRegex.exec(before)) !== null) {
|
|
210
|
+
completions.push({ label: match[1], type: 'variable' })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add from metadata if available
|
|
214
|
+
if (metadata) {
|
|
215
|
+
const params = metadata.params
|
|
216
|
+
? Object.keys(metadata.params).join(', ')
|
|
217
|
+
: ''
|
|
218
|
+
const returnType = metadata.returns?.type || 'any'
|
|
219
|
+
if (metadata.name) {
|
|
220
|
+
completions.push({
|
|
221
|
+
label: metadata.name,
|
|
222
|
+
type: 'function',
|
|
223
|
+
detail: `(${params}) -> ${returnType}`,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Filter by prefix
|
|
230
|
+
if (word) {
|
|
231
|
+
const lowerWord = word.toLowerCase()
|
|
232
|
+
completions = completions.filter((c) =>
|
|
233
|
+
c.label.toLowerCase().startsWith(lowerWord)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return completions
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
describe('Autocomplete', () => {
|
|
241
|
+
describe('Type context (after :)', () => {
|
|
242
|
+
it('suggests types after colon in parameter', () => {
|
|
243
|
+
const { source, position } = parseSource('function foo(x: |)')
|
|
244
|
+
const completions = getCompletions({ source, position })
|
|
245
|
+
|
|
246
|
+
expect(completions.some((c) => c.label === "''")).toBe(true)
|
|
247
|
+
expect(completions.some((c) => c.label === '0')).toBe(true)
|
|
248
|
+
expect(completions.some((c) => c.label === 'true')).toBe(true)
|
|
249
|
+
expect(completions.every((c) => c.type === 'type')).toBe(true)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('suggests types after colon with space', () => {
|
|
253
|
+
const { source, position } = parseSource('function foo(name: |)')
|
|
254
|
+
const completions = getCompletions({ source, position })
|
|
255
|
+
|
|
256
|
+
expect(completions.length).toBeGreaterThan(0)
|
|
257
|
+
expect(completions.every((c) => c.type === 'type')).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('Return type context (after ->)', () => {
|
|
262
|
+
it('suggests types after arrow', () => {
|
|
263
|
+
const { source, position } = parseSource('function foo(x: 0) -> |')
|
|
264
|
+
const completions = getCompletions({ source, position })
|
|
265
|
+
|
|
266
|
+
expect(completions.some((c) => c.label === '{}')).toBe(true)
|
|
267
|
+
expect(completions.every((c) => c.type === 'type')).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('General context', () => {
|
|
272
|
+
it('suggests keywords at start of file', () => {
|
|
273
|
+
const { source, position } = parseSource('|')
|
|
274
|
+
const completions = getCompletions({ source, position })
|
|
275
|
+
|
|
276
|
+
expect(completions.some((c) => c.label === 'function')).toBe(true)
|
|
277
|
+
expect(completions.some((c) => c.label === 'const')).toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('suggests keywords with prefix', () => {
|
|
281
|
+
const { source, position } = parseSource('func|')
|
|
282
|
+
const completions = getCompletions({ source, position })
|
|
283
|
+
|
|
284
|
+
expect(completions.some((c) => c.label === 'function')).toBe(true)
|
|
285
|
+
expect(completions.every((c) => c.label.startsWith('func'))).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('suggests global objects like console', () => {
|
|
289
|
+
const { source, position } = parseSource('con|')
|
|
290
|
+
const completions = getCompletions({ source, position })
|
|
291
|
+
|
|
292
|
+
expect(completions.some((c) => c.label === 'console')).toBe(true)
|
|
293
|
+
expect(completions.some((c) => c.label === 'const')).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('suggests Math, JSON, and other globals', () => {
|
|
297
|
+
const { source, position } = parseSource('|')
|
|
298
|
+
const completions = getCompletions({ source, position })
|
|
299
|
+
|
|
300
|
+
expect(completions.some((c) => c.label === 'Math')).toBe(true)
|
|
301
|
+
expect(completions.some((c) => c.label === 'JSON')).toBe(true)
|
|
302
|
+
expect(completions.some((c) => c.label === 'Array')).toBe(true)
|
|
303
|
+
expect(completions.some((c) => c.label === 'Object')).toBe(true)
|
|
304
|
+
expect(completions.some((c) => c.label === 'parseInt')).toBe(true)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('suggests declared functions', () => {
|
|
308
|
+
const { source, position } = parseSource(`
|
|
309
|
+
function greet(name: '') {
|
|
310
|
+
return 'Hello ' + name
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
gr|
|
|
314
|
+
`)
|
|
315
|
+
const completions = getCompletions({ source, position })
|
|
316
|
+
|
|
317
|
+
expect(
|
|
318
|
+
completions.some((c) => c.label === 'greet' && c.type === 'function')
|
|
319
|
+
).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('suggests declared variables', () => {
|
|
323
|
+
const { source, position } = parseSource(`
|
|
324
|
+
const message = 'hello'
|
|
325
|
+
let count = 0
|
|
326
|
+
|
|
327
|
+
me|
|
|
328
|
+
`)
|
|
329
|
+
const completions = getCompletions({ source, position })
|
|
330
|
+
|
|
331
|
+
expect(
|
|
332
|
+
completions.some((c) => c.label === 'message' && c.type === 'variable')
|
|
333
|
+
).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('does not suggest variables declared after cursor', () => {
|
|
337
|
+
const { source, position } = parseSource(`
|
|
338
|
+
const before = 1
|
|
339
|
+
|
|
|
340
|
+
const after = 2
|
|
341
|
+
`)
|
|
342
|
+
const completions = getCompletions({ source, position })
|
|
343
|
+
|
|
344
|
+
expect(completions.some((c) => c.label === 'before')).toBe(true)
|
|
345
|
+
expect(completions.some((c) => c.label === 'after')).toBe(false)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('Expect matchers (after expect().)', () => {
|
|
350
|
+
it('suggests matchers after expect().', () => {
|
|
351
|
+
const { source, position } = parseSource('expect(value).|')
|
|
352
|
+
const completions = getCompletions({ source, position })
|
|
353
|
+
|
|
354
|
+
expect(completions.some((c) => c.label === 'toBe')).toBe(true)
|
|
355
|
+
expect(completions.some((c) => c.label === 'toEqual')).toBe(true)
|
|
356
|
+
expect(completions.some((c) => c.label === 'toThrow')).toBe(true)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('filters matchers by prefix', () => {
|
|
360
|
+
const { source, position } = parseSource('expect(x).toB|')
|
|
361
|
+
const completions = getCompletions({ source, position })
|
|
362
|
+
|
|
363
|
+
expect(completions.every((c) => c.label.startsWith('toB'))).toBe(true)
|
|
364
|
+
expect(completions.some((c) => c.label === 'toBe')).toBe(true)
|
|
365
|
+
expect(completions.some((c) => c.label === 'toBeTruthy')).toBe(true)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
describe('Property completions (after object.)', () => {
|
|
370
|
+
it('suggests console methods after console.', () => {
|
|
371
|
+
const { source, position } = parseSource('console.|')
|
|
372
|
+
const completions = getCompletions({ source, position })
|
|
373
|
+
|
|
374
|
+
expect(completions.some((c) => c.label === 'log')).toBe(true)
|
|
375
|
+
expect(completions.some((c) => c.label === 'error')).toBe(true)
|
|
376
|
+
expect(completions.some((c) => c.label === 'warn')).toBe(true)
|
|
377
|
+
expect(completions.every((c) => c.type === 'method')).toBe(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('suggests Math methods and constants after Math.', () => {
|
|
381
|
+
const { source, position } = parseSource('Math.|')
|
|
382
|
+
const completions = getCompletions({ source, position })
|
|
383
|
+
|
|
384
|
+
expect(completions.some((c) => c.label === 'floor')).toBe(true)
|
|
385
|
+
expect(completions.some((c) => c.label === 'ceil')).toBe(true)
|
|
386
|
+
expect(completions.some((c) => c.label === 'random')).toBe(true)
|
|
387
|
+
expect(
|
|
388
|
+
completions.some((c) => c.label === 'PI' && c.type === 'property')
|
|
389
|
+
).toBe(true)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('suggests JSON methods after JSON.', () => {
|
|
393
|
+
const { source, position } = parseSource('JSON.|')
|
|
394
|
+
const completions = getCompletions({ source, position })
|
|
395
|
+
|
|
396
|
+
expect(completions.some((c) => c.label === 'parse')).toBe(true)
|
|
397
|
+
expect(completions.some((c) => c.label === 'stringify')).toBe(true)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('suggests Object methods after Object.', () => {
|
|
401
|
+
const { source, position } = parseSource('Object.|')
|
|
402
|
+
const completions = getCompletions({ source, position })
|
|
403
|
+
|
|
404
|
+
expect(completions.some((c) => c.label === 'keys')).toBe(true)
|
|
405
|
+
expect(completions.some((c) => c.label === 'values')).toBe(true)
|
|
406
|
+
expect(completions.some((c) => c.label === 'entries')).toBe(true)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('filters property completions by prefix', () => {
|
|
410
|
+
const { source, position } = parseSource('console.lo|')
|
|
411
|
+
const completions = getCompletions({ source, position })
|
|
412
|
+
|
|
413
|
+
expect(completions.some((c) => c.label === 'log')).toBe(true)
|
|
414
|
+
// Should filter to only those starting with 'lo'
|
|
415
|
+
expect(completions.every((c) => c.label.startsWith('lo'))).toBe(true)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('returns empty for unknown objects', () => {
|
|
419
|
+
const { source, position } = parseSource('unknownObj.|')
|
|
420
|
+
const completions = getCompletions({ source, position })
|
|
421
|
+
|
|
422
|
+
expect(completions.length).toBe(0)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('With transpiled metadata', () => {
|
|
427
|
+
it('extracts function signature from transpiled code', () => {
|
|
428
|
+
const source = `function add(a: 0, b: 0) -> 0 {
|
|
429
|
+
return a + b
|
|
430
|
+
}`
|
|
431
|
+
const metadata = getMetadata(source)
|
|
432
|
+
|
|
433
|
+
expect(metadata).toBeDefined()
|
|
434
|
+
// metadata is now keyed by function name
|
|
435
|
+
expect(metadata?.add?.params?.a?.type?.kind).toBe('number')
|
|
436
|
+
expect(metadata?.add?.params?.b?.type?.kind).toBe('number')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('extracts example-based types', () => {
|
|
440
|
+
const source = `function greet(name: 'World', times = 1) -! '' {
|
|
441
|
+
return 'Hello ' + name
|
|
442
|
+
}`
|
|
443
|
+
const metadata = getMetadata(source)
|
|
444
|
+
|
|
445
|
+
expect(metadata).toBeDefined()
|
|
446
|
+
// metadata is now keyed by function name
|
|
447
|
+
expect(metadata?.greet?.params?.name?.type?.kind).toBe('string')
|
|
448
|
+
expect(metadata?.greet?.params?.times?.type?.kind).toBe('number')
|
|
449
|
+
expect(metadata?.greet?.params?.times?.default).toBe(1)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('handles object return types', () => {
|
|
453
|
+
const source = `function createUser(name: '', age: 0) -> { name: '', age: 0 } {
|
|
454
|
+
return { name, age }
|
|
455
|
+
}`
|
|
456
|
+
const metadata = getMetadata(source)
|
|
457
|
+
|
|
458
|
+
expect(metadata).toBeDefined()
|
|
459
|
+
// metadata is now keyed by function name
|
|
460
|
+
expect(metadata?.createUser?.returns?.kind).toBe('object')
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
describe('Real-world code examples', () => {
|
|
465
|
+
it('handles incomplete function definition', () => {
|
|
466
|
+
const { source, position } = parseSource(`
|
|
467
|
+
function processOrder(order: |
|
|
468
|
+
`)
|
|
469
|
+
const completions = getCompletions({ source, position })
|
|
470
|
+
|
|
471
|
+
// Should suggest types even with incomplete syntax
|
|
472
|
+
expect(completions.some((c) => c.type === 'type')).toBe(true)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('handles code mid-edit', () => {
|
|
476
|
+
const { source, position } = parseSource(`
|
|
477
|
+
function ship(to: '12345', quantity = 1) {
|
|
478
|
+
const result = httpFetch({ url: '/api/ship' })
|
|
479
|
+
ret|
|
|
480
|
+
}
|
|
481
|
+
`)
|
|
482
|
+
const completions = getCompletions({ source, position })
|
|
483
|
+
|
|
484
|
+
// typing 'ret' should suggest 'return' (starts with 'ret')
|
|
485
|
+
expect(completions.some((c) => c.label === 'return')).toBe(true)
|
|
486
|
+
// 'result' doesn't start with 'ret', so it shouldn't be suggested
|
|
487
|
+
expect(completions.some((c) => c.label === 'result')).toBe(false)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('suggests variables when typing their prefix', () => {
|
|
491
|
+
const { source, position } = parseSource(`
|
|
492
|
+
function ship(to: '12345', quantity = 1) {
|
|
493
|
+
const result = httpFetch({ url: '/api/ship' })
|
|
494
|
+
res|
|
|
495
|
+
}
|
|
496
|
+
`)
|
|
497
|
+
const completions = getCompletions({ source, position })
|
|
498
|
+
|
|
499
|
+
// typing 'res' should suggest 'result'
|
|
500
|
+
expect(completions.some((c) => c.label === 'result')).toBe(true)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('suggests variables declared in same scope', () => {
|
|
504
|
+
const { source, position } = parseSource(`
|
|
505
|
+
const x = 1
|
|
506
|
+
const y = 2
|
|
507
|
+
|
|
|
508
|
+
`)
|
|
509
|
+
const completions = getCompletions({ source, position })
|
|
510
|
+
|
|
511
|
+
expect(completions.some((c) => c.label === 'x')).toBe(true)
|
|
512
|
+
expect(completions.some((c) => c.label === 'y')).toBe(true)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('handles test block context', () => {
|
|
516
|
+
const { source, position } = parseSource(`
|
|
517
|
+
function add(a: 0, b: 0) {
|
|
518
|
+
return a + b
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
test('add works') {
|
|
522
|
+
const result = add(1, 2)
|
|
523
|
+
expect(result).|
|
|
524
|
+
}
|
|
525
|
+
`)
|
|
526
|
+
const completions = getCompletions({ source, position })
|
|
527
|
+
|
|
528
|
+
expect(completions.some((c) => c.label === 'toBe')).toBe(true)
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
})
|