@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 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=handle-query.test.d.ts.map
@@ -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 import_analysis2 = require("@formspec/analysis");
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 import_analysis = require("@formspec/analysis");
47
+ var import_protocol = require("@formspec/analysis/protocol");
47
48
  function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
48
- const workspaceId = (0, import_analysis.getFormSpecWorkspaceId)(workspaceRoot);
49
- const runtimeDirectory = (0, import_analysis.getFormSpecWorkspaceRuntimeDirectory)(workspaceRoot);
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, import_analysis.getFormSpecManifestPath)(workspaceRoot),
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: import_analysis.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
70
- analysisSchemaVersion: import_analysis.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
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 ?? 250);
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
- switch (query.kind) {
194
- case "health":
195
- return {
196
- protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
197
- kind: "health",
198
- manifest: this.manifest
199
- };
200
- case "completion": {
201
- const environment = this.getSourceEnvironment(query.filePath);
202
- if (environment === null) {
203
- return {
204
- protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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, import_analysis2.isFormSpecSemanticQuery)(query)) {
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: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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
- getSourceEnvironment(filePath) {
305
- const program = this.options.getProgram();
306
- if (program === void 0) {
307
- return null;
308
- }
309
- const sourceFile = program.getSourceFile(filePath);
310
- if (sourceFile === void 0) {
311
- return null;
312
- }
313
- return {
314
- sourceFile,
315
- checker: program.getTypeChecker()
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 environment = this.getSourceEnvironment(filePath);
314
+ getFileSnapshot(filePath, performance) {
315
+ const startedAt = (0, import_internal.getFormSpecPerformanceNow)();
316
+ const environment = this.getSourceEnvironment(filePath, performance);
320
317
  if (environment === null) {
321
- return {
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, import_analysis2.buildFormSpecAnalysisFileSnapshot)(environment.sourceFile, {
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,