@formspec/ts-plugin 0.1.0-alpha.20 → 0.1.0-alpha.22
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/README.md +64 -0
- package/dist/__tests__/downstream-authoring-host.test.d.ts +2 -0
- package/dist/__tests__/downstream-authoring-host.test.d.ts.map +1 -0
- package/dist/__tests__/handle-query.test.d.ts +2 -0
- package/dist/__tests__/handle-query.test.d.ts.map +1 -0
- package/dist/__tests__/helpers.d.ts +11 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/semantic-service.test.d.ts +2 -0
- package/dist/__tests__/semantic-service.test.d.ts.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.cjs +510 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +520 -125
- package/dist/index.js.map +1 -1
- package/dist/perf-utils.d.ts +3 -0
- package/dist/perf-utils.d.ts.map +1 -0
- package/dist/reference-host-example.d.ts +14 -0
- package/dist/reference-host-example.d.ts.map +1 -0
- package/dist/semantic-service.d.ts +116 -0
- package/dist/semantic-service.d.ts.map +1 -0
- package/dist/service.d.ts +39 -19
- package/dist/service.d.ts.map +1 -1
- package/dist/ts-plugin.d.ts +458 -0
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION4,
|
|
4
|
+
FORMSPEC_ANALYSIS_SCHEMA_VERSION as FORMSPEC_ANALYSIS_SCHEMA_VERSION2
|
|
5
|
+
} from "@formspec/analysis/protocol";
|
|
6
|
+
|
|
1
7
|
// src/service.ts
|
|
2
8
|
import fs from "fs/promises";
|
|
3
9
|
import net from "net";
|
|
4
10
|
import "typescript";
|
|
5
11
|
import {
|
|
6
|
-
FORMSPEC_ANALYSIS_PROTOCOL_VERSION as
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
getSubjectType,
|
|
11
|
-
getCommentHoverInfoAtOffset,
|
|
12
|
-
getSemanticCommentCompletionContextAtOffset,
|
|
13
|
-
isFormSpecSemanticQuery,
|
|
14
|
-
resolveDeclarationPlacement,
|
|
15
|
-
serializeCompletionContext,
|
|
16
|
-
serializeHoverInfo
|
|
17
|
-
} from "@formspec/analysis";
|
|
12
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
13
|
+
isFormSpecSemanticQuery
|
|
14
|
+
} from "@formspec/analysis/protocol";
|
|
15
|
+
import "@formspec/analysis/internal";
|
|
18
16
|
|
|
19
17
|
// src/workspace.ts
|
|
20
18
|
import os from "os";
|
|
@@ -25,7 +23,7 @@ import {
|
|
|
25
23
|
getFormSpecManifestPath,
|
|
26
24
|
getFormSpecWorkspaceId,
|
|
27
25
|
getFormSpecWorkspaceRuntimeDirectory
|
|
28
|
-
} from "@formspec/analysis";
|
|
26
|
+
} from "@formspec/analysis/protocol";
|
|
29
27
|
function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
|
|
30
28
|
const workspaceId = getFormSpecWorkspaceId(workspaceRoot);
|
|
31
29
|
const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);
|
|
@@ -74,10 +72,426 @@ function sanitizeScopeSegment(value) {
|
|
|
74
72
|
return sanitized.length > 0 ? sanitized : "formspec";
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
// src/semantic-service.ts
|
|
76
|
+
import "typescript";
|
|
77
|
+
import {
|
|
78
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
79
|
+
computeFormSpecTextHash
|
|
80
|
+
} from "@formspec/analysis/protocol";
|
|
81
|
+
import {
|
|
82
|
+
buildFormSpecAnalysisFileSnapshot,
|
|
83
|
+
createFormSpecPerformanceRecorder,
|
|
84
|
+
findDeclarationForCommentOffset,
|
|
85
|
+
getCommentHoverInfoAtOffset,
|
|
86
|
+
getSemanticCommentCompletionContextAtOffset,
|
|
87
|
+
getFormSpecPerformanceNow,
|
|
88
|
+
getSubjectType,
|
|
89
|
+
optionalMeasure,
|
|
90
|
+
resolveDeclarationPlacement,
|
|
91
|
+
serializeCompletionContext,
|
|
92
|
+
serializeHoverInfo
|
|
93
|
+
} from "@formspec/analysis/internal";
|
|
94
|
+
|
|
95
|
+
// src/constants.ts
|
|
96
|
+
var FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;
|
|
97
|
+
var FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 3e4;
|
|
98
|
+
var FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
|
|
99
|
+
var FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
|
|
100
|
+
|
|
101
|
+
// src/perf-utils.ts
|
|
102
|
+
function formatPerformanceEvent(event) {
|
|
103
|
+
const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
|
|
104
|
+
return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/semantic-service.ts
|
|
108
|
+
var STATS_ONLY_EVENT_NAMES = /* @__PURE__ */ new Set([
|
|
109
|
+
"analysis.syntheticCheckBatch.cacheHit",
|
|
110
|
+
"analysis.narrowSyntheticCheckBatch.cacheHit",
|
|
111
|
+
"analysis.syntheticCheckBatch.cacheMiss",
|
|
112
|
+
"analysis.narrowSyntheticCheckBatch.cacheMiss",
|
|
113
|
+
"analysis.syntheticCheckBatch.createProgram",
|
|
114
|
+
"analysis.narrowSyntheticCheckBatch.createProgram"
|
|
115
|
+
]);
|
|
116
|
+
var StatsOnlyPerformanceRecorder = class {
|
|
117
|
+
mutableEvents = [];
|
|
118
|
+
get events() {
|
|
119
|
+
return this.mutableEvents;
|
|
120
|
+
}
|
|
121
|
+
measure(name, detail, callback) {
|
|
122
|
+
const result = callback();
|
|
123
|
+
if (STATS_ONLY_EVENT_NAMES.has(name)) {
|
|
124
|
+
this.mutableEvents.push({
|
|
125
|
+
name,
|
|
126
|
+
durationMs: 0,
|
|
127
|
+
...detail === void 0 ? {} : { detail }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
record(event) {
|
|
133
|
+
if (STATS_ONLY_EVENT_NAMES.has(event.name)) {
|
|
134
|
+
this.mutableEvents.push(event);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var FormSpecSemanticService = class {
|
|
139
|
+
constructor(options) {
|
|
140
|
+
this.options = options;
|
|
141
|
+
}
|
|
142
|
+
snapshotCache = /* @__PURE__ */ new Map();
|
|
143
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
144
|
+
stats = {
|
|
145
|
+
queryTotals: {
|
|
146
|
+
completion: 0,
|
|
147
|
+
hover: 0,
|
|
148
|
+
diagnostics: 0,
|
|
149
|
+
fileSnapshot: 0
|
|
150
|
+
},
|
|
151
|
+
queryPathTotals: {
|
|
152
|
+
diagnostics: { cold: 0, warm: 0 },
|
|
153
|
+
fileSnapshot: { cold: 0, warm: 0 }
|
|
154
|
+
},
|
|
155
|
+
fileSnapshotCacheHits: 0,
|
|
156
|
+
fileSnapshotCacheMisses: 0,
|
|
157
|
+
syntheticBatchCacheHits: 0,
|
|
158
|
+
syntheticBatchCacheMisses: 0,
|
|
159
|
+
syntheticCompileCount: 0,
|
|
160
|
+
syntheticCompileApplications: 0
|
|
161
|
+
};
|
|
162
|
+
/** Resolves semantic completion context for a comment cursor position. */
|
|
163
|
+
getCompletionContext(filePath, offset) {
|
|
164
|
+
this.stats.queryTotals.completion += 1;
|
|
165
|
+
return this.runMeasured(
|
|
166
|
+
"semantic.getCompletionContext",
|
|
167
|
+
{ filePath, offset },
|
|
168
|
+
(performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
|
|
169
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
170
|
+
sourceHash: context.sourceHash,
|
|
171
|
+
context: serializeCompletionContext(
|
|
172
|
+
getSemanticCommentCompletionContextAtOffset(context.sourceFile.text, offset, {
|
|
173
|
+
checker: context.checker,
|
|
174
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
175
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
}))
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
/** Resolves semantic hover payload for a comment cursor position. */
|
|
182
|
+
getHover(filePath, offset) {
|
|
183
|
+
this.stats.queryTotals.hover += 1;
|
|
184
|
+
return this.runMeasured(
|
|
185
|
+
"semantic.getHover",
|
|
186
|
+
{ filePath, offset },
|
|
187
|
+
(performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
|
|
188
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
189
|
+
sourceHash: context.sourceHash,
|
|
190
|
+
hover: serializeHoverInfo(
|
|
191
|
+
getCommentHoverInfoAtOffset(context.sourceFile.text, offset, {
|
|
192
|
+
checker: context.checker,
|
|
193
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
194
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
}))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
/** Returns canonical FormSpec diagnostics for a file in the current host program. */
|
|
201
|
+
getDiagnostics(filePath) {
|
|
202
|
+
this.stats.queryTotals.diagnostics += 1;
|
|
203
|
+
return this.runMeasured("semantic.getDiagnostics", { filePath }, (performance2) => {
|
|
204
|
+
const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
|
|
205
|
+
this.recordQueryPath("diagnostics", cacheState);
|
|
206
|
+
return {
|
|
207
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
208
|
+
sourceHash: snapshot.sourceHash,
|
|
209
|
+
diagnostics: snapshot.diagnostics
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Returns the full serialized semantic snapshot for a file. */
|
|
214
|
+
getFileSnapshot(filePath) {
|
|
215
|
+
this.stats.queryTotals.fileSnapshot += 1;
|
|
216
|
+
return this.runMeasured("semantic.getFileSnapshot", { filePath }, (performance2) => {
|
|
217
|
+
const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
|
|
218
|
+
this.recordQueryPath("fileSnapshot", cacheState);
|
|
219
|
+
return snapshot;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** Schedules a debounced background refresh for the file snapshot cache. */
|
|
223
|
+
scheduleSnapshotRefresh(filePath) {
|
|
224
|
+
const existing = this.refreshTimers.get(filePath);
|
|
225
|
+
if (existing !== void 0) {
|
|
226
|
+
clearTimeout(existing);
|
|
227
|
+
}
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
try {
|
|
230
|
+
this.getFileSnapshot(filePath);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.options.logger?.info(
|
|
233
|
+
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
this.refreshTimers.delete(filePath);
|
|
237
|
+
}, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);
|
|
238
|
+
timer.unref();
|
|
239
|
+
this.refreshTimers.set(filePath, timer);
|
|
240
|
+
}
|
|
241
|
+
/** Clears pending timers and cached semantic snapshots. */
|
|
242
|
+
dispose() {
|
|
243
|
+
for (const timer of this.refreshTimers.values()) {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
this.refreshTimers.clear();
|
|
247
|
+
this.snapshotCache.clear();
|
|
248
|
+
}
|
|
249
|
+
/** Returns a copy of the current performance and cache counters. */
|
|
250
|
+
getStats() {
|
|
251
|
+
return {
|
|
252
|
+
queryTotals: { ...this.stats.queryTotals },
|
|
253
|
+
queryPathTotals: {
|
|
254
|
+
diagnostics: { ...this.stats.queryPathTotals.diagnostics },
|
|
255
|
+
fileSnapshot: { ...this.stats.queryPathTotals.fileSnapshot }
|
|
256
|
+
},
|
|
257
|
+
fileSnapshotCacheHits: this.stats.fileSnapshotCacheHits,
|
|
258
|
+
fileSnapshotCacheMisses: this.stats.fileSnapshotCacheMisses,
|
|
259
|
+
syntheticBatchCacheHits: this.stats.syntheticBatchCacheHits,
|
|
260
|
+
syntheticBatchCacheMisses: this.stats.syntheticBatchCacheMisses,
|
|
261
|
+
syntheticCompileCount: this.stats.syntheticCompileCount,
|
|
262
|
+
syntheticCompileApplications: this.stats.syntheticCompileApplications
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
runMeasured(name, detail, fn) {
|
|
266
|
+
const performance2 = this.options.enablePerformanceLogging === true ? createFormSpecPerformanceRecorder() : new StatsOnlyPerformanceRecorder();
|
|
267
|
+
const result = optionalMeasure(performance2, name, detail, () => fn(performance2));
|
|
268
|
+
this.updateStatsFromPerformanceEvents(performance2.events);
|
|
269
|
+
if (this.options.enablePerformanceLogging === true) {
|
|
270
|
+
this.logPerformanceEvents(name, performance2.events);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
withCommentQueryContext(filePath, offset, performance2, handler) {
|
|
275
|
+
return optionalMeasure(
|
|
276
|
+
performance2,
|
|
277
|
+
"semantic.resolveCommentQueryContext",
|
|
278
|
+
{
|
|
279
|
+
filePath,
|
|
280
|
+
offset
|
|
281
|
+
},
|
|
282
|
+
() => {
|
|
283
|
+
const environment = this.getSourceEnvironment(filePath, performance2);
|
|
284
|
+
if (environment === null) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const declaration = optionalMeasure(
|
|
288
|
+
performance2,
|
|
289
|
+
"semantic.findDeclarationForCommentOffset",
|
|
290
|
+
{
|
|
291
|
+
filePath,
|
|
292
|
+
offset
|
|
293
|
+
},
|
|
294
|
+
() => findDeclarationForCommentOffset(environment.sourceFile, offset)
|
|
295
|
+
);
|
|
296
|
+
const placement = declaration === null ? null : optionalMeasure(
|
|
297
|
+
performance2,
|
|
298
|
+
"semantic.resolveDeclarationPlacement",
|
|
299
|
+
void 0,
|
|
300
|
+
() => resolveDeclarationPlacement(declaration)
|
|
301
|
+
);
|
|
302
|
+
const subjectType = declaration === null ? void 0 : optionalMeasure(
|
|
303
|
+
performance2,
|
|
304
|
+
"semantic.getSubjectType",
|
|
305
|
+
void 0,
|
|
306
|
+
() => getSubjectType(declaration, environment.checker)
|
|
307
|
+
);
|
|
308
|
+
return handler({
|
|
309
|
+
...environment,
|
|
310
|
+
declaration,
|
|
311
|
+
placement,
|
|
312
|
+
subjectType
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
getFileSnapshotWithCacheState(filePath, performance2) {
|
|
318
|
+
const startedAt = getFormSpecPerformanceNow();
|
|
319
|
+
const environment = this.getSourceEnvironment(filePath, performance2);
|
|
320
|
+
if (environment === null) {
|
|
321
|
+
this.stats.fileSnapshotCacheMisses += 1;
|
|
322
|
+
const snapshot2 = {
|
|
323
|
+
filePath,
|
|
324
|
+
sourceHash: "",
|
|
325
|
+
generatedAt: this.getNow().toISOString(),
|
|
326
|
+
comments: [],
|
|
327
|
+
diagnostics: [
|
|
328
|
+
{
|
|
329
|
+
code: "MISSING_SOURCE_FILE",
|
|
330
|
+
category: "infrastructure",
|
|
331
|
+
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
332
|
+
range: { start: 0, end: 0 },
|
|
333
|
+
severity: "warning",
|
|
334
|
+
relatedLocations: [],
|
|
335
|
+
data: {
|
|
336
|
+
filePath
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
};
|
|
341
|
+
performance2.record({
|
|
342
|
+
name: "semantic.getFileSnapshot.result",
|
|
343
|
+
durationMs: getFormSpecPerformanceNow() - startedAt,
|
|
344
|
+
detail: {
|
|
345
|
+
filePath,
|
|
346
|
+
cache: "missing-source"
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
snapshot: snapshot2,
|
|
351
|
+
cacheState: "missing-source"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const cached = this.snapshotCache.get(filePath);
|
|
355
|
+
if (cached?.sourceHash === environment.sourceHash) {
|
|
356
|
+
this.stats.fileSnapshotCacheHits += 1;
|
|
357
|
+
performance2.record({
|
|
358
|
+
name: "semantic.getFileSnapshot.result",
|
|
359
|
+
durationMs: getFormSpecPerformanceNow() - startedAt,
|
|
360
|
+
detail: {
|
|
361
|
+
filePath,
|
|
362
|
+
cache: "hit"
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
snapshot: cached.snapshot,
|
|
367
|
+
cacheState: "hit"
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
this.stats.fileSnapshotCacheMisses += 1;
|
|
371
|
+
const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {
|
|
372
|
+
checker: environment.checker,
|
|
373
|
+
now: () => this.getNow(),
|
|
374
|
+
performance: performance2
|
|
375
|
+
});
|
|
376
|
+
this.snapshotCache.set(filePath, {
|
|
377
|
+
sourceHash: environment.sourceHash,
|
|
378
|
+
snapshot
|
|
379
|
+
});
|
|
380
|
+
performance2.record({
|
|
381
|
+
name: "semantic.getFileSnapshot.result",
|
|
382
|
+
durationMs: getFormSpecPerformanceNow() - startedAt,
|
|
383
|
+
detail: {
|
|
384
|
+
filePath,
|
|
385
|
+
cache: "miss"
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
snapshot,
|
|
390
|
+
cacheState: "miss"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
getNow() {
|
|
394
|
+
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
395
|
+
}
|
|
396
|
+
getSourceEnvironment(filePath, performance2) {
|
|
397
|
+
return optionalMeasure(
|
|
398
|
+
performance2,
|
|
399
|
+
"semantic.getSourceEnvironment",
|
|
400
|
+
{
|
|
401
|
+
filePath
|
|
402
|
+
},
|
|
403
|
+
() => {
|
|
404
|
+
const program = optionalMeasure(
|
|
405
|
+
performance2,
|
|
406
|
+
"semantic.sourceEnvironment.getProgram",
|
|
407
|
+
void 0,
|
|
408
|
+
() => this.options.getProgram()
|
|
409
|
+
);
|
|
410
|
+
if (program === void 0) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const sourceFile = optionalMeasure(
|
|
414
|
+
performance2,
|
|
415
|
+
"semantic.sourceEnvironment.getSourceFile",
|
|
416
|
+
void 0,
|
|
417
|
+
() => program.getSourceFile(filePath)
|
|
418
|
+
);
|
|
419
|
+
if (sourceFile === void 0) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const checker = optionalMeasure(
|
|
423
|
+
performance2,
|
|
424
|
+
"semantic.sourceEnvironment.getTypeChecker",
|
|
425
|
+
void 0,
|
|
426
|
+
() => program.getTypeChecker()
|
|
427
|
+
);
|
|
428
|
+
const sourceHash = optionalMeasure(
|
|
429
|
+
performance2,
|
|
430
|
+
"semantic.sourceEnvironment.computeTextHash",
|
|
431
|
+
void 0,
|
|
432
|
+
() => computeFormSpecTextHash(sourceFile.text)
|
|
433
|
+
);
|
|
434
|
+
return {
|
|
435
|
+
sourceFile,
|
|
436
|
+
checker,
|
|
437
|
+
sourceHash
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
recordQueryPath(kind, cacheState) {
|
|
443
|
+
if (cacheState === "hit") {
|
|
444
|
+
this.stats.queryPathTotals[kind].warm += 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.stats.queryPathTotals[kind].cold += 1;
|
|
448
|
+
}
|
|
449
|
+
updateStatsFromPerformanceEvents(events) {
|
|
450
|
+
for (const event of events) {
|
|
451
|
+
if (event.name === "analysis.syntheticCheckBatch.cacheHit" || event.name === "analysis.narrowSyntheticCheckBatch.cacheHit") {
|
|
452
|
+
this.stats.syntheticBatchCacheHits += 1;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (event.name === "analysis.syntheticCheckBatch.cacheMiss" || event.name === "analysis.narrowSyntheticCheckBatch.cacheMiss") {
|
|
456
|
+
this.stats.syntheticBatchCacheMisses += 1;
|
|
457
|
+
const applicationCount = event.detail?.["applicationCount"];
|
|
458
|
+
if (typeof applicationCount === "number") {
|
|
459
|
+
this.stats.syntheticCompileApplications += applicationCount;
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (event.name === "analysis.syntheticCheckBatch.createProgram" || event.name === "analysis.narrowSyntheticCheckBatch.createProgram") {
|
|
464
|
+
this.stats.syntheticCompileCount += 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
logPerformanceEvents(rootEventName, events) {
|
|
469
|
+
const logger = this.options.logger;
|
|
470
|
+
if (logger === void 0 || events.length === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const rootEvent = [...events].reverse().find((event) => event.name === rootEventName);
|
|
474
|
+
if (rootEvent === void 0) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
|
|
478
|
+
if (rootEvent.durationMs < thresholdMs) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const sortedHotspots = [...events].filter((event) => event.name !== rootEventName).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
|
|
482
|
+
const lines = [
|
|
483
|
+
`[FormSpec][perf] ${formatPerformanceEvent(rootEvent)}`,
|
|
484
|
+
...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`)
|
|
485
|
+
];
|
|
486
|
+
logger.info(lines.join("\n"));
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
77
490
|
// src/service.ts
|
|
78
491
|
var FormSpecPluginService = class {
|
|
79
492
|
constructor(options) {
|
|
80
493
|
this.options = options;
|
|
494
|
+
this.semanticService = new FormSpecSemanticService(options);
|
|
81
495
|
this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
|
|
82
496
|
this.manifest = createFormSpecAnalysisManifest(
|
|
83
497
|
options.workspaceRoot,
|
|
@@ -87,12 +501,19 @@ var FormSpecPluginService = class {
|
|
|
87
501
|
}
|
|
88
502
|
manifest;
|
|
89
503
|
runtimePaths;
|
|
90
|
-
|
|
91
|
-
refreshTimers = /* @__PURE__ */ new Map();
|
|
504
|
+
semanticService;
|
|
92
505
|
server = null;
|
|
93
506
|
getManifest() {
|
|
94
507
|
return this.manifest;
|
|
95
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Returns the underlying semantic service used by this reference wrapper.
|
|
511
|
+
*
|
|
512
|
+
* @public
|
|
513
|
+
*/
|
|
514
|
+
getSemanticService() {
|
|
515
|
+
return this.semanticService;
|
|
516
|
+
}
|
|
96
517
|
async start() {
|
|
97
518
|
if (this.server !== null) {
|
|
98
519
|
return;
|
|
@@ -104,8 +525,25 @@ var FormSpecPluginService = class {
|
|
|
104
525
|
this.server = net.createServer((socket) => {
|
|
105
526
|
let buffer = "";
|
|
106
527
|
socket.setEncoding("utf8");
|
|
528
|
+
socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
|
|
529
|
+
this.options.logger?.info(
|
|
530
|
+
`[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
|
|
531
|
+
);
|
|
532
|
+
socket.destroy();
|
|
533
|
+
});
|
|
107
534
|
socket.on("data", (chunk) => {
|
|
108
535
|
buffer += String(chunk);
|
|
536
|
+
if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
|
|
537
|
+
socket.end(
|
|
538
|
+
`${JSON.stringify({
|
|
539
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
540
|
+
kind: "error",
|
|
541
|
+
error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
|
|
542
|
+
})}
|
|
543
|
+
`
|
|
544
|
+
);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
109
547
|
const newlineIndex = buffer.indexOf("\n");
|
|
110
548
|
if (newlineIndex < 0) {
|
|
111
549
|
return;
|
|
@@ -134,11 +572,7 @@ var FormSpecPluginService = class {
|
|
|
134
572
|
await this.writeManifest();
|
|
135
573
|
}
|
|
136
574
|
async stop() {
|
|
137
|
-
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
}
|
|
140
|
-
this.refreshTimers.clear();
|
|
141
|
-
this.snapshotCache.clear();
|
|
575
|
+
this.semanticService.dispose();
|
|
142
576
|
const server = this.server;
|
|
143
577
|
this.server = null;
|
|
144
578
|
if (server?.listening === true) {
|
|
@@ -155,96 +589,65 @@ var FormSpecPluginService = class {
|
|
|
155
589
|
await this.cleanupRuntimeArtifacts();
|
|
156
590
|
}
|
|
157
591
|
scheduleSnapshotRefresh(filePath) {
|
|
158
|
-
|
|
159
|
-
if (existing !== void 0) {
|
|
160
|
-
clearTimeout(existing);
|
|
161
|
-
}
|
|
162
|
-
const timer = setTimeout(() => {
|
|
163
|
-
try {
|
|
164
|
-
this.getFileSnapshot(filePath);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
this.options.logger?.info(
|
|
167
|
-
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
this.refreshTimers.delete(filePath);
|
|
171
|
-
}, this.options.snapshotDebounceMs ?? 250);
|
|
172
|
-
this.refreshTimers.set(filePath, timer);
|
|
592
|
+
this.semanticService.scheduleSnapshotRefresh(filePath);
|
|
173
593
|
}
|
|
174
594
|
handleQuery(query) {
|
|
595
|
+
if (this.options.enablePerformanceLogging === true) {
|
|
596
|
+
const startedAt = performance.now();
|
|
597
|
+
const response = this.executeQuery(query);
|
|
598
|
+
this.logQueryDuration(query, performance.now() - startedAt);
|
|
599
|
+
return response;
|
|
600
|
+
}
|
|
601
|
+
return this.executeQuery(query);
|
|
602
|
+
}
|
|
603
|
+
executeQuery(query) {
|
|
175
604
|
switch (query.kind) {
|
|
176
605
|
case "health":
|
|
177
606
|
return {
|
|
178
|
-
protocolVersion:
|
|
607
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
179
608
|
kind: "health",
|
|
180
609
|
manifest: this.manifest
|
|
181
610
|
};
|
|
182
611
|
case "completion": {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
612
|
+
const result = this.semanticService.getCompletionContext(query.filePath, query.offset);
|
|
613
|
+
if (result === null) {
|
|
185
614
|
return {
|
|
186
|
-
protocolVersion:
|
|
615
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
187
616
|
kind: "error",
|
|
188
617
|
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
189
618
|
};
|
|
190
619
|
}
|
|
191
|
-
const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);
|
|
192
|
-
const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);
|
|
193
|
-
const subjectType = declaration === null ? void 0 : getSubjectType(declaration, environment.checker);
|
|
194
|
-
const context = getSemanticCommentCompletionContextAtOffset(
|
|
195
|
-
environment.sourceFile.text,
|
|
196
|
-
query.offset,
|
|
197
|
-
{
|
|
198
|
-
checker: environment.checker,
|
|
199
|
-
...placement === null ? {} : { placement },
|
|
200
|
-
...subjectType === void 0 ? {} : { subjectType }
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
620
|
return {
|
|
204
|
-
|
|
205
|
-
kind: "completion"
|
|
206
|
-
sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
|
|
207
|
-
context: serializeCompletionContext(context)
|
|
621
|
+
...result,
|
|
622
|
+
kind: "completion"
|
|
208
623
|
};
|
|
209
624
|
}
|
|
210
625
|
case "hover": {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
626
|
+
const result = this.semanticService.getHover(query.filePath, query.offset);
|
|
627
|
+
if (result === null) {
|
|
213
628
|
return {
|
|
214
|
-
protocolVersion:
|
|
629
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
215
630
|
kind: "error",
|
|
216
631
|
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
217
632
|
};
|
|
218
633
|
}
|
|
219
|
-
const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);
|
|
220
|
-
const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);
|
|
221
|
-
const subjectType = declaration === null ? void 0 : getSubjectType(declaration, environment.checker);
|
|
222
|
-
const hover = getCommentHoverInfoAtOffset(environment.sourceFile.text, query.offset, {
|
|
223
|
-
checker: environment.checker,
|
|
224
|
-
...placement === null ? {} : { placement },
|
|
225
|
-
...subjectType === void 0 ? {} : { subjectType }
|
|
226
|
-
});
|
|
227
634
|
return {
|
|
228
|
-
|
|
229
|
-
kind: "hover"
|
|
230
|
-
sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
|
|
231
|
-
hover: serializeHoverInfo(hover)
|
|
635
|
+
...result,
|
|
636
|
+
kind: "hover"
|
|
232
637
|
};
|
|
233
638
|
}
|
|
234
639
|
case "diagnostics": {
|
|
235
|
-
const
|
|
640
|
+
const result = this.semanticService.getDiagnostics(query.filePath);
|
|
236
641
|
return {
|
|
237
|
-
|
|
238
|
-
kind: "diagnostics"
|
|
239
|
-
sourceHash: snapshot.sourceHash,
|
|
240
|
-
diagnostics: snapshot.diagnostics
|
|
642
|
+
...result,
|
|
643
|
+
kind: "diagnostics"
|
|
241
644
|
};
|
|
242
645
|
}
|
|
243
646
|
case "file-snapshot":
|
|
244
647
|
return {
|
|
245
|
-
protocolVersion:
|
|
648
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
246
649
|
kind: "file-snapshot",
|
|
247
|
-
snapshot: this.getFileSnapshot(query.filePath)
|
|
650
|
+
snapshot: this.semanticService.getFileSnapshot(query.filePath)
|
|
248
651
|
};
|
|
249
652
|
default: {
|
|
250
653
|
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
@@ -263,7 +666,7 @@ var FormSpecPluginService = class {
|
|
|
263
666
|
} catch (error) {
|
|
264
667
|
socket.end(
|
|
265
668
|
`${JSON.stringify({
|
|
266
|
-
protocolVersion:
|
|
669
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
|
|
267
670
|
kind: "error",
|
|
268
671
|
error: error instanceof Error ? error.message : String(error)
|
|
269
672
|
})}
|
|
@@ -283,54 +686,24 @@ var FormSpecPluginService = class {
|
|
|
283
686
|
await fs.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
284
687
|
}
|
|
285
688
|
}
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
return
|
|
689
|
+
logQueryDuration(query, durationMs) {
|
|
690
|
+
const logger = this.options.logger;
|
|
691
|
+
if (logger === void 0) {
|
|
692
|
+
return;
|
|
290
693
|
}
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
return
|
|
694
|
+
const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
|
|
695
|
+
if (durationMs < thresholdMs) {
|
|
696
|
+
return;
|
|
294
697
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
698
|
+
const event = {
|
|
699
|
+
name: "plugin.handleQuery",
|
|
700
|
+
durationMs,
|
|
701
|
+
detail: {
|
|
702
|
+
kind: query.kind,
|
|
703
|
+
...query.kind === "health" ? {} : { filePath: query.filePath }
|
|
704
|
+
}
|
|
298
705
|
};
|
|
299
|
-
|
|
300
|
-
getFileSnapshot(filePath) {
|
|
301
|
-
const environment = this.getSourceEnvironment(filePath);
|
|
302
|
-
if (environment === null) {
|
|
303
|
-
return {
|
|
304
|
-
filePath,
|
|
305
|
-
sourceHash: "",
|
|
306
|
-
generatedAt: this.getNow().toISOString(),
|
|
307
|
-
comments: [],
|
|
308
|
-
diagnostics: [
|
|
309
|
-
{
|
|
310
|
-
code: "MISSING_SOURCE_FILE",
|
|
311
|
-
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
312
|
-
range: { start: 0, end: 0 },
|
|
313
|
-
severity: "warning"
|
|
314
|
-
}
|
|
315
|
-
]
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);
|
|
319
|
-
const cached = this.snapshotCache.get(filePath);
|
|
320
|
-
if (cached?.sourceHash === sourceHash) {
|
|
321
|
-
return cached.snapshot;
|
|
322
|
-
}
|
|
323
|
-
const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {
|
|
324
|
-
checker: environment.checker
|
|
325
|
-
});
|
|
326
|
-
this.snapshotCache.set(filePath, {
|
|
327
|
-
sourceHash,
|
|
328
|
-
snapshot
|
|
329
|
-
});
|
|
330
|
-
return snapshot;
|
|
331
|
-
}
|
|
332
|
-
getNow() {
|
|
333
|
-
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
706
|
+
logger.info(`[FormSpec][perf] ${formatPerformanceEvent(event)}`);
|
|
334
707
|
}
|
|
335
708
|
};
|
|
336
709
|
function createLanguageServiceProxy(languageService, semanticService) {
|
|
@@ -367,9 +740,23 @@ function createLanguageServiceProxy(languageService, semanticService) {
|
|
|
367
740
|
|
|
368
741
|
// src/index.ts
|
|
369
742
|
var services = /* @__PURE__ */ new Map();
|
|
743
|
+
var PERF_LOG_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE";
|
|
744
|
+
var PERF_LOG_THRESHOLD_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS";
|
|
370
745
|
function formatPluginError(error) {
|
|
371
746
|
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
372
747
|
}
|
|
748
|
+
function readBooleanEnvFlag(name) {
|
|
749
|
+
const rawValue = process.env[name];
|
|
750
|
+
return rawValue === "1" || rawValue === "true";
|
|
751
|
+
}
|
|
752
|
+
function readNumberEnvFlag(name) {
|
|
753
|
+
const rawValue = process.env[name];
|
|
754
|
+
if (rawValue === void 0 || rawValue.trim() === "") {
|
|
755
|
+
return void 0;
|
|
756
|
+
}
|
|
757
|
+
const parsed = Number(rawValue);
|
|
758
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
759
|
+
}
|
|
373
760
|
function getOrCreateService(info, typescriptVersion) {
|
|
374
761
|
const workspaceRoot = info.project.getCurrentDirectory();
|
|
375
762
|
const existing = services.get(workspaceRoot);
|
|
@@ -378,11 +765,14 @@ function getOrCreateService(info, typescriptVersion) {
|
|
|
378
765
|
attachProjectCloseHandler(info, workspaceRoot, existing);
|
|
379
766
|
return existing.service;
|
|
380
767
|
}
|
|
768
|
+
const performanceLogThresholdMs = readNumberEnvFlag(PERF_LOG_THRESHOLD_ENV_VAR);
|
|
381
769
|
const service = new FormSpecPluginService({
|
|
382
770
|
workspaceRoot,
|
|
383
771
|
typescriptVersion,
|
|
384
772
|
getProgram: () => info.languageService.getProgram(),
|
|
385
|
-
logger: info.project.projectService.logger
|
|
773
|
+
logger: info.project.projectService.logger,
|
|
774
|
+
enablePerformanceLogging: readBooleanEnvFlag(PERF_LOG_ENV_VAR),
|
|
775
|
+
...performanceLogThresholdMs === void 0 ? {} : { performanceLogThresholdMs }
|
|
386
776
|
});
|
|
387
777
|
const serviceEntry = {
|
|
388
778
|
service,
|
|
@@ -424,11 +814,16 @@ function init(modules) {
|
|
|
424
814
|
return {
|
|
425
815
|
create(info) {
|
|
426
816
|
const service = getOrCreateService(info, typescriptVersion);
|
|
427
|
-
return createLanguageServiceProxy(info.languageService, service);
|
|
817
|
+
return createLanguageServiceProxy(info.languageService, service.getSemanticService());
|
|
428
818
|
}
|
|
429
819
|
};
|
|
430
820
|
}
|
|
431
821
|
export {
|
|
822
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION4 as FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
823
|
+
FORMSPEC_ANALYSIS_SCHEMA_VERSION2 as FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
824
|
+
FormSpecPluginService,
|
|
825
|
+
FormSpecSemanticService,
|
|
826
|
+
createLanguageServiceProxy,
|
|
432
827
|
init
|
|
433
828
|
};
|
|
434
829
|
//# sourceMappingURL=index.js.map
|