@formspec/ts-plugin 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +8 -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/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/index.cjs +291 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +290 -105
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +18 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
# @formspec/ts-plugin
|
|
2
2
|
|
|
3
3
|
TypeScript language service plugin for FormSpec semantic comment analysis.
|
|
4
|
+
|
|
5
|
+
## Profiling
|
|
6
|
+
|
|
7
|
+
Set `FORMSPEC_PLUGIN_PROFILE=1` to enable semantic query hotspot logging.
|
|
8
|
+
|
|
9
|
+
Set `FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS=<number>` to raise or lower the
|
|
10
|
+
minimum total query duration required before a profiling summary is logged.
|
|
11
|
+
Empty or non-finite values are ignored.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-query.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/handle-query.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { FormSpecPluginService } from "../service.js";
|
|
3
|
+
export declare const FORM_SPEC_PLUGIN_TEST_SOCKET_TIMEOUT_MS = 1000;
|
|
4
|
+
export interface TestProgramContext {
|
|
5
|
+
readonly workspaceRoot: string;
|
|
6
|
+
readonly filePath: string;
|
|
7
|
+
readonly program: ts.Program;
|
|
8
|
+
}
|
|
9
|
+
export declare function createProgramContext(sourceText: string): Promise<TestProgramContext>;
|
|
10
|
+
export declare function expectErrorResponse(response: ReturnType<FormSpecPluginService["handleQuery"]>, fragment: string): void;
|
|
11
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/__tests__/helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE3D,eAAO,MAAM,uCAAuC,OAAQ,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;CAC9B;AAED,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgB1F;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,UAAU,CAAC,qBAAqB,CAAC,aAAa,CAAC,CAAC,EAC1D,QAAQ,EAAE,MAAM,GACf,IAAI,CAMN"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES: number;
|
|
2
|
+
export declare const FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare const FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
|
|
4
|
+
export declare const FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
|
|
5
|
+
export declare const FORM_SPEC_PLUGIN_PERFORMANCE_EVENT: {
|
|
6
|
+
readonly handleQuery: "plugin.handleQuery";
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yCAAyC,QAAa,CAAC;AACpE,eAAO,MAAM,uCAAuC,QAAS,CAAC;AAC9D,eAAO,MAAM,qDAAqD,KAAK,CAAC;AACxE,eAAO,MAAM,6CAA6C,MAAM,CAAC;AAEjE,eAAO,MAAM,kCAAkC;;CAErC,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -38,15 +38,16 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
38
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
39
39
|
var import_node_net = __toESM(require("net"), 1);
|
|
40
40
|
var ts = require("typescript");
|
|
41
|
-
var
|
|
41
|
+
var import_protocol2 = require("@formspec/analysis/protocol");
|
|
42
|
+
var import_internal = require("@formspec/analysis/internal");
|
|
42
43
|
|
|
43
44
|
// src/workspace.ts
|
|
44
45
|
var import_node_os = __toESM(require("os"), 1);
|
|
45
46
|
var import_node_path = __toESM(require("path"), 1);
|
|
46
|
-
var
|
|
47
|
+
var import_protocol = require("@formspec/analysis/protocol");
|
|
47
48
|
function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
|
|
48
|
-
const workspaceId = (0,
|
|
49
|
-
const runtimeDirectory = (0,
|
|
49
|
+
const workspaceId = (0, import_protocol.getFormSpecWorkspaceId)(workspaceRoot);
|
|
50
|
+
const runtimeDirectory = (0, import_protocol.getFormSpecWorkspaceRuntimeDirectory)(workspaceRoot);
|
|
50
51
|
const sanitizedUserScope = sanitizeScopeSegment(userScope);
|
|
51
52
|
const endpoint = platform === "win32" ? {
|
|
52
53
|
kind: "windows-pipe",
|
|
@@ -59,15 +60,15 @@ function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.plat
|
|
|
59
60
|
workspaceRoot,
|
|
60
61
|
workspaceId,
|
|
61
62
|
runtimeDirectory,
|
|
62
|
-
manifestPath: (0,
|
|
63
|
+
manifestPath: (0, import_protocol.getFormSpecManifestPath)(workspaceRoot),
|
|
63
64
|
endpoint
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
function createFormSpecAnalysisManifest(workspaceRoot, typescriptVersion, generation, extensionFingerprint = "builtin") {
|
|
67
68
|
const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);
|
|
68
69
|
return {
|
|
69
|
-
protocolVersion:
|
|
70
|
-
analysisSchemaVersion:
|
|
70
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
71
|
+
analysisSchemaVersion: import_protocol.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
71
72
|
workspaceRoot,
|
|
72
73
|
workspaceId: paths.workspaceId,
|
|
73
74
|
endpoint: paths.endpoint,
|
|
@@ -92,6 +93,15 @@ function sanitizeScopeSegment(value) {
|
|
|
92
93
|
return sanitized.length > 0 ? sanitized : "formspec";
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// src/constants.ts
|
|
97
|
+
var FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;
|
|
98
|
+
var FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 3e4;
|
|
99
|
+
var FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
|
|
100
|
+
var FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
|
|
101
|
+
var FORM_SPEC_PLUGIN_PERFORMANCE_EVENT = {
|
|
102
|
+
handleQuery: "plugin.handleQuery"
|
|
103
|
+
};
|
|
104
|
+
|
|
95
105
|
// src/service.ts
|
|
96
106
|
var FormSpecPluginService = class {
|
|
97
107
|
constructor(options) {
|
|
@@ -122,8 +132,25 @@ var FormSpecPluginService = class {
|
|
|
122
132
|
this.server = import_node_net.default.createServer((socket) => {
|
|
123
133
|
let buffer = "";
|
|
124
134
|
socket.setEncoding("utf8");
|
|
135
|
+
socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
|
|
136
|
+
this.options.logger?.info(
|
|
137
|
+
`[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
|
|
138
|
+
);
|
|
139
|
+
socket.destroy();
|
|
140
|
+
});
|
|
125
141
|
socket.on("data", (chunk) => {
|
|
126
142
|
buffer += String(chunk);
|
|
143
|
+
if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
|
|
144
|
+
socket.end(
|
|
145
|
+
`${JSON.stringify({
|
|
146
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
147
|
+
kind: "error",
|
|
148
|
+
error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
|
|
149
|
+
})}
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
127
154
|
const newlineIndex = buffer.indexOf("\n");
|
|
128
155
|
if (newlineIndex < 0) {
|
|
129
156
|
return;
|
|
@@ -179,100 +206,36 @@ var FormSpecPluginService = class {
|
|
|
179
206
|
}
|
|
180
207
|
const timer = setTimeout(() => {
|
|
181
208
|
try {
|
|
182
|
-
this.getFileSnapshot(filePath);
|
|
209
|
+
this.getFileSnapshot(filePath, void 0);
|
|
183
210
|
} catch (error) {
|
|
184
211
|
this.options.logger?.info(
|
|
185
212
|
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
186
213
|
);
|
|
187
214
|
}
|
|
188
215
|
this.refreshTimers.delete(filePath);
|
|
189
|
-
}, this.options.snapshotDebounceMs ??
|
|
216
|
+
}, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);
|
|
190
217
|
this.refreshTimers.set(filePath, timer);
|
|
191
218
|
}
|
|
192
219
|
handleQuery(query) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
kind: "error",
|
|
206
|
-
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
207
|
-
};
|
|
208
|
-
}
|
|
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
|
-
return {
|
|
222
|
-
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
223
|
-
kind: "completion",
|
|
224
|
-
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
225
|
-
context: (0, import_analysis2.serializeCompletionContext)(context)
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
case "hover": {
|
|
229
|
-
const environment = this.getSourceEnvironment(query.filePath);
|
|
230
|
-
if (environment === null) {
|
|
231
|
-
return {
|
|
232
|
-
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
233
|
-
kind: "error",
|
|
234
|
-
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
235
|
-
};
|
|
236
|
-
}
|
|
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
|
-
return {
|
|
246
|
-
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
247
|
-
kind: "hover",
|
|
248
|
-
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
249
|
-
hover: (0, import_analysis2.serializeHoverInfo)(hover)
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
case "diagnostics": {
|
|
253
|
-
const snapshot = this.getFileSnapshot(query.filePath);
|
|
254
|
-
return {
|
|
255
|
-
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
256
|
-
kind: "diagnostics",
|
|
257
|
-
sourceHash: snapshot.sourceHash,
|
|
258
|
-
diagnostics: snapshot.diagnostics
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
case "file-snapshot":
|
|
262
|
-
return {
|
|
263
|
-
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
264
|
-
kind: "file-snapshot",
|
|
265
|
-
snapshot: this.getFileSnapshot(query.filePath)
|
|
266
|
-
};
|
|
267
|
-
default: {
|
|
268
|
-
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
269
|
-
}
|
|
220
|
+
const performance = this.options.enablePerformanceLogging === true ? (0, import_internal.createFormSpecPerformanceRecorder)() : void 0;
|
|
221
|
+
const response = (0, import_internal.optionalMeasure)(
|
|
222
|
+
performance,
|
|
223
|
+
FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery,
|
|
224
|
+
{
|
|
225
|
+
kind: query.kind,
|
|
226
|
+
...query.kind === "health" ? {} : { filePath: query.filePath }
|
|
227
|
+
},
|
|
228
|
+
() => this.executeQuery(query, performance)
|
|
229
|
+
);
|
|
230
|
+
if (performance !== void 0) {
|
|
231
|
+
this.logPerformanceEvents(performance.events);
|
|
270
232
|
}
|
|
233
|
+
return response;
|
|
271
234
|
}
|
|
272
235
|
respondToSocket(socket, payload) {
|
|
273
236
|
try {
|
|
274
237
|
const query = JSON.parse(payload);
|
|
275
|
-
if (!(0,
|
|
238
|
+
if (!(0, import_protocol2.isFormSpecSemanticQuery)(query)) {
|
|
276
239
|
throw new Error("Invalid FormSpec semantic query payload");
|
|
277
240
|
}
|
|
278
241
|
const response = this.handleQuery(query);
|
|
@@ -281,7 +244,7 @@ var FormSpecPluginService = class {
|
|
|
281
244
|
} catch (error) {
|
|
282
245
|
socket.end(
|
|
283
246
|
`${JSON.stringify({
|
|
284
|
-
protocolVersion:
|
|
247
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
285
248
|
kind: "error",
|
|
286
249
|
error: error instanceof Error ? error.message : String(error)
|
|
287
250
|
})}
|
|
@@ -301,24 +264,58 @@ var FormSpecPluginService = class {
|
|
|
301
264
|
await import_promises.default.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
302
265
|
}
|
|
303
266
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
267
|
+
withCommentQueryContext(filePath, offset, handler, performance) {
|
|
268
|
+
return (0, import_internal.optionalMeasure)(
|
|
269
|
+
performance,
|
|
270
|
+
"plugin.resolveCommentQueryContext",
|
|
271
|
+
{
|
|
272
|
+
filePath,
|
|
273
|
+
offset
|
|
274
|
+
},
|
|
275
|
+
() => {
|
|
276
|
+
const environment = this.getSourceEnvironment(filePath, performance);
|
|
277
|
+
if (environment === null) {
|
|
278
|
+
return {
|
|
279
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
280
|
+
kind: "error",
|
|
281
|
+
error: `Unable to resolve TypeScript source file for ${filePath}`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const declaration = (0, import_internal.optionalMeasure)(
|
|
285
|
+
performance,
|
|
286
|
+
"plugin.findDeclarationForCommentOffset",
|
|
287
|
+
{
|
|
288
|
+
filePath,
|
|
289
|
+
offset
|
|
290
|
+
},
|
|
291
|
+
() => (0, import_internal.findDeclarationForCommentOffset)(environment.sourceFile, offset)
|
|
292
|
+
);
|
|
293
|
+
const placement = declaration === null ? null : (0, import_internal.optionalMeasure)(
|
|
294
|
+
performance,
|
|
295
|
+
"plugin.resolveDeclarationPlacement",
|
|
296
|
+
void 0,
|
|
297
|
+
() => (0, import_internal.resolveDeclarationPlacement)(declaration)
|
|
298
|
+
);
|
|
299
|
+
const subjectType = declaration === null ? void 0 : (0, import_internal.optionalMeasure)(
|
|
300
|
+
performance,
|
|
301
|
+
"plugin.getSubjectType",
|
|
302
|
+
void 0,
|
|
303
|
+
() => (0, import_internal.getSubjectType)(declaration, environment.checker)
|
|
304
|
+
);
|
|
305
|
+
return handler({
|
|
306
|
+
...environment,
|
|
307
|
+
declaration,
|
|
308
|
+
placement,
|
|
309
|
+
subjectType
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
);
|
|
317
313
|
}
|
|
318
|
-
getFileSnapshot(filePath) {
|
|
319
|
-
const
|
|
314
|
+
getFileSnapshot(filePath, performance) {
|
|
315
|
+
const startedAt = (0, import_internal.getFormSpecPerformanceNow)();
|
|
316
|
+
const environment = this.getSourceEnvironment(filePath, performance);
|
|
320
317
|
if (environment === null) {
|
|
321
|
-
|
|
318
|
+
const missingSourceSnapshot = {
|
|
322
319
|
filePath,
|
|
323
320
|
sourceHash: "",
|
|
324
321
|
generatedAt: this.getNow().toISOString(),
|
|
@@ -332,25 +329,192 @@ var FormSpecPluginService = class {
|
|
|
332
329
|
}
|
|
333
330
|
]
|
|
334
331
|
};
|
|
332
|
+
performance?.record({
|
|
333
|
+
name: "plugin.getFileSnapshot",
|
|
334
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
335
|
+
detail: {
|
|
336
|
+
filePath,
|
|
337
|
+
cache: "missing-source"
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return missingSourceSnapshot;
|
|
335
341
|
}
|
|
336
|
-
const sourceHash = (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text);
|
|
337
342
|
const cached = this.snapshotCache.get(filePath);
|
|
338
|
-
if (cached?.sourceHash === sourceHash) {
|
|
343
|
+
if (cached?.sourceHash === environment.sourceHash) {
|
|
344
|
+
performance?.record({
|
|
345
|
+
name: "plugin.getFileSnapshot",
|
|
346
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
347
|
+
detail: {
|
|
348
|
+
filePath,
|
|
349
|
+
cache: "hit"
|
|
350
|
+
}
|
|
351
|
+
});
|
|
339
352
|
return cached.snapshot;
|
|
340
353
|
}
|
|
341
|
-
const snapshot = (0,
|
|
342
|
-
checker: environment.checker
|
|
354
|
+
const snapshot = (0, import_internal.buildFormSpecAnalysisFileSnapshot)(environment.sourceFile, {
|
|
355
|
+
checker: environment.checker,
|
|
356
|
+
now: () => this.getNow(),
|
|
357
|
+
...performance === void 0 ? {} : { performance }
|
|
343
358
|
});
|
|
344
359
|
this.snapshotCache.set(filePath, {
|
|
345
|
-
sourceHash,
|
|
360
|
+
sourceHash: environment.sourceHash,
|
|
346
361
|
snapshot
|
|
347
362
|
});
|
|
363
|
+
performance?.record({
|
|
364
|
+
name: "plugin.getFileSnapshot",
|
|
365
|
+
durationMs: (0, import_internal.getFormSpecPerformanceNow)() - startedAt,
|
|
366
|
+
detail: {
|
|
367
|
+
filePath,
|
|
368
|
+
cache: "miss"
|
|
369
|
+
}
|
|
370
|
+
});
|
|
348
371
|
return snapshot;
|
|
349
372
|
}
|
|
350
373
|
getNow() {
|
|
351
374
|
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
352
375
|
}
|
|
376
|
+
executeQuery(query, performance) {
|
|
377
|
+
switch (query.kind) {
|
|
378
|
+
case "health":
|
|
379
|
+
return {
|
|
380
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
381
|
+
kind: "health",
|
|
382
|
+
manifest: this.manifest
|
|
383
|
+
};
|
|
384
|
+
case "completion":
|
|
385
|
+
return this.withCommentQueryContext(
|
|
386
|
+
query.filePath,
|
|
387
|
+
query.offset,
|
|
388
|
+
(context) => ({
|
|
389
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
390
|
+
kind: "completion",
|
|
391
|
+
sourceHash: context.sourceHash,
|
|
392
|
+
context: (0, import_internal.serializeCompletionContext)(
|
|
393
|
+
(0, import_internal.getSemanticCommentCompletionContextAtOffset)(context.sourceFile.text, query.offset, {
|
|
394
|
+
checker: context.checker,
|
|
395
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
396
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
397
|
+
})
|
|
398
|
+
)
|
|
399
|
+
}),
|
|
400
|
+
performance
|
|
401
|
+
);
|
|
402
|
+
case "hover":
|
|
403
|
+
return this.withCommentQueryContext(
|
|
404
|
+
query.filePath,
|
|
405
|
+
query.offset,
|
|
406
|
+
(context) => ({
|
|
407
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
408
|
+
kind: "hover",
|
|
409
|
+
sourceHash: context.sourceHash,
|
|
410
|
+
hover: (0, import_internal.serializeHoverInfo)(
|
|
411
|
+
(0, import_internal.getCommentHoverInfoAtOffset)(context.sourceFile.text, query.offset, {
|
|
412
|
+
checker: context.checker,
|
|
413
|
+
...context.placement === null ? {} : { placement: context.placement },
|
|
414
|
+
...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
|
|
415
|
+
})
|
|
416
|
+
)
|
|
417
|
+
}),
|
|
418
|
+
performance
|
|
419
|
+
);
|
|
420
|
+
case "diagnostics": {
|
|
421
|
+
const snapshot = this.getFileSnapshot(query.filePath, performance);
|
|
422
|
+
return {
|
|
423
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
424
|
+
kind: "diagnostics",
|
|
425
|
+
sourceHash: snapshot.sourceHash,
|
|
426
|
+
diagnostics: snapshot.diagnostics
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
case "file-snapshot":
|
|
430
|
+
return {
|
|
431
|
+
protocolVersion: import_protocol2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
432
|
+
kind: "file-snapshot",
|
|
433
|
+
snapshot: this.getFileSnapshot(query.filePath, performance)
|
|
434
|
+
};
|
|
435
|
+
default: {
|
|
436
|
+
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
getSourceEnvironment(filePath, performance) {
|
|
441
|
+
return (0, import_internal.optionalMeasure)(
|
|
442
|
+
performance,
|
|
443
|
+
"plugin.getSourceEnvironment",
|
|
444
|
+
{
|
|
445
|
+
filePath
|
|
446
|
+
},
|
|
447
|
+
() => {
|
|
448
|
+
const program = (0, import_internal.optionalMeasure)(
|
|
449
|
+
performance,
|
|
450
|
+
"plugin.sourceEnvironment.getProgram",
|
|
451
|
+
void 0,
|
|
452
|
+
() => this.options.getProgram()
|
|
453
|
+
);
|
|
454
|
+
if (program === void 0) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const sourceFile = (0, import_internal.optionalMeasure)(
|
|
458
|
+
performance,
|
|
459
|
+
"plugin.sourceEnvironment.getSourceFile",
|
|
460
|
+
void 0,
|
|
461
|
+
() => program.getSourceFile(filePath)
|
|
462
|
+
);
|
|
463
|
+
if (sourceFile === void 0) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
const checker = (0, import_internal.optionalMeasure)(
|
|
467
|
+
performance,
|
|
468
|
+
"plugin.sourceEnvironment.getTypeChecker",
|
|
469
|
+
void 0,
|
|
470
|
+
() => program.getTypeChecker()
|
|
471
|
+
);
|
|
472
|
+
const sourceHash = (0, import_internal.optionalMeasure)(
|
|
473
|
+
performance,
|
|
474
|
+
"plugin.sourceEnvironment.computeTextHash",
|
|
475
|
+
void 0,
|
|
476
|
+
() => (0, import_protocol2.computeFormSpecTextHash)(sourceFile.text)
|
|
477
|
+
);
|
|
478
|
+
return {
|
|
479
|
+
sourceFile,
|
|
480
|
+
checker,
|
|
481
|
+
sourceHash
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
logPerformanceEvents(events) {
|
|
487
|
+
const logger = this.options.logger;
|
|
488
|
+
if (logger === void 0 || events.length === 0) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
let rootEvent;
|
|
492
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
493
|
+
const candidate = events[index];
|
|
494
|
+
if (candidate?.name === FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery) {
|
|
495
|
+
rootEvent = candidate;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (rootEvent === void 0) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
|
|
503
|
+
if (rootEvent.durationMs < thresholdMs) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const sortedHotspots = [...events].filter((event) => event.name !== FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
|
|
507
|
+
const lines = [
|
|
508
|
+
`[FormSpec][perf] ${rootEvent.name} ${formatPerformanceEvent(rootEvent)}`,
|
|
509
|
+
...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`)
|
|
510
|
+
];
|
|
511
|
+
logger.info(lines.join("\n"));
|
|
512
|
+
}
|
|
353
513
|
};
|
|
514
|
+
function formatPerformanceEvent(event) {
|
|
515
|
+
const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
|
|
516
|
+
return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
|
|
517
|
+
}
|
|
354
518
|
function createLanguageServiceProxy(languageService, semanticService) {
|
|
355
519
|
const wrapWithSnapshotRefresh = (fn) => {
|
|
356
520
|
return (fileName, ...args) => {
|
|
@@ -385,9 +549,23 @@ function createLanguageServiceProxy(languageService, semanticService) {
|
|
|
385
549
|
|
|
386
550
|
// src/index.ts
|
|
387
551
|
var services = /* @__PURE__ */ new Map();
|
|
552
|
+
var PERF_LOG_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE";
|
|
553
|
+
var PERF_LOG_THRESHOLD_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS";
|
|
388
554
|
function formatPluginError(error) {
|
|
389
555
|
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
390
556
|
}
|
|
557
|
+
function readBooleanEnvFlag(name) {
|
|
558
|
+
const rawValue = process.env[name];
|
|
559
|
+
return rawValue === "1" || rawValue === "true";
|
|
560
|
+
}
|
|
561
|
+
function readNumberEnvFlag(name) {
|
|
562
|
+
const rawValue = process.env[name];
|
|
563
|
+
if (rawValue === void 0 || rawValue.trim() === "") {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
const parsed = Number(rawValue);
|
|
567
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
568
|
+
}
|
|
391
569
|
function getOrCreateService(info, typescriptVersion) {
|
|
392
570
|
const workspaceRoot = info.project.getCurrentDirectory();
|
|
393
571
|
const existing = services.get(workspaceRoot);
|
|
@@ -396,11 +574,14 @@ function getOrCreateService(info, typescriptVersion) {
|
|
|
396
574
|
attachProjectCloseHandler(info, workspaceRoot, existing);
|
|
397
575
|
return existing.service;
|
|
398
576
|
}
|
|
577
|
+
const performanceLogThresholdMs = readNumberEnvFlag(PERF_LOG_THRESHOLD_ENV_VAR);
|
|
399
578
|
const service = new FormSpecPluginService({
|
|
400
579
|
workspaceRoot,
|
|
401
580
|
typescriptVersion,
|
|
402
581
|
getProgram: () => info.languageService.getProgram(),
|
|
403
|
-
logger: info.project.projectService.logger
|
|
582
|
+
logger: info.project.projectService.logger,
|
|
583
|
+
enablePerformanceLogging: readBooleanEnvFlag(PERF_LOG_ENV_VAR),
|
|
584
|
+
...performanceLogThresholdMs === void 0 ? {} : { performanceLogThresholdMs }
|
|
404
585
|
});
|
|
405
586
|
const serviceEntry = {
|
|
406
587
|
service,
|