@ttsc/wasm 0.14.0-dev.20260529.2 → 0.14.1
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/dist/ttsc.wasm +0 -0
- package/host/fountain.go +290 -290
- package/lib/src/MemFSError.d.ts +3 -3
- package/lib/src/MemFSError.js +3 -3
- package/lib/src/bootTtsc.d.ts +12 -12
- package/lib/src/bootTtsc.js +33 -34
- package/lib/src/bootTtsc.js.map +1 -1
- package/lib/src/createMemFS.js +2 -2
- package/lib/src/createMemFS.js.map +1 -1
- package/lib/src/parseResult.d.ts +2 -3
- package/lib/src/parseResult.js +2 -3
- package/lib/src/parseResult.js.map +1 -1
- package/lib/src/structures/IBootTtscOptions.d.ts +6 -7
- package/lib/src/structures/ITtscApi.d.ts +5 -5
- package/lib/src/structures/ITtscCompileResult.d.ts +2 -2
- package/lib/src/structures/ITtscPositionQuery.d.ts +4 -1
- package/lib/src/structures/ITtscTransformResult.d.ts +2 -2
- package/lib/src/structures/IWasmExecFS.d.ts +3 -3
- package/package.json +2 -2
- package/src/MemFSError.ts +3 -3
- package/src/bootTtsc.ts +31 -33
- package/src/createMemFS.ts +0 -1
- package/src/parseResult.ts +2 -3
- package/src/structures/IBootTtscOptions.ts +6 -7
- package/src/structures/ITtscApi.ts +5 -5
- package/src/structures/ITtscCompileResult.ts +2 -2
- package/src/structures/ITtscPositionQuery.ts +4 -1
- package/src/structures/ITtscTransformResult.ts +2 -2
- package/src/structures/IWasmExecFS.ts +3 -3
package/host/fountain.go
CHANGED
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
package host
|
|
21
21
|
|
|
22
22
|
import (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
"encoding/json"
|
|
24
|
+
"fmt"
|
|
25
|
+
"path/filepath"
|
|
26
|
+
"sync"
|
|
27
|
+
"sync/atomic"
|
|
28
|
+
"syscall/js"
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
31
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
// snapshotEntry pairs a Program with the cwd it was loaded against so we can
|
|
@@ -44,108 +44,108 @@ import (
|
|
|
44
44
|
// two fountain verbs invoked in the same frame could land their Checker
|
|
45
45
|
// reads on opposite sides of an internal mutation and corrupt state.
|
|
46
46
|
type snapshotEntry struct {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
mu sync.Mutex
|
|
48
|
+
prog *driver.Program
|
|
49
|
+
cwd string
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
var (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
snapshotsMu sync.RWMutex
|
|
54
|
+
snapshots = map[string]*snapshotEntry{}
|
|
55
|
+
nextHandle atomic.Uint64
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
// fountainAPIMap returns the verb → js.Func map appended to globalThis[apiName]
|
|
59
59
|
// during Expose. Kept in a helper so host.go's API map stays scannable.
|
|
60
60
|
func fountainAPIMap() map[string]any {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
return map[string]any{
|
|
62
|
+
"snapshot": js.FuncOf(jsSnapshot),
|
|
63
|
+
"releaseSnapshot": js.FuncOf(jsReleaseSnapshot),
|
|
64
|
+
"snapshots": js.FuncOf(jsListSnapshots),
|
|
65
|
+
"getSourceFiles": js.FuncOf(jsGetSourceFiles),
|
|
66
|
+
"getSourceFileText": js.FuncOf(jsGetSourceFileText),
|
|
67
|
+
"getDiagnostics": js.FuncOf(jsGetDiagnostics),
|
|
68
|
+
"getNodeAtPosition": js.FuncOf(jsGetNodeAtPosition),
|
|
69
|
+
"getTypeAtPosition": js.FuncOf(jsGetTypeAtPosition),
|
|
70
|
+
"getSymbolAtPosition": js.FuncOf(jsGetSymbolAtPosition),
|
|
71
|
+
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// SnapshotResult is the response shape for `snapshot()`.
|
|
75
75
|
type SnapshotResult struct {
|
|
76
|
-
|
|
76
|
+
Handle string `json:"handle"`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// ReleaseSnapshotResult is the response shape for `releaseSnapshot()`.
|
|
80
80
|
type ReleaseSnapshotResult struct {
|
|
81
|
-
|
|
81
|
+
Released bool `json:"released"`
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ListSnapshotsResult is the response shape for `snapshots()`.
|
|
85
85
|
type ListSnapshotsResult struct {
|
|
86
|
-
|
|
86
|
+
Handles []string `json:"handles"`
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// GetSourceFilesResult is the response shape for `getSourceFiles()`.
|
|
90
90
|
type GetSourceFilesResult struct {
|
|
91
|
-
|
|
91
|
+
Files []string `json:"files"`
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// GetSourceFileTextResult is the response shape for `getSourceFileText()`.
|
|
95
95
|
type GetSourceFileTextResult struct {
|
|
96
|
-
|
|
96
|
+
Text string `json:"text"`
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// GetDiagnosticsResult is the response shape for `getDiagnostics()`.
|
|
100
100
|
type GetDiagnosticsResult struct {
|
|
101
|
-
|
|
101
|
+
Diagnostics []CompileDiagnostic `json:"diagnostics"`
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// NodeInfo is the serialized AST node returned by `getNodeAtPosition`.
|
|
105
105
|
type NodeInfo struct {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
Kind int `json:"kind"`
|
|
107
|
+
KindName string `json:"kindName"`
|
|
108
|
+
Pos int `json:"pos"`
|
|
109
|
+
End int `json:"end"`
|
|
110
|
+
Text string `json:"text,omitempty"`
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// GetNodeAtPositionResult is the response shape for `getNodeAtPosition()`.
|
|
114
114
|
type GetNodeAtPositionResult struct {
|
|
115
|
-
|
|
115
|
+
Node *NodeInfo `json:"node"`
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// TypeInfo is the serialized type returned by `getTypeAtPosition`.
|
|
119
119
|
type TypeInfo struct {
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
Text string `json:"text"`
|
|
121
|
+
Flags int `json:"flags"`
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// GetTypeAtPositionResult is the response shape for `getTypeAtPosition()`.
|
|
125
125
|
type GetTypeAtPositionResult struct {
|
|
126
|
-
|
|
126
|
+
Type *TypeInfo `json:"type"`
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// SymbolDeclaration is the serialized declaration site returned by
|
|
130
130
|
// `getSymbolAtPosition`.
|
|
131
131
|
type SymbolDeclaration struct {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
File *string `json:"file"`
|
|
133
|
+
Pos int `json:"pos"`
|
|
134
|
+
End int `json:"end"`
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// SymbolInfo is the serialized symbol returned by `getSymbolAtPosition`.
|
|
138
138
|
type SymbolInfo struct {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
Name string `json:"name"`
|
|
140
|
+
Text string `json:"text,omitempty"`
|
|
141
|
+
Flags int `json:"flags"`
|
|
142
|
+
Declarations []SymbolDeclaration `json:"declarations,omitempty"`
|
|
143
|
+
DeclarationCount int `json:"declarationCount,omitempty"`
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// GetSymbolAtPositionResult is the response shape for `getSymbolAtPosition()`.
|
|
147
147
|
type GetSymbolAtPositionResult struct {
|
|
148
|
-
|
|
148
|
+
Symbol *SymbolInfo `json:"symbol"`
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// jsSnapshot({cwd, tsconfig?}) → Promise<ITtscResult>.
|
|
@@ -153,35 +153,35 @@ type GetSymbolAtPositionResult struct {
|
|
|
153
153
|
// Loads the project once and retains the Program for follow-up queries. The
|
|
154
154
|
// returned handle is opaque; treat it as a string.
|
|
155
155
|
func jsSnapshot(this js.Value, args []js.Value) any {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
156
|
+
opts := optionsArg(args)
|
|
157
|
+
return makePromise(func() any {
|
|
158
|
+
cwd := stringProp(opts, "cwd")
|
|
159
|
+
tsconfig := stringProp(opts, "tsconfig")
|
|
160
|
+
if cwd == "" {
|
|
161
|
+
return errorResponse(2, "host.snapshot: \"cwd\" is required")
|
|
162
|
+
}
|
|
163
|
+
if tsconfig == "" {
|
|
164
|
+
tsconfig = "tsconfig.json"
|
|
165
|
+
}
|
|
166
|
+
prog, diags, err := driver.LoadProgram(cwd, tsconfig, driver.LoadProgramOptions{
|
|
167
|
+
ForceNoEmit: true,
|
|
168
|
+
})
|
|
169
|
+
if err != nil {
|
|
170
|
+
return errorResponse(2, err.Error())
|
|
171
|
+
}
|
|
172
|
+
if prog == nil {
|
|
173
|
+
msg := "host.snapshot: project load failed"
|
|
174
|
+
if len(diags) > 0 {
|
|
175
|
+
msg = diags[0].Message
|
|
176
|
+
}
|
|
177
|
+
return errorResponse(2, msg)
|
|
178
|
+
}
|
|
179
|
+
handle := fmt.Sprintf("snap-%d", nextHandle.Add(1))
|
|
180
|
+
snapshotsMu.Lock()
|
|
181
|
+
snapshots[handle] = &snapshotEntry{prog: prog, cwd: cwd}
|
|
182
|
+
snapshotsMu.Unlock()
|
|
183
|
+
return fountainOK(SnapshotResult{Handle: handle})
|
|
184
|
+
})
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// jsReleaseSnapshot({handle}) → Promise<ITtscResult>.
|
|
@@ -194,100 +194,100 @@ func jsSnapshot(this js.Value, args []js.Value) any {
|
|
|
194
194
|
// (withSnapshot's RLock) finishes before the Program is closed. This is the
|
|
195
195
|
// pair of the TOCTOU guarantee documented on withSnapshot.
|
|
196
196
|
func jsReleaseSnapshot(this js.Value, args []js.Value) any {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
197
|
+
opts := optionsArg(args)
|
|
198
|
+
return makePromise(func() any {
|
|
199
|
+
handle := stringProp(opts, "handle")
|
|
200
|
+
if handle == "" {
|
|
201
|
+
return errorResponse(2, "host.releaseSnapshot: \"handle\" is required")
|
|
202
|
+
}
|
|
203
|
+
snapshotsMu.Lock()
|
|
204
|
+
defer snapshotsMu.Unlock()
|
|
205
|
+
entry, ok := snapshots[handle]
|
|
206
|
+
if !ok {
|
|
207
|
+
return fountainOK(ReleaseSnapshotResult{Released: false})
|
|
208
|
+
}
|
|
209
|
+
delete(snapshots, handle)
|
|
210
|
+
if entry.prog != nil {
|
|
211
|
+
// Recover from any panic inside Close so the rest of the wasm
|
|
212
|
+
// instance survives; the entry has already been removed from
|
|
213
|
+
// the table so the handle is effectively released either way.
|
|
214
|
+
func() {
|
|
215
|
+
defer func() { _ = recover() }()
|
|
216
|
+
_ = entry.prog.Close()
|
|
217
|
+
}()
|
|
218
|
+
}
|
|
219
|
+
return fountainOK(ReleaseSnapshotResult{Released: true})
|
|
220
|
+
})
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// jsListSnapshots() → Promise<ITtscResult>. Debug aid — lets a JS embedder
|
|
224
224
|
// verify it has released what it thinks it has.
|
|
225
225
|
func jsListSnapshots(this js.Value, args []js.Value) any {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
return makePromise(func() any {
|
|
227
|
+
snapshotsMu.RLock()
|
|
228
|
+
handles := make([]string, 0, len(snapshots))
|
|
229
|
+
for h := range snapshots {
|
|
230
|
+
handles = append(handles, h)
|
|
231
|
+
}
|
|
232
|
+
snapshotsMu.RUnlock()
|
|
233
|
+
return fountainOK(ListSnapshotsResult{Handles: handles})
|
|
234
|
+
})
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// jsGetSourceFiles({handle}) → Promise<ITtscResult>. Lists the non-
|
|
238
238
|
// declaration source files in the program, keyed by project-relative path
|
|
239
239
|
// (same convention as build/transform output).
|
|
240
240
|
func jsGetSourceFiles(this js.Value, args []js.Value) any {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
return withSnapshot(args, func(entry *snapshotEntry, _ js.Value) any {
|
|
242
|
+
files := entry.prog.SourceFiles()
|
|
243
|
+
out := make([]string, 0, len(files))
|
|
244
|
+
for _, f := range files {
|
|
245
|
+
out = append(out, snapshotFileKey(entry.cwd, f.FileName()))
|
|
246
|
+
}
|
|
247
|
+
return fountainOK(GetSourceFilesResult{Files: out})
|
|
248
|
+
})
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
// jsGetSourceFileText({handle, path}) → Promise<ITtscResult>. Returns the
|
|
252
252
|
// current source text the Program is holding for the file. After transform
|
|
253
253
|
// plugins have run, this reflects the post-transform text.
|
|
254
254
|
func jsGetSourceFileText(this js.Value, args []js.Value) any {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
255
|
+
return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
|
|
256
|
+
path := stringProp(opts, "path")
|
|
257
|
+
if path == "" {
|
|
258
|
+
return errorResponse(2, "host.getSourceFileText: \"path\" is required")
|
|
259
|
+
}
|
|
260
|
+
file := resolveSnapshotFile(entry, path)
|
|
261
|
+
if file == nil {
|
|
262
|
+
return errorResponse(2, fmt.Sprintf("host.getSourceFileText: file not found: %q", path))
|
|
263
|
+
}
|
|
264
|
+
return fountainOK(GetSourceFileTextResult{Text: file.Text()})
|
|
265
|
+
})
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
// jsGetDiagnostics({handle, file?}) → Promise<ITtscResult>. Returns the same
|
|
269
269
|
// diagnostic shape as build/check/transform. When `file` is set, results are
|
|
270
270
|
// filtered to that project-relative path.
|
|
271
271
|
func jsGetDiagnostics(this js.Value, args []js.Value) any {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
272
|
+
return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
|
|
273
|
+
filter := stringProp(opts, "file")
|
|
274
|
+
diags := entry.prog.Diagnostics()
|
|
275
|
+
out := make([]CompileDiagnostic, 0, len(diags))
|
|
276
|
+
for _, d := range diags {
|
|
277
|
+
api := toAPIDiagnostic(d)
|
|
278
|
+
if filter != "" {
|
|
279
|
+
if api.File == nil {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
rel := snapshotFileKey(entry.cwd, *api.File)
|
|
283
|
+
if rel != filter && *api.File != filter {
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
out = append(out, api)
|
|
288
|
+
}
|
|
289
|
+
return fountainOK(GetDiagnosticsResult{Diagnostics: out})
|
|
290
|
+
})
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// jsGetNodeAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
@@ -296,10 +296,10 @@ func jsGetDiagnostics(this js.Value, args []js.Value) any {
|
|
|
296
296
|
// should resolve it to a byte offset before calling (the @ttsc/playground
|
|
297
297
|
// helper does this).
|
|
298
298
|
func jsGetNodeAtPosition(this js.Value, args []js.Value) any {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
return withSnapshotPosition(args, func(_ *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
300
|
+
node := ast.GetNodeAtPosition(file, pos, false)
|
|
301
|
+
return fountainOK(GetNodeAtPositionResult{Node: nodeInfoOf(node)})
|
|
302
|
+
})
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// jsGetTypeAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
@@ -307,39 +307,39 @@ func jsGetNodeAtPosition(this js.Value, args []js.Value) any {
|
|
|
307
307
|
// pinned single checker for its type. Returns `{type: null}` when there is no
|
|
308
308
|
// node at that position or no checker is available.
|
|
309
309
|
func jsGetTypeAtPosition(this js.Value, args []js.Value) any {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
310
|
+
return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
311
|
+
node := ast.GetNodeAtPosition(file, pos, false)
|
|
312
|
+
if node == nil || entry.prog.Checker == nil {
|
|
313
|
+
return fountainOK(GetTypeAtPositionResult{Type: nil})
|
|
314
|
+
}
|
|
315
|
+
t := entry.prog.Checker.GetTypeAtLocation(node)
|
|
316
|
+
if t == nil {
|
|
317
|
+
return fountainOK(GetTypeAtPositionResult{Type: nil})
|
|
318
|
+
}
|
|
319
|
+
return fountainOK(GetTypeAtPositionResult{
|
|
320
|
+
Type: &TypeInfo{
|
|
321
|
+
Text: entry.prog.Checker.TypeToString(t),
|
|
322
|
+
Flags: int(t.Flags()),
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
// jsGetSymbolAtPosition({handle, path, position}) → Promise<ITtscResult>.
|
|
329
329
|
// Returns `{symbol: null}` when the node at position has no associated
|
|
330
330
|
// symbol (e.g. punctuation, whitespace).
|
|
331
331
|
func jsGetSymbolAtPosition(this js.Value, args []js.Value) any {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
332
|
+
return withSnapshotPosition(args, func(entry *snapshotEntry, file *ast.SourceFile, pos int) any {
|
|
333
|
+
node := ast.GetNodeAtPosition(file, pos, false)
|
|
334
|
+
if node == nil || entry.prog.Checker == nil {
|
|
335
|
+
return fountainOK(GetSymbolAtPositionResult{Symbol: nil})
|
|
336
|
+
}
|
|
337
|
+
sym := entry.prog.Checker.GetSymbolAtLocation(node)
|
|
338
|
+
if sym == nil {
|
|
339
|
+
return fountainOK(GetSymbolAtPositionResult{Symbol: nil})
|
|
340
|
+
}
|
|
341
|
+
return fountainOK(GetSymbolAtPositionResult{Symbol: symbolInfoOf(entry, sym)})
|
|
342
|
+
})
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// withSnapshot is the shared envelope for every read-only fountain verb. It
|
|
@@ -354,22 +354,22 @@ func jsGetSymbolAtPosition(this js.Value, args []js.Value) any {
|
|
|
354
354
|
// thread-safe; two fountain verbs invoked in the same frame could
|
|
355
355
|
// otherwise interleave Checker state mutations.
|
|
356
356
|
func withSnapshot(args []js.Value, fn func(*snapshotEntry, js.Value) any) any {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
357
|
+
opts := optionsArg(args)
|
|
358
|
+
return makePromise(func() any {
|
|
359
|
+
handle := stringProp(opts, "handle")
|
|
360
|
+
if handle == "" {
|
|
361
|
+
return errorResponse(2, "host: \"handle\" is required")
|
|
362
|
+
}
|
|
363
|
+
snapshotsMu.RLock()
|
|
364
|
+
defer snapshotsMu.RUnlock()
|
|
365
|
+
entry := snapshots[handle]
|
|
366
|
+
if entry == nil {
|
|
367
|
+
return errorResponse(2, fmt.Sprintf("host: snapshot %q not found (already released or never created)", handle))
|
|
368
|
+
}
|
|
369
|
+
entry.mu.Lock()
|
|
370
|
+
defer entry.mu.Unlock()
|
|
371
|
+
return fn(entry, opts)
|
|
372
|
+
})
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
// withSnapshotPosition is the shared envelope for the 3 position-bound
|
|
@@ -378,128 +378,128 @@ func withSnapshot(args []js.Value, fn func(*snapshotEntry, js.Value) any) any {
|
|
|
378
378
|
// out-of-range offset reaches `ast.GetNodeAtPosition` only as a clean
|
|
379
379
|
// error response instead of an internal panic.
|
|
380
380
|
func withSnapshotPosition(args []js.Value, fn func(*snapshotEntry, *ast.SourceFile, int) any) any {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
381
|
+
return withSnapshot(args, func(entry *snapshotEntry, opts js.Value) any {
|
|
382
|
+
path := stringProp(opts, "path")
|
|
383
|
+
if path == "" {
|
|
384
|
+
return errorResponse(2, "host: \"path\" is required")
|
|
385
|
+
}
|
|
386
|
+
posVal := opts.Get("position")
|
|
387
|
+
if posVal.Type() != js.TypeNumber {
|
|
388
|
+
return errorResponse(2, "host: \"position\" must be a number (byte offset)")
|
|
389
|
+
}
|
|
390
|
+
pos := posVal.Int()
|
|
391
|
+
if pos < 0 {
|
|
392
|
+
return errorResponse(2, "host: \"position\" must be non-negative")
|
|
393
|
+
}
|
|
394
|
+
file := resolveSnapshotFile(entry, path)
|
|
395
|
+
if file == nil {
|
|
396
|
+
return errorResponse(2, fmt.Sprintf("host: file %q not found in snapshot", path))
|
|
397
|
+
}
|
|
398
|
+
if pos > len(file.Text()) {
|
|
399
|
+
return errorResponse(2, fmt.Sprintf("host: \"position\" %d exceeds file length %d", pos, len(file.Text())))
|
|
400
|
+
}
|
|
401
|
+
return fn(entry, file, pos)
|
|
402
|
+
})
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
// resolveSnapshotFile finds a SourceFile by path, accepting either an
|
|
406
406
|
// absolute path or a path relative to the snapshot's cwd.
|
|
407
407
|
func resolveSnapshotFile(entry *snapshotEntry, path string) *ast.SourceFile {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
408
|
+
if file := entry.prog.SourceFile(path); file != nil {
|
|
409
|
+
return file
|
|
410
|
+
}
|
|
411
|
+
if !filepath.IsAbs(path) {
|
|
412
|
+
abs := filepath.Join(entry.cwd, path)
|
|
413
|
+
if file := entry.prog.SourceFile(abs); file != nil {
|
|
414
|
+
return file
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return nil
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
// snapshotFileKey mirrors apiOutputKey but is exported for use by all
|
|
421
421
|
// fountain endpoints that return file paths.
|
|
422
422
|
func snapshotFileKey(cwd, fileName string) string {
|
|
423
|
-
|
|
423
|
+
return apiOutputKey(cwd, fileName)
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
// fountainOK wraps a JSON-able payload in the standard `{code, stdout,
|
|
427
427
|
// stderr, result}` envelope. The payload is JSON-encoded into `result`; JS
|
|
428
428
|
// callers `parseResult<T>` it the same way they do for build/check/transform.
|
|
429
429
|
func fountainOK(payload any) any {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
430
|
+
data, err := json.Marshal(payload)
|
|
431
|
+
if err != nil {
|
|
432
|
+
return errorResponse(3, fmt.Sprintf("host: fountain result marshal failed: %v", err))
|
|
433
|
+
}
|
|
434
|
+
return js.ValueOf(map[string]any{
|
|
435
|
+
"code": 0,
|
|
436
|
+
"stdout": "",
|
|
437
|
+
"stderr": "",
|
|
438
|
+
"result": string(data),
|
|
439
|
+
})
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
// nodeInfoOf converts a *ast.Node into the JSON-serializable NodeInfo.
|
|
443
443
|
func nodeInfoOf(node *ast.Node) *NodeInfo {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
444
|
+
if node == nil {
|
|
445
|
+
return nil
|
|
446
|
+
}
|
|
447
|
+
info := &NodeInfo{
|
|
448
|
+
Kind: int(node.Kind),
|
|
449
|
+
KindName: astKindName(node.Kind),
|
|
450
|
+
Pos: node.Pos(),
|
|
451
|
+
End: node.End(),
|
|
452
|
+
}
|
|
453
|
+
if text := shimscanner.GetTextOfNode(node); text != "" {
|
|
454
|
+
info.Text = text
|
|
455
|
+
}
|
|
456
|
+
return info
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
// symbolInfoOf converts a *ast.Symbol into the JSON-serializable SymbolInfo,
|
|
460
460
|
// including up to a few declaration sites so callers can implement "go to
|
|
461
461
|
// definition" without follow-up snapshot queries.
|
|
462
462
|
func symbolInfoOf(entry *snapshotEntry, sym *ast.Symbol) *SymbolInfo {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
463
|
+
if sym == nil {
|
|
464
|
+
return nil
|
|
465
|
+
}
|
|
466
|
+
info := &SymbolInfo{
|
|
467
|
+
Name: sym.Name,
|
|
468
|
+
Flags: int(sym.Flags),
|
|
469
|
+
}
|
|
470
|
+
if entry.prog.Checker != nil {
|
|
471
|
+
info.Text = entry.prog.Checker.SymbolToString(sym)
|
|
472
|
+
}
|
|
473
|
+
decls := sym.Declarations
|
|
474
|
+
if len(decls) > 0 {
|
|
475
|
+
// Cap at 16 to keep merged-namespace symbols (e.g. global lib types)
|
|
476
|
+
// from ballooning the response.
|
|
477
|
+
const maxDecls = 16
|
|
478
|
+
n := len(decls)
|
|
479
|
+
capped := n
|
|
480
|
+
if n > maxDecls {
|
|
481
|
+
capped = maxDecls
|
|
482
|
+
}
|
|
483
|
+
out := make([]SymbolDeclaration, 0, capped)
|
|
484
|
+
for _, d := range decls[:capped] {
|
|
485
|
+
if d == nil {
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
item := SymbolDeclaration{Pos: d.Pos(), End: d.End()}
|
|
489
|
+
if file := ast.GetSourceFileOfNode(d); file != nil {
|
|
490
|
+
key := snapshotFileKey(entry.cwd, file.FileName())
|
|
491
|
+
item.File = &key
|
|
492
|
+
}
|
|
493
|
+
out = append(out, item)
|
|
494
|
+
}
|
|
495
|
+
info.Declarations = out
|
|
496
|
+
info.DeclarationCount = n
|
|
497
|
+
}
|
|
498
|
+
return info
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
// astKindName renders an ast.Kind to its string representation. ast.Kind has
|
|
502
502
|
// a Stringer impl in tsgo (`%v` produces "FunctionDeclaration" etc.).
|
|
503
503
|
func astKindName(k ast.Kind) string {
|
|
504
|
-
|
|
504
|
+
return fmt.Sprintf("%v", k)
|
|
505
505
|
}
|