@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.cjs
CHANGED
|
@@ -30,23 +30,30 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION: () => import_protocol4.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
34
|
+
FORMSPEC_ANALYSIS_SCHEMA_VERSION: () => import_protocol4.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
35
|
+
FormSpecPluginService: () => FormSpecPluginService,
|
|
36
|
+
FormSpecSemanticService: () => FormSpecSemanticService,
|
|
37
|
+
createLanguageServiceProxy: () => createLanguageServiceProxy,
|
|
33
38
|
init: () => init
|
|
34
39
|
});
|
|
35
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
var import_protocol4 = require("@formspec/analysis/protocol");
|
|
36
42
|
|
|
37
43
|
// src/service.ts
|
|
38
44
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
39
45
|
var import_node_net = __toESM(require("net"), 1);
|
|
40
|
-
var
|
|
41
|
-
var
|
|
46
|
+
var ts2 = require("typescript");
|
|
47
|
+
var import_protocol3 = require("@formspec/analysis/protocol");
|
|
48
|
+
var import_internal2 = require("@formspec/analysis/internal");
|
|
42
49
|
|
|
43
50
|
// src/workspace.ts
|
|
44
51
|
var import_node_os = __toESM(require("os"), 1);
|
|
45
52
|
var import_node_path = __toESM(require("path"), 1);
|
|
46
|
-
var
|
|
53
|
+
var import_protocol = require("@formspec/analysis/protocol");
|
|
47
54
|
function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
|
|
48
|
-
const workspaceId = (0,
|
|
49
|
-
const runtimeDirectory = (0,
|
|
55
|
+
const workspaceId = (0, import_protocol.getFormSpecWorkspaceId)(workspaceRoot);
|
|
56
|
+
const runtimeDirectory = (0, import_protocol.getFormSpecWorkspaceRuntimeDirectory)(workspaceRoot);
|
|
50
57
|
const sanitizedUserScope = sanitizeScopeSegment(userScope);
|
|
51
58
|
const endpoint = platform === "win32" ? {
|
|
52
59
|
kind: "windows-pipe",
|
|
@@ -59,15 +66,15 @@ function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.plat
|
|
|
59
66
|
workspaceRoot,
|
|
60
67
|
workspaceId,
|
|
61
68
|
runtimeDirectory,
|
|
62
|
-
manifestPath: (0,
|
|
69
|
+
manifestPath: (0, import_protocol.getFormSpecManifestPath)(workspaceRoot),
|
|
63
70
|
endpoint
|
|
64
71
|
};
|
|
65
72
|
}
|
|
66
73
|
function createFormSpecAnalysisManifest(workspaceRoot, typescriptVersion, generation, extensionFingerprint = "builtin") {
|
|
67
74
|
const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);
|
|
68
75
|
return {
|
|
69
|
-
protocolVersion:
|
|
70
|
-
analysisSchemaVersion:
|
|
76
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
77
|
+
analysisSchemaVersion: import_protocol.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
71
78
|
workspaceRoot,
|
|
72
79
|
workspaceId: paths.workspaceId,
|
|
73
80
|
endpoint: paths.endpoint,
|
|
@@ -92,10 +99,411 @@ function sanitizeScopeSegment(value) {
|
|
|
92
99
|
return sanitized.length > 0 ? sanitized : "formspec";
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
// src/semantic-service.ts
|
|
103
|
+
var ts = require("typescript");
|
|
104
|
+
var import_protocol2 = require("@formspec/analysis/protocol");
|
|
105
|
+
var import_internal = require("@formspec/analysis/internal");
|
|
106
|
+
|
|
107
|
+
// src/constants.ts
|
|
108
|
+
var FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;
|
|
109
|
+
var FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 3e4;
|
|
110
|
+
var FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
|
|
111
|
+
var FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
|
|
112
|
+
|
|
113
|
+
// src/perf-utils.ts
|
|
114
|
+
function formatPerformanceEvent(event) {
|
|
115
|
+
const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
|
|
116
|
+
return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/semantic-service.ts
|
|
120
|
+
var STATS_ONLY_EVENT_NAMES = /* @__PURE__ */ new Set([
|
|
121
|
+
"analysis.syntheticCheckBatch.cacheHit",
|
|
122
|
+
"analysis.narrowSyntheticCheckBatch.cacheHit",
|
|
123
|
+
"analysis.syntheticCheckBatch.cacheMiss",
|
|
124
|
+
"analysis.narrowSyntheticCheckBatch.cacheMiss",
|
|
125
|
+
"analysis.syntheticCheckBatch.createProgram",
|
|
126
|
+
"analysis.narrowSyntheticCheckBatch.createProgram"
|
|
127
|
+
]);
|
|
128
|
+
var StatsOnlyPerformanceRecorder = class {
|
|
129
|
+
mutableEvents = [];
|
|
130
|
+
get events() {
|
|
131
|
+
return this.mutableEvents;
|
|
132
|
+
}
|
|
133
|
+
measure(name, detail, callback) {
|
|
134
|
+
const result = callback();
|
|
135
|
+
if (STATS_ONLY_EVENT_NAMES.has(name)) {
|
|
136
|
+
this.mutableEvents.push({
|
|
137
|
+
name,
|
|
138
|
+
durationMs: 0,
|
|
139
|
+
...detail === void 0 ? {} : { detail }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
record(event) {
|
|
145
|
+
if (STATS_ONLY_EVENT_NAMES.has(event.name)) {
|
|
146
|
+
this.mutableEvents.push(event);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var FormSpecSemanticService = class {
|
|
151
|
+
constructor(options) {
|
|
152
|
+
this.options = options;
|
|
153
|
+
}
|
|
154
|
+
snapshotCache = /* @__PURE__ */ new Map();
|
|
155
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
156
|
+
stats = {
|
|
157
|
+
queryTotals: {
|
|
158
|
+
completion: 0,
|
|
159
|
+
hover: 0,
|
|
160
|
+
diagnostics: 0,
|
|
161
|
+
fileSnapshot: 0
|
|
162
|
+
},
|
|
163
|
+
queryPathTotals: {
|
|
164
|
+
diagnostics: { cold: 0, warm: 0 },
|
|
165
|
+
fileSnapshot: { cold: 0, warm: 0 }
|
|
166
|
+
},
|
|
167
|
+
fileSnapshotCacheHits: 0,
|
|
168
|
+
fileSnapshotCacheMisses: 0,
|
|
169
|
+
syntheticBatchCacheHits: 0,
|
|
170
|
+
syntheticBatchCacheMisses: 0,
|
|
171
|
+
syntheticCompileCount: 0,
|
|
172
|
+
syntheticCompileApplications: 0
|
|
173
|
+
};
|
|
174
|
+
/** Resolves semantic completion context for a comment cursor position. */
|
|
175
|
+
getCompletionContext(filePath, offset) {
|
|
176
|
+
this.stats.queryTotals.completion += 1;
|
|
177
|
+
return this.runMeasured(
|
|
178
|
+
"semantic.getCompletionContext",
|
|
179
|
+
{ filePath, offset },
|
|
180
|
+
(performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
|
|
181
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
182
|
+
sourceHash: context.sourceHash,
|
|
183
|
+
context: (0, import_internal.serializeCompletionContext)(
|
|
184
|
+
(0, import_internal.getSemanticCommentCompletionContextAtOffset)(context.sourceFile.text, offset, {
|
|
185
|
+
checker: context.checker,
|
|
186
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
187
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
}))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
/** Resolves semantic hover payload for a comment cursor position. */
|
|
194
|
+
getHover(filePath, offset) {
|
|
195
|
+
this.stats.queryTotals.hover += 1;
|
|
196
|
+
return this.runMeasured(
|
|
197
|
+
"semantic.getHover",
|
|
198
|
+
{ filePath, offset },
|
|
199
|
+
(performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
|
|
200
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
201
|
+
sourceHash: context.sourceHash,
|
|
202
|
+
hover: (0, import_internal.serializeHoverInfo)(
|
|
203
|
+
(0, import_internal.getCommentHoverInfoAtOffset)(context.sourceFile.text, offset, {
|
|
204
|
+
checker: context.checker,
|
|
205
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
206
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
}))
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
/** Returns canonical FormSpec diagnostics for a file in the current host program. */
|
|
213
|
+
getDiagnostics(filePath) {
|
|
214
|
+
this.stats.queryTotals.diagnostics += 1;
|
|
215
|
+
return this.runMeasured("semantic.getDiagnostics", { filePath }, (performance2) => {
|
|
216
|
+
const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
|
|
217
|
+
this.recordQueryPath("diagnostics", cacheState);
|
|
218
|
+
return {
|
|
219
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
220
|
+
sourceHash: snapshot.sourceHash,
|
|
221
|
+
diagnostics: snapshot.diagnostics
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** Returns the full serialized semantic snapshot for a file. */
|
|
226
|
+
getFileSnapshot(filePath) {
|
|
227
|
+
this.stats.queryTotals.fileSnapshot += 1;
|
|
228
|
+
return this.runMeasured("semantic.getFileSnapshot", { filePath }, (performance2) => {
|
|
229
|
+
const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
|
|
230
|
+
this.recordQueryPath("fileSnapshot", cacheState);
|
|
231
|
+
return snapshot;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/** Schedules a debounced background refresh for the file snapshot cache. */
|
|
235
|
+
scheduleSnapshotRefresh(filePath) {
|
|
236
|
+
const existing = this.refreshTimers.get(filePath);
|
|
237
|
+
if (existing !== void 0) {
|
|
238
|
+
clearTimeout(existing);
|
|
239
|
+
}
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
try {
|
|
242
|
+
this.getFileSnapshot(filePath);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.options.logger?.info(
|
|
245
|
+
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
this.refreshTimers.delete(filePath);
|
|
249
|
+
}, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);
|
|
250
|
+
timer.unref();
|
|
251
|
+
this.refreshTimers.set(filePath, timer);
|
|
252
|
+
}
|
|
253
|
+
/** Clears pending timers and cached semantic snapshots. */
|
|
254
|
+
dispose() {
|
|
255
|
+
for (const timer of this.refreshTimers.values()) {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
}
|
|
258
|
+
this.refreshTimers.clear();
|
|
259
|
+
this.snapshotCache.clear();
|
|
260
|
+
}
|
|
261
|
+
/** Returns a copy of the current performance and cache counters. */
|
|
262
|
+
getStats() {
|
|
263
|
+
return {
|
|
264
|
+
queryTotals: { ...this.stats.queryTotals },
|
|
265
|
+
queryPathTotals: {
|
|
266
|
+
diagnostics: { ...this.stats.queryPathTotals.diagnostics },
|
|
267
|
+
fileSnapshot: { ...this.stats.queryPathTotals.fileSnapshot }
|
|
268
|
+
},
|
|
269
|
+
fileSnapshotCacheHits: this.stats.fileSnapshotCacheHits,
|
|
270
|
+
fileSnapshotCacheMisses: this.stats.fileSnapshotCacheMisses,
|
|
271
|
+
syntheticBatchCacheHits: this.stats.syntheticBatchCacheHits,
|
|
272
|
+
syntheticBatchCacheMisses: this.stats.syntheticBatchCacheMisses,
|
|
273
|
+
syntheticCompileCount: this.stats.syntheticCompileCount,
|
|
274
|
+
syntheticCompileApplications: this.stats.syntheticCompileApplications
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
runMeasured(name, detail, fn) {
|
|
278
|
+
const performance2 = this.options.enablePerformanceLogging === true ? (0, import_internal.createFormSpecPerformanceRecorder)() : new StatsOnlyPerformanceRecorder();
|
|
279
|
+
const result = (0, import_internal.optionalMeasure)(performance2, name, detail, () => fn(performance2));
|
|
280
|
+
this.updateStatsFromPerformanceEvents(performance2.events);
|
|
281
|
+
if (this.options.enablePerformanceLogging === true) {
|
|
282
|
+
this.logPerformanceEvents(name, performance2.events);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
withCommentQueryContext(filePath, offset, performance2, handler) {
|
|
287
|
+
return (0, import_internal.optionalMeasure)(
|
|
288
|
+
performance2,
|
|
289
|
+
"semantic.resolveCommentQueryContext",
|
|
290
|
+
{
|
|
291
|
+
filePath,
|
|
292
|
+
offset
|
|
293
|
+
},
|
|
294
|
+
() => {
|
|
295
|
+
const environment = this.getSourceEnvironment(filePath, performance2);
|
|
296
|
+
if (environment === null) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const declaration = (0, import_internal.optionalMeasure)(
|
|
300
|
+
performance2,
|
|
301
|
+
"semantic.findDeclarationForCommentOffset",
|
|
302
|
+
{
|
|
303
|
+
filePath,
|
|
304
|
+
offset
|
|
305
|
+
},
|
|
306
|
+
() => (0, import_internal.findDeclarationForCommentOffset)(environment.sourceFile, offset)
|
|
307
|
+
);
|
|
308
|
+
const placement = declaration === null ? null : (0, import_internal.optionalMeasure)(
|
|
309
|
+
performance2,
|
|
310
|
+
"semantic.resolveDeclarationPlacement",
|
|
311
|
+
void 0,
|
|
312
|
+
() => (0, import_internal.resolveDeclarationPlacement)(declaration)
|
|
313
|
+
);
|
|
314
|
+
const subjectType = declaration === null ? void 0 : (0, import_internal.optionalMeasure)(
|
|
315
|
+
performance2,
|
|
316
|
+
"semantic.getSubjectType",
|
|
317
|
+
void 0,
|
|
318
|
+
() => (0, import_internal.getSubjectType)(declaration, environment.checker)
|
|
319
|
+
);
|
|
320
|
+
return handler({
|
|
321
|
+
...environment,
|
|
322
|
+
declaration,
|
|
323
|
+
placement,
|
|
324
|
+
subjectType
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
getFileSnapshotWithCacheState(filePath, performance2) {
|
|
330
|
+
const startedAt = (0, import_internal.getFormSpecPerformanceNow)();
|
|
331
|
+
const environment = this.getSourceEnvironment(filePath, performance2);
|
|
332
|
+
if (environment === null) {
|
|
333
|
+
this.stats.fileSnapshotCacheMisses += 1;
|
|
334
|
+
const snapshot2 = {
|
|
335
|
+
filePath,
|
|
336
|
+
sourceHash: "",
|
|
337
|
+
generatedAt: this.getNow().toISOString(),
|
|
338
|
+
comments: [],
|
|
339
|
+
diagnostics: [
|
|
340
|
+
{
|
|
341
|
+
code: "MISSING_SOURCE_FILE",
|
|
342
|
+
category: "infrastructure",
|
|
343
|
+
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
344
|
+
range: { start: 0, end: 0 },
|
|
345
|
+
severity: "warning",
|
|
346
|
+
relatedLocations: [],
|
|
347
|
+
data: {
|
|
348
|
+
filePath
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
};
|
|
353
|
+
performance2.record({
|
|
354
|
+
name: "semantic.getFileSnapshot.result",
|
|
355
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
356
|
+
detail: {
|
|
357
|
+
filePath,
|
|
358
|
+
cache: "missing-source"
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
snapshot: snapshot2,
|
|
363
|
+
cacheState: "missing-source"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const cached = this.snapshotCache.get(filePath);
|
|
367
|
+
if (cached?.sourceHash === environment.sourceHash) {
|
|
368
|
+
this.stats.fileSnapshotCacheHits += 1;
|
|
369
|
+
performance2.record({
|
|
370
|
+
name: "semantic.getFileSnapshot.result",
|
|
371
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
372
|
+
detail: {
|
|
373
|
+
filePath,
|
|
374
|
+
cache: "hit"
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
return {
|
|
378
|
+
snapshot: cached.snapshot,
|
|
379
|
+
cacheState: "hit"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
this.stats.fileSnapshotCacheMisses += 1;
|
|
383
|
+
const snapshot = (0, import_internal.buildFormSpecAnalysisFileSnapshot)(environment.sourceFile, {
|
|
384
|
+
checker: environment.checker,
|
|
385
|
+
now: () => this.getNow(),
|
|
386
|
+
performance: performance2
|
|
387
|
+
});
|
|
388
|
+
this.snapshotCache.set(filePath, {
|
|
389
|
+
sourceHash: environment.sourceHash,
|
|
390
|
+
snapshot
|
|
391
|
+
});
|
|
392
|
+
performance2.record({
|
|
393
|
+
name: "semantic.getFileSnapshot.result",
|
|
394
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
395
|
+
detail: {
|
|
396
|
+
filePath,
|
|
397
|
+
cache: "miss"
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
snapshot,
|
|
402
|
+
cacheState: "miss"
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
getNow() {
|
|
406
|
+
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
407
|
+
}
|
|
408
|
+
getSourceEnvironment(filePath, performance2) {
|
|
409
|
+
return (0, import_internal.optionalMeasure)(
|
|
410
|
+
performance2,
|
|
411
|
+
"semantic.getSourceEnvironment",
|
|
412
|
+
{
|
|
413
|
+
filePath
|
|
414
|
+
},
|
|
415
|
+
() => {
|
|
416
|
+
const program = (0, import_internal.optionalMeasure)(
|
|
417
|
+
performance2,
|
|
418
|
+
"semantic.sourceEnvironment.getProgram",
|
|
419
|
+
void 0,
|
|
420
|
+
() => this.options.getProgram()
|
|
421
|
+
);
|
|
422
|
+
if (program === void 0) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const sourceFile = (0, import_internal.optionalMeasure)(
|
|
426
|
+
performance2,
|
|
427
|
+
"semantic.sourceEnvironment.getSourceFile",
|
|
428
|
+
void 0,
|
|
429
|
+
() => program.getSourceFile(filePath)
|
|
430
|
+
);
|
|
431
|
+
if (sourceFile === void 0) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const checker = (0, import_internal.optionalMeasure)(
|
|
435
|
+
performance2,
|
|
436
|
+
"semantic.sourceEnvironment.getTypeChecker",
|
|
437
|
+
void 0,
|
|
438
|
+
() => program.getTypeChecker()
|
|
439
|
+
);
|
|
440
|
+
const sourceHash = (0, import_internal.optionalMeasure)(
|
|
441
|
+
performance2,
|
|
442
|
+
"semantic.sourceEnvironment.computeTextHash",
|
|
443
|
+
void 0,
|
|
444
|
+
() => (0, import_protocol2.computeFormSpecTextHash)(sourceFile.text)
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
sourceFile,
|
|
448
|
+
checker,
|
|
449
|
+
sourceHash
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
recordQueryPath(kind, cacheState) {
|
|
455
|
+
if (cacheState === "hit") {
|
|
456
|
+
this.stats.queryPathTotals[kind].warm += 1;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.stats.queryPathTotals[kind].cold += 1;
|
|
460
|
+
}
|
|
461
|
+
updateStatsFromPerformanceEvents(events) {
|
|
462
|
+
for (const event of events) {
|
|
463
|
+
if (event.name === "analysis.syntheticCheckBatch.cacheHit" || event.name === "analysis.narrowSyntheticCheckBatch.cacheHit") {
|
|
464
|
+
this.stats.syntheticBatchCacheHits += 1;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (event.name === "analysis.syntheticCheckBatch.cacheMiss" || event.name === "analysis.narrowSyntheticCheckBatch.cacheMiss") {
|
|
468
|
+
this.stats.syntheticBatchCacheMisses += 1;
|
|
469
|
+
const applicationCount = event.detail?.["applicationCount"];
|
|
470
|
+
if (typeof applicationCount === "number") {
|
|
471
|
+
this.stats.syntheticCompileApplications += applicationCount;
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (event.name === "analysis.syntheticCheckBatch.createProgram" || event.name === "analysis.narrowSyntheticCheckBatch.createProgram") {
|
|
476
|
+
this.stats.syntheticCompileCount += 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
logPerformanceEvents(rootEventName, events) {
|
|
481
|
+
const logger = this.options.logger;
|
|
482
|
+
if (logger === void 0 || events.length === 0) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const rootEvent = [...events].reverse().find((event) => event.name === rootEventName);
|
|
486
|
+
if (rootEvent === void 0) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
|
|
490
|
+
if (rootEvent.durationMs < thresholdMs) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const sortedHotspots = [...events].filter((event) => event.name !== rootEventName).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
|
|
494
|
+
const lines = [
|
|
495
|
+
`[FormSpec][perf] ${formatPerformanceEvent(rootEvent)}`,
|
|
496
|
+
...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`)
|
|
497
|
+
];
|
|
498
|
+
logger.info(lines.join("\n"));
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
95
502
|
// src/service.ts
|
|
96
503
|
var FormSpecPluginService = class {
|
|
97
504
|
constructor(options) {
|
|
98
505
|
this.options = options;
|
|
506
|
+
this.semanticService = new FormSpecSemanticService(options);
|
|
99
507
|
this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
|
|
100
508
|
this.manifest = createFormSpecAnalysisManifest(
|
|
101
509
|
options.workspaceRoot,
|
|
@@ -105,12 +513,19 @@ var FormSpecPluginService = class {
|
|
|
105
513
|
}
|
|
106
514
|
manifest;
|
|
107
515
|
runtimePaths;
|
|
108
|
-
|
|
109
|
-
refreshTimers = /* @__PURE__ */ new Map();
|
|
516
|
+
semanticService;
|
|
110
517
|
server = null;
|
|
111
518
|
getManifest() {
|
|
112
519
|
return this.manifest;
|
|
113
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Returns the underlying semantic service used by this reference wrapper.
|
|
523
|
+
*
|
|
524
|
+
* @public
|
|
525
|
+
*/
|
|
526
|
+
getSemanticService() {
|
|
527
|
+
return this.semanticService;
|
|
528
|
+
}
|
|
114
529
|
async start() {
|
|
115
530
|
if (this.server !== null) {
|
|
116
531
|
return;
|
|
@@ -122,8 +537,25 @@ var FormSpecPluginService = class {
|
|
|
122
537
|
this.server = import_node_net.default.createServer((socket) => {
|
|
123
538
|
let buffer = "";
|
|
124
539
|
socket.setEncoding("utf8");
|
|
540
|
+
socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
|
|
541
|
+
this.options.logger?.info(
|
|
542
|
+
`[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
|
|
543
|
+
);
|
|
544
|
+
socket.destroy();
|
|
545
|
+
});
|
|
125
546
|
socket.on("data", (chunk) => {
|
|
126
547
|
buffer += String(chunk);
|
|
548
|
+
if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
|
|
549
|
+
socket.end(
|
|
550
|
+
`${JSON.stringify({
|
|
551
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
552
|
+
kind: "error",
|
|
553
|
+
error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
|
|
554
|
+
})}
|
|
555
|
+
`
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
127
559
|
const newlineIndex = buffer.indexOf("\n");
|
|
128
560
|
if (newlineIndex < 0) {
|
|
129
561
|
return;
|
|
@@ -152,11 +584,7 @@ var FormSpecPluginService = class {
|
|
|
152
584
|
await this.writeManifest();
|
|
153
585
|
}
|
|
154
586
|
async stop() {
|
|
155
|
-
|
|
156
|
-
clearTimeout(timer);
|
|
157
|
-
}
|
|
158
|
-
this.refreshTimers.clear();
|
|
159
|
-
this.snapshotCache.clear();
|
|
587
|
+
this.semanticService.dispose();
|
|
160
588
|
const server = this.server;
|
|
161
589
|
this.server = null;
|
|
162
590
|
if (server?.listening === true) {
|
|
@@ -173,96 +601,65 @@ var FormSpecPluginService = class {
|
|
|
173
601
|
await this.cleanupRuntimeArtifacts();
|
|
174
602
|
}
|
|
175
603
|
scheduleSnapshotRefresh(filePath) {
|
|
176
|
-
|
|
177
|
-
if (existing !== void 0) {
|
|
178
|
-
clearTimeout(existing);
|
|
179
|
-
}
|
|
180
|
-
const timer = setTimeout(() => {
|
|
181
|
-
try {
|
|
182
|
-
this.getFileSnapshot(filePath);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
this.options.logger?.info(
|
|
185
|
-
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
this.refreshTimers.delete(filePath);
|
|
189
|
-
}, this.options.snapshotDebounceMs ?? 250);
|
|
190
|
-
this.refreshTimers.set(filePath, timer);
|
|
604
|
+
this.semanticService.scheduleSnapshotRefresh(filePath);
|
|
191
605
|
}
|
|
192
606
|
handleQuery(query) {
|
|
607
|
+
if (this.options.enablePerformanceLogging === true) {
|
|
608
|
+
const startedAt = performance.now();
|
|
609
|
+
const response = this.executeQuery(query);
|
|
610
|
+
this.logQueryDuration(query, performance.now() - startedAt);
|
|
611
|
+
return response;
|
|
612
|
+
}
|
|
613
|
+
return this.executeQuery(query);
|
|
614
|
+
}
|
|
615
|
+
executeQuery(query) {
|
|
193
616
|
switch (query.kind) {
|
|
194
617
|
case "health":
|
|
195
618
|
return {
|
|
196
|
-
protocolVersion:
|
|
619
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
197
620
|
kind: "health",
|
|
198
621
|
manifest: this.manifest
|
|
199
622
|
};
|
|
200
623
|
case "completion": {
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
624
|
+
const result = this.semanticService.getCompletionContext(query.filePath, query.offset);
|
|
625
|
+
if (result === null) {
|
|
203
626
|
return {
|
|
204
|
-
protocolVersion:
|
|
627
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
205
628
|
kind: "error",
|
|
206
629
|
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
207
630
|
};
|
|
208
631
|
}
|
|
209
|
-
const declaration = (0, import_analysis2.findDeclarationForCommentOffset)(environment.sourceFile, query.offset);
|
|
210
|
-
const placement = declaration === null ? null : (0, import_analysis2.resolveDeclarationPlacement)(declaration);
|
|
211
|
-
const subjectType = declaration === null ? void 0 : (0, import_analysis2.getSubjectType)(declaration, environment.checker);
|
|
212
|
-
const context = (0, import_analysis2.getSemanticCommentCompletionContextAtOffset)(
|
|
213
|
-
environment.sourceFile.text,
|
|
214
|
-
query.offset,
|
|
215
|
-
{
|
|
216
|
-
checker: environment.checker,
|
|
217
|
-
...placement === null ? {} : { placement },
|
|
218
|
-
...subjectType === void 0 ? {} : { subjectType }
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
632
|
return {
|
|
222
|
-
|
|
223
|
-
kind: "completion"
|
|
224
|
-
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
225
|
-
context: (0, import_analysis2.serializeCompletionContext)(context)
|
|
633
|
+
...result,
|
|
634
|
+
kind: "completion"
|
|
226
635
|
};
|
|
227
636
|
}
|
|
228
637
|
case "hover": {
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
638
|
+
const result = this.semanticService.getHover(query.filePath, query.offset);
|
|
639
|
+
if (result === null) {
|
|
231
640
|
return {
|
|
232
|
-
protocolVersion:
|
|
641
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
233
642
|
kind: "error",
|
|
234
643
|
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
235
644
|
};
|
|
236
645
|
}
|
|
237
|
-
const declaration = (0, import_analysis2.findDeclarationForCommentOffset)(environment.sourceFile, query.offset);
|
|
238
|
-
const placement = declaration === null ? null : (0, import_analysis2.resolveDeclarationPlacement)(declaration);
|
|
239
|
-
const subjectType = declaration === null ? void 0 : (0, import_analysis2.getSubjectType)(declaration, environment.checker);
|
|
240
|
-
const hover = (0, import_analysis2.getCommentHoverInfoAtOffset)(environment.sourceFile.text, query.offset, {
|
|
241
|
-
checker: environment.checker,
|
|
242
|
-
...placement === null ? {} : { placement },
|
|
243
|
-
...subjectType === void 0 ? {} : { subjectType }
|
|
244
|
-
});
|
|
245
646
|
return {
|
|
246
|
-
|
|
247
|
-
kind: "hover"
|
|
248
|
-
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
249
|
-
hover: (0, import_analysis2.serializeHoverInfo)(hover)
|
|
647
|
+
...result,
|
|
648
|
+
kind: "hover"
|
|
250
649
|
};
|
|
251
650
|
}
|
|
252
651
|
case "diagnostics": {
|
|
253
|
-
const
|
|
652
|
+
const result = this.semanticService.getDiagnostics(query.filePath);
|
|
254
653
|
return {
|
|
255
|
-
|
|
256
|
-
kind: "diagnostics"
|
|
257
|
-
sourceHash: snapshot.sourceHash,
|
|
258
|
-
diagnostics: snapshot.diagnostics
|
|
654
|
+
...result,
|
|
655
|
+
kind: "diagnostics"
|
|
259
656
|
};
|
|
260
657
|
}
|
|
261
658
|
case "file-snapshot":
|
|
262
659
|
return {
|
|
263
|
-
protocolVersion:
|
|
660
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
264
661
|
kind: "file-snapshot",
|
|
265
|
-
snapshot: this.getFileSnapshot(query.filePath)
|
|
662
|
+
snapshot: this.semanticService.getFileSnapshot(query.filePath)
|
|
266
663
|
};
|
|
267
664
|
default: {
|
|
268
665
|
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
@@ -272,7 +669,7 @@ var FormSpecPluginService = class {
|
|
|
272
669
|
respondToSocket(socket, payload) {
|
|
273
670
|
try {
|
|
274
671
|
const query = JSON.parse(payload);
|
|
275
|
-
if (!(0,
|
|
672
|
+
if (!(0, import_protocol3.isFormSpecSemanticQuery)(query)) {
|
|
276
673
|
throw new Error("Invalid FormSpec semantic query payload");
|
|
277
674
|
}
|
|
278
675
|
const response = this.handleQuery(query);
|
|
@@ -281,7 +678,7 @@ var FormSpecPluginService = class {
|
|
|
281
678
|
} catch (error) {
|
|
282
679
|
socket.end(
|
|
283
680
|
`${JSON.stringify({
|
|
284
|
-
protocolVersion:
|
|
681
|
+
protocolVersion: import_protocol3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
285
682
|
kind: "error",
|
|
286
683
|
error: error instanceof Error ? error.message : String(error)
|
|
287
684
|
})}
|
|
@@ -301,54 +698,24 @@ var FormSpecPluginService = class {
|
|
|
301
698
|
await import_promises.default.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
302
699
|
}
|
|
303
700
|
}
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
return
|
|
701
|
+
logQueryDuration(query, durationMs) {
|
|
702
|
+
const logger = this.options.logger;
|
|
703
|
+
if (logger === void 0) {
|
|
704
|
+
return;
|
|
308
705
|
}
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
311
|
-
return
|
|
706
|
+
const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
|
|
707
|
+
if (durationMs < thresholdMs) {
|
|
708
|
+
return;
|
|
312
709
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
710
|
+
const event = {
|
|
711
|
+
name: "plugin.handleQuery",
|
|
712
|
+
durationMs,
|
|
713
|
+
detail: {
|
|
714
|
+
kind: query.kind,
|
|
715
|
+
...query.kind === "health" ? {} : { filePath: query.filePath }
|
|
716
|
+
}
|
|
316
717
|
};
|
|
317
|
-
|
|
318
|
-
getFileSnapshot(filePath) {
|
|
319
|
-
const environment = this.getSourceEnvironment(filePath);
|
|
320
|
-
if (environment === null) {
|
|
321
|
-
return {
|
|
322
|
-
filePath,
|
|
323
|
-
sourceHash: "",
|
|
324
|
-
generatedAt: this.getNow().toISOString(),
|
|
325
|
-
comments: [],
|
|
326
|
-
diagnostics: [
|
|
327
|
-
{
|
|
328
|
-
code: "MISSING_SOURCE_FILE",
|
|
329
|
-
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
330
|
-
range: { start: 0, end: 0 },
|
|
331
|
-
severity: "warning"
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
const sourceHash = (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text);
|
|
337
|
-
const cached = this.snapshotCache.get(filePath);
|
|
338
|
-
if (cached?.sourceHash === sourceHash) {
|
|
339
|
-
return cached.snapshot;
|
|
340
|
-
}
|
|
341
|
-
const snapshot = (0, import_analysis2.buildFormSpecAnalysisFileSnapshot)(environment.sourceFile, {
|
|
342
|
-
checker: environment.checker
|
|
343
|
-
});
|
|
344
|
-
this.snapshotCache.set(filePath, {
|
|
345
|
-
sourceHash,
|
|
346
|
-
snapshot
|
|
347
|
-
});
|
|
348
|
-
return snapshot;
|
|
349
|
-
}
|
|
350
|
-
getNow() {
|
|
351
|
-
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
718
|
+
logger.info(`[FormSpec][perf] ${formatPerformanceEvent(event)}`);
|
|
352
719
|
}
|
|
353
720
|
};
|
|
354
721
|
function createLanguageServiceProxy(languageService, semanticService) {
|
|
@@ -385,9 +752,23 @@ function createLanguageServiceProxy(languageService, semanticService) {
|
|
|
385
752
|
|
|
386
753
|
// src/index.ts
|
|
387
754
|
var services = /* @__PURE__ */ new Map();
|
|
755
|
+
var PERF_LOG_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE";
|
|
756
|
+
var PERF_LOG_THRESHOLD_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS";
|
|
388
757
|
function formatPluginError(error) {
|
|
389
758
|
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
390
759
|
}
|
|
760
|
+
function readBooleanEnvFlag(name) {
|
|
761
|
+
const rawValue = process.env[name];
|
|
762
|
+
return rawValue === "1" || rawValue === "true";
|
|
763
|
+
}
|
|
764
|
+
function readNumberEnvFlag(name) {
|
|
765
|
+
const rawValue = process.env[name];
|
|
766
|
+
if (rawValue === void 0 || rawValue.trim() === "") {
|
|
767
|
+
return void 0;
|
|
768
|
+
}
|
|
769
|
+
const parsed = Number(rawValue);
|
|
770
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
771
|
+
}
|
|
391
772
|
function getOrCreateService(info, typescriptVersion) {
|
|
392
773
|
const workspaceRoot = info.project.getCurrentDirectory();
|
|
393
774
|
const existing = services.get(workspaceRoot);
|
|
@@ -396,11 +777,14 @@ function getOrCreateService(info, typescriptVersion) {
|
|
|
396
777
|
attachProjectCloseHandler(info, workspaceRoot, existing);
|
|
397
778
|
return existing.service;
|
|
398
779
|
}
|
|
780
|
+
const performanceLogThresholdMs = readNumberEnvFlag(PERF_LOG_THRESHOLD_ENV_VAR);
|
|
399
781
|
const service = new FormSpecPluginService({
|
|
400
782
|
workspaceRoot,
|
|
401
783
|
typescriptVersion,
|
|
402
784
|
getProgram: () => info.languageService.getProgram(),
|
|
403
|
-
logger: info.project.projectService.logger
|
|
785
|
+
logger: info.project.projectService.logger,
|
|
786
|
+
enablePerformanceLogging: readBooleanEnvFlag(PERF_LOG_ENV_VAR),
|
|
787
|
+
...performanceLogThresholdMs === void 0 ? {} : { performanceLogThresholdMs }
|
|
404
788
|
});
|
|
405
789
|
const serviceEntry = {
|
|
406
790
|
service,
|
|
@@ -442,12 +826,17 @@ function init(modules) {
|
|
|
442
826
|
return {
|
|
443
827
|
create(info) {
|
|
444
828
|
const service = getOrCreateService(info, typescriptVersion);
|
|
445
|
-
return createLanguageServiceProxy(info.languageService, service);
|
|
829
|
+
return createLanguageServiceProxy(info.languageService, service.getSemanticService());
|
|
446
830
|
}
|
|
447
831
|
};
|
|
448
832
|
}
|
|
449
833
|
// Annotate the CommonJS export names for ESM import in node:
|
|
450
834
|
0 && (module.exports = {
|
|
835
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
836
|
+
FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
837
|
+
FormSpecPluginService,
|
|
838
|
+
FormSpecSemanticService,
|
|
839
|
+
createLanguageServiceProxy,
|
|
451
840
|
init
|
|
452
841
|
});
|
|
453
842
|
//# sourceMappingURL=index.cjs.map
|