tjs-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
package/bin/docs.js ADDED
@@ -0,0 +1,170 @@
1
+ /*
2
+ * docs.js - Documentation extractor for agent-99
3
+ *
4
+ * Scans source files for /*# ... *​/ markdown blocks and .md files,
5
+ * outputs demo/docs.json for the documentation site.
6
+ *
7
+ * Adapted from tosijs-ui's docs.js
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
11
+ import { join, extname, basename } from 'path'
12
+
13
+ const TRIM_REGEX = /^#+ |`/g
14
+
15
+ function metadata(content, filePath) {
16
+ let source = content.match(/<\!\-\-(\{.*\})\-\->|\/\*(\{.*\})\*\//)
17
+ let data = {}
18
+ if (source) {
19
+ try {
20
+ data = JSON.parse(source[1] || source[2])
21
+ } catch (e) {
22
+ console.error('bad metadata in doc', filePath)
23
+ }
24
+ }
25
+ return data
26
+ }
27
+
28
+ // Section order for navigation hierarchy
29
+ const SECTION_ORDER = {
30
+ home: 0,
31
+ meta: 1,
32
+ tjs: 2,
33
+ ajs: 3,
34
+ }
35
+
36
+ function hierarchicalSort(a, b) {
37
+ // First sort by section
38
+ const sectionA = SECTION_ORDER[a.section] ?? 99
39
+ const sectionB = SECTION_ORDER[b.section] ?? 99
40
+ if (sectionA !== sectionB) return sectionA - sectionB
41
+
42
+ // Then by group (docs before others)
43
+ const groupA = a.group || ''
44
+ const groupB = b.group || ''
45
+ if (groupA !== groupB) return groupA.localeCompare(groupB)
46
+
47
+ // Then by order
48
+ const orderA = a.order ?? 99
49
+ const orderB = b.order ?? 99
50
+ if (orderA !== orderB) return orderA - orderB
51
+
52
+ // Finally alphabetically by title
53
+ return a.title.localeCompare(b.title)
54
+ }
55
+
56
+ function findMarkdownFiles(dirs, ignore) {
57
+ let markdownFiles = []
58
+
59
+ function traverseDirectory(dir, ignore) {
60
+ const files = readdirSync(dir)
61
+ if (ignore.includes(basename(dir))) {
62
+ return
63
+ }
64
+
65
+ files.forEach((file) => {
66
+ const filePath = join(dir, file)
67
+
68
+ // Skip if in ignore list
69
+ if (ignore.includes(file)) {
70
+ return
71
+ }
72
+
73
+ const stats = statSync(filePath)
74
+
75
+ if (stats.isDirectory()) {
76
+ traverseDirectory(filePath, ignore)
77
+ } else if (extname(file) === '.md') {
78
+ const content = readFileSync(filePath, 'utf8')
79
+ // Find the first heading line (skip metadata comments)
80
+ const lines = content.split('\n')
81
+ let titleLine = lines.find((line) => line.startsWith('#')) || lines[0]
82
+ markdownFiles.push({
83
+ text: content,
84
+ title: titleLine.replace(TRIM_REGEX, ''),
85
+ filename: file,
86
+ path: filePath,
87
+ ...metadata(content, filePath),
88
+ })
89
+ } else if (['.ts', '.js'].includes(extname(file))) {
90
+ const content = readFileSync(filePath, 'utf8')
91
+ // Match /*# ... */ blocks (inline markdown documentation)
92
+ const docs = content.match(/\/\*#[\s\S]+?\*\//g) || []
93
+ if (docs.length) {
94
+ const markdown = docs.map((s) => s.substring(3, s.length - 2).trim())
95
+ const text = markdown.join('\n\n---\n\n')
96
+ // Use filename as title for source files (more descriptive than first heading)
97
+ const fileTitle = basename(file, extname(file))
98
+ markdownFiles.push({
99
+ text,
100
+ title: `${fileTitle} (inline docs)`,
101
+ filename: file,
102
+ path: filePath,
103
+ ...metadata(content, filePath),
104
+ })
105
+ }
106
+ }
107
+ })
108
+ }
109
+
110
+ dirs.forEach((dir) => {
111
+ traverseDirectory(dir, ignore)
112
+ })
113
+
114
+ return markdownFiles.sort(hierarchicalSort)
115
+ }
116
+
117
+ function saveAsJSON(data, outputFilePath) {
118
+ const jsonData = JSON.stringify(data, null, 2)
119
+ writeFileSync(outputFilePath, jsonData, 'utf8')
120
+ console.log(`Generated ${outputFilePath} with ${data.length} documents`)
121
+ }
122
+
123
+ // Directories to ignore
124
+ const ignore = [
125
+ 'node_modules',
126
+ 'dist',
127
+ 'docs',
128
+ 'third-party',
129
+ '.git',
130
+ '.archive',
131
+ 'editors',
132
+ 'demo',
133
+ 'bin',
134
+ ]
135
+
136
+ // Directories to search
137
+ const directoriesToSearch = ['.']
138
+
139
+ // Dedupe by normalized path
140
+ function dedupeByPath(docs) {
141
+ const seen = new Map()
142
+ for (const doc of docs) {
143
+ // Normalize path to avoid duplicates from different starting points
144
+ const normalizedPath = doc.path.replace(/^\.\//, '')
145
+ if (!seen.has(normalizedPath)) {
146
+ seen.set(normalizedPath, doc)
147
+ }
148
+ }
149
+ return Array.from(seen.values())
150
+ }
151
+
152
+ // Find all documentation and dedupe
153
+ const markdownFiles = dedupeByPath(
154
+ findMarkdownFiles(directoriesToSearch, ignore)
155
+ )
156
+
157
+ // Save to demo/docs.json
158
+ const outputPath = './demo/docs.json'
159
+ saveAsJSON(markdownFiles, outputPath)
160
+
161
+ // List what was found
162
+ console.log('\nDocuments found:')
163
+ markdownFiles.forEach((doc, i) => {
164
+ const section = doc.section ? `[${doc.section}]` : ''
165
+ const group = doc.group ? `/${doc.group}` : ''
166
+ const navTitle = doc.navTitle ? ` → "${doc.navTitle}"` : ''
167
+ console.log(
168
+ ` ${i + 1}. ${section}${group} ${doc.title}${navTitle} (${doc.filename})`
169
+ )
170
+ })
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ #
3
+ # Install AsyncJS syntax highlighting for Cursor
4
+ #
5
+ # Usage: npx ajs-install-cursor
6
+ # or: ./node_modules/.bin/ajs-install-cursor
7
+
8
+ set -e
9
+
10
+ # Find the real package directory (resolve symlinks for npx compatibility)
11
+ SCRIPT_PATH="${BASH_SOURCE[0]}"
12
+ # Follow symlinks to get the real path
13
+ while [ -L "$SCRIPT_PATH" ]; do
14
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
15
+ SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
16
+ # Handle relative symlinks
17
+ [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
18
+ done
19
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
20
+ PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
21
+ EXTENSION_SRC="$PACKAGE_DIR/editors/vscode"
22
+
23
+ # Determine Cursor extensions directory
24
+ if [[ "$OSTYPE" == "darwin"* ]]; then
25
+ CURSOR_EXT_DIR="$HOME/.cursor/extensions"
26
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
27
+ CURSOR_EXT_DIR="$HOME/.cursor/extensions"
28
+ elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
29
+ CURSOR_EXT_DIR="$APPDATA/Cursor/User/extensions"
30
+ else
31
+ echo "Unknown OS: $OSTYPE"
32
+ echo "Please manually copy $EXTENSION_SRC to your Cursor extensions directory"
33
+ exit 1
34
+ fi
35
+
36
+ TARGET_DIR="$CURSOR_EXT_DIR/tosijs-ajs-0.1.0"
37
+
38
+ echo "Installing AsyncJS syntax highlighting for Cursor..."
39
+ echo " Source: $EXTENSION_SRC"
40
+ echo " Target: $TARGET_DIR"
41
+
42
+ # Check source exists
43
+ if [ ! -d "$EXTENSION_SRC" ]; then
44
+ echo "Error: Extension source not found at $EXTENSION_SRC"
45
+ echo ""
46
+ echo "If you installed via npm/npx, try running from your project directory:"
47
+ echo " ./node_modules/tjs-lang/bin/install-cursor.sh"
48
+ exit 1
49
+ fi
50
+
51
+ # Create extensions directory if needed
52
+ mkdir -p "$CURSOR_EXT_DIR"
53
+
54
+ # Remove old version if exists
55
+ if [ -d "$TARGET_DIR" ]; then
56
+ echo " Removing old version..."
57
+ rm -rf "$TARGET_DIR"
58
+ fi
59
+
60
+ # Copy extension
61
+ cp -r "$EXTENSION_SRC" "$TARGET_DIR"
62
+
63
+ echo ""
64
+ echo "Installation complete!"
65
+ echo ""
66
+ echo "Please restart Cursor to enable AsyncJS syntax highlighting."
67
+ echo ""
68
+ echo "Features:"
69
+ echo " - Syntax highlighting for .ajs files"
70
+ echo " - Embedded highlighting in ajs\`...\` template literals"
71
+ echo " - Error highlighting for forbidden syntax (new, class, async, etc.)"
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ #
3
+ # Install AsyncJS syntax highlighting for VS Code
4
+ #
5
+ # Usage: npx ajs-install-vscode
6
+ # or: ./node_modules/.bin/ajs-install-vscode
7
+
8
+ set -e
9
+
10
+ # Find the real package directory (resolve symlinks for npx compatibility)
11
+ SCRIPT_PATH="${BASH_SOURCE[0]}"
12
+ # Follow symlinks to get the real path
13
+ while [ -L "$SCRIPT_PATH" ]; do
14
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
15
+ SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
16
+ # Handle relative symlinks
17
+ [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH"
18
+ done
19
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
20
+ PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
21
+ EXTENSION_SRC="$PACKAGE_DIR/editors/vscode"
22
+
23
+ # Determine VS Code extensions directory
24
+ if [[ "$OSTYPE" == "darwin"* ]]; then
25
+ VSCODE_EXT_DIR="$HOME/.vscode/extensions"
26
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
27
+ VSCODE_EXT_DIR="$HOME/.vscode/extensions"
28
+ elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
29
+ VSCODE_EXT_DIR="$APPDATA/Code/User/extensions"
30
+ else
31
+ echo "Unknown OS: $OSTYPE"
32
+ echo "Please manually copy $EXTENSION_SRC to your VS Code extensions directory"
33
+ exit 1
34
+ fi
35
+
36
+ TARGET_DIR="$VSCODE_EXT_DIR/tosijs-ajs-0.1.0"
37
+
38
+ echo "Installing AsyncJS syntax highlighting for VS Code..."
39
+ echo " Source: $EXTENSION_SRC"
40
+ echo " Target: $TARGET_DIR"
41
+
42
+ # Check source exists
43
+ if [ ! -d "$EXTENSION_SRC" ]; then
44
+ echo "Error: Extension source not found at $EXTENSION_SRC"
45
+ echo ""
46
+ echo "If you installed via npm/npx, try running from your project directory:"
47
+ echo " ./node_modules/tjs-lang/bin/install-vscode.sh"
48
+ exit 1
49
+ fi
50
+
51
+ # Create extensions directory if needed
52
+ mkdir -p "$VSCODE_EXT_DIR"
53
+
54
+ # Remove old version if exists
55
+ if [ -d "$TARGET_DIR" ]; then
56
+ echo " Removing old version..."
57
+ rm -rf "$TARGET_DIR"
58
+ fi
59
+
60
+ # Copy extension
61
+ cp -r "$EXTENSION_SRC" "$TARGET_DIR"
62
+
63
+ echo ""
64
+ echo "Installation complete!"
65
+ echo ""
66
+ echo "Please restart VS Code to enable AsyncJS syntax highlighting."
67
+ echo ""
68
+ echo "Features:"
69
+ echo " - Syntax highlighting for .ajs files"
70
+ echo " - Embedded highlighting in ajs\`...\` template literals"
71
+ echo " - Error highlighting for forbidden syntax (new, class, async, etc.)"
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,28 @@
1
+ // Run with: bun select-local-models.ts
2
+ import { LocalModels } from '../src/batteries/models'
3
+ async function selectModels() {
4
+ console.log('🔥 Auditing local models...')
5
+ const localModels = new LocalModels()
6
+ await localModels.audit()
7
+ console.log('\n✅ Audit complete. Default models selected:')
8
+ try {
9
+ console.log(` - LLM: ${localModels.getLLM().id}`)
10
+ } catch (e) {
11
+ console.log(` - LLM: Not available`)
12
+ }
13
+ try {
14
+ console.log(` - Structured LLM: ${localModels.getStructuredLLM().id}`)
15
+ } catch (e) {
16
+ console.log(` - Structured LLM: Not available`)
17
+ }
18
+ try {
19
+ console.log(
20
+ ` - Embedding: ${localModels.getEmbedding().id} (Dim: ${
21
+ localModels.getEmbedding().dimension
22
+ })`
23
+ )
24
+ } catch (e) {
25
+ console.log(` - Embedding: Not available`)
26
+ }
27
+ }
28
+ await selectModels()
@@ -0,0 +1,31 @@
1
+ // Run with: bun select-local-models.ts
2
+ import { LocalModels } from '../src/batteries/models'
3
+
4
+ async function selectModels() {
5
+ console.log('🔥 Auditing local models...')
6
+ const localModels = new LocalModels()
7
+ await localModels.audit()
8
+
9
+ console.log('\n✅ Audit complete. Default models selected:')
10
+ try {
11
+ console.log(` - LLM: ${localModels.getLLM().id}`)
12
+ } catch (e) {
13
+ console.log(` - LLM: Not available`)
14
+ }
15
+ try {
16
+ console.log(` - Structured LLM: ${localModels.getStructuredLLM().id}`)
17
+ } catch (e) {
18
+ console.log(` - Structured LLM: Not available`)
19
+ }
20
+ try {
21
+ console.log(
22
+ ` - Embedding: ${localModels.getEmbedding().id} (Dim: ${
23
+ localModels.getEmbedding().dimension
24
+ })`
25
+ )
26
+ } catch (e) {
27
+ console.log(` - Embedding: Not available`)
28
+ }
29
+ }
30
+
31
+ await selectModels()
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Tests for TJS autocompletion
3
+ */
4
+
5
+ import { describe, it, expect } from 'bun:test'
6
+ import { getCompletions, getSignatureHelp } from './src/autocomplete'
7
+
8
+ describe('getCompletions', () => {
9
+ describe('keyword completions', () => {
10
+ it('should suggest function keyword', () => {
11
+ const completions = getCompletions({
12
+ source: 'func',
13
+ position: 4,
14
+ })
15
+
16
+ const funcCompletion = completions.find((c) => c.label === 'function')
17
+ expect(funcCompletion).toBeDefined()
18
+ expect(funcCompletion?.kind).toBe('keyword')
19
+ })
20
+
21
+ it('should suggest test block', () => {
22
+ const completions = getCompletions({
23
+ source: 'tes',
24
+ position: 3,
25
+ })
26
+
27
+ const testCompletion = completions.find((c) => c.label === 'test')
28
+ expect(testCompletion).toBeDefined()
29
+ expect(testCompletion?.snippet).toContain("test('")
30
+ })
31
+
32
+ it('should suggest unsafe block', () => {
33
+ const completions = getCompletions({
34
+ source: 'uns',
35
+ position: 3,
36
+ })
37
+
38
+ const unsafeCompletion = completions.find((c) => c.label === 'unsafe')
39
+ expect(unsafeCompletion).toBeDefined()
40
+ })
41
+ })
42
+
43
+ describe('type completions', () => {
44
+ it('should suggest types after colon', () => {
45
+ const completions = getCompletions({
46
+ source: 'function foo(x: ',
47
+ position: 16,
48
+ })
49
+
50
+ expect(completions.some((c) => c.label === "''")).toBe(true)
51
+ expect(completions.some((c) => c.label === '0')).toBe(true)
52
+ expect(completions.some((c) => c.label === 'true')).toBe(true)
53
+ })
54
+
55
+ it('should suggest types after return arrow', () => {
56
+ const completions = getCompletions({
57
+ source: 'function foo() -> ',
58
+ position: 18,
59
+ })
60
+
61
+ expect(completions.some((c) => c.label === "''")).toBe(true)
62
+ expect(completions.some((c) => c.label === '[]')).toBeFalsy() // Not a valid type
63
+ })
64
+ })
65
+
66
+ describe('function completions', () => {
67
+ it('should extract functions from source', () => {
68
+ const source = `
69
+ function greet(name) { return name }
70
+ function add(a, b) { return a + b }
71
+ `
72
+ const completions = getCompletions({
73
+ source,
74
+ position: source.length,
75
+ })
76
+
77
+ expect(completions.some((c) => c.label === 'greet')).toBe(true)
78
+ expect(completions.some((c) => c.label === 'add')).toBe(true)
79
+ })
80
+
81
+ it('should include runtime functions', () => {
82
+ const completions = getCompletions({
83
+ source: 'is',
84
+ position: 2,
85
+ })
86
+
87
+ const isError = completions.find((c) => c.label === 'isError')
88
+ expect(isError).toBeDefined()
89
+ expect(isError?.kind).toBe('function')
90
+ })
91
+ })
92
+
93
+ describe('variable completions', () => {
94
+ it('should extract declared variables', () => {
95
+ const source = `
96
+ const foo = 1
97
+ let bar = 2
98
+ f
99
+ `
100
+ const completions = getCompletions({
101
+ source,
102
+ position: source.length - 1,
103
+ })
104
+
105
+ expect(completions.some((c) => c.label === 'foo')).toBe(true)
106
+ expect(completions.some((c) => c.label === 'bar')).toBe(true)
107
+ })
108
+ })
109
+
110
+ describe('metadata-based completions', () => {
111
+ it('should use __tjs metadata for function info', () => {
112
+ const completions = getCompletions({
113
+ source: 'gre',
114
+ position: 3,
115
+ metadata: {
116
+ greet: {
117
+ params: {
118
+ name: { type: 'string', required: true },
119
+ },
120
+ returns: { type: 'string' },
121
+ description: 'Greet someone',
122
+ },
123
+ },
124
+ })
125
+
126
+ const greet = completions.find((c) => c.label === 'greet')
127
+ expect(greet).toBeDefined()
128
+ expect(greet?.detail).toContain('name')
129
+ expect(greet?.detail).toContain('string')
130
+ expect(greet?.documentation).toBe('Greet someone')
131
+ })
132
+ })
133
+
134
+ describe('expect matchers', () => {
135
+ it('should suggest matchers after expect().', () => {
136
+ const completions = getCompletions({
137
+ source: 'expect(x).',
138
+ position: 10,
139
+ })
140
+
141
+ expect(completions.some((c) => c.label === 'toBe')).toBe(true)
142
+ expect(completions.some((c) => c.label === 'toEqual')).toBe(true)
143
+ expect(completions.some((c) => c.label === 'toContain')).toBe(true)
144
+ })
145
+ })
146
+
147
+ describe('filtering', () => {
148
+ it('should filter by prefix', () => {
149
+ const completions = getCompletions({
150
+ source: 'to',
151
+ position: 2,
152
+ })
153
+
154
+ // Should not include function, let, etc.
155
+ expect(
156
+ completions.every((c) => c.label.toLowerCase().startsWith('to'))
157
+ ).toBe(true)
158
+ })
159
+
160
+ it('should sort by relevance', () => {
161
+ const completions = getCompletions({
162
+ source: 'function foo() {}\nfoo',
163
+ position: 21,
164
+ })
165
+
166
+ // Function should come before keyword
167
+ const fooIndex = completions.findIndex((c) => c.label === 'foo')
168
+ const funcIndex = completions.findIndex((c) => c.label === 'function')
169
+
170
+ // Our foo function should appear (even if filtered out, the fn should be found)
171
+ expect(fooIndex).toBeGreaterThanOrEqual(0)
172
+ })
173
+ })
174
+ })
175
+
176
+ describe('getSignatureHelp', () => {
177
+ it('should provide signature for function call', () => {
178
+ const help = getSignatureHelp({
179
+ source: 'greet(',
180
+ position: 6,
181
+ metadata: {
182
+ greet: {
183
+ params: {
184
+ name: { type: 'string', required: true },
185
+ greeting: { type: 'string', required: false },
186
+ },
187
+ },
188
+ },
189
+ })
190
+
191
+ expect(help).toBeDefined()
192
+ expect(help?.signature).toContain('greet')
193
+ expect(help?.signature).toContain('name')
194
+ expect(help?.activeParam).toBe(0)
195
+ })
196
+
197
+ it('should track active parameter', () => {
198
+ const help = getSignatureHelp({
199
+ source: 'greet("Alice", ',
200
+ position: 15,
201
+ metadata: {
202
+ greet: {
203
+ params: {
204
+ name: { type: 'string', required: true },
205
+ greeting: { type: 'string', required: false },
206
+ },
207
+ },
208
+ },
209
+ })
210
+
211
+ expect(help?.activeParam).toBe(1)
212
+ })
213
+
214
+ it('should return null outside function call', () => {
215
+ const help = getSignatureHelp({
216
+ source: 'const x = 1',
217
+ position: 11,
218
+ })
219
+
220
+ expect(help).toBeNull()
221
+ })
222
+
223
+ it('should provide help for runtime functions', () => {
224
+ const help = getSignatureHelp({
225
+ source: 'isError(',
226
+ position: 8,
227
+ })
228
+
229
+ expect(help).toBeDefined()
230
+ expect(help?.signature).toContain('isError')
231
+ })
232
+ })