@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/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 ts = require("typescript");
41
- var import_analysis2 = require("@formspec/analysis");
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 import_analysis = require("@formspec/analysis");
53
+ var import_protocol = require("@formspec/analysis/protocol");
47
54
  function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
48
- const workspaceId = (0, import_analysis.getFormSpecWorkspaceId)(workspaceRoot);
49
- const runtimeDirectory = (0, import_analysis.getFormSpecWorkspaceRuntimeDirectory)(workspaceRoot);
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, import_analysis.getFormSpecManifestPath)(workspaceRoot),
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: import_analysis.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
70
- analysisSchemaVersion: import_analysis.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
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
- snapshotCache = /* @__PURE__ */ new Map();
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
- for (const timer of this.refreshTimers.values()) {
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
- const existing = this.refreshTimers.get(filePath);
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: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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 environment = this.getSourceEnvironment(query.filePath);
202
- if (environment === null) {
624
+ const result = this.semanticService.getCompletionContext(query.filePath, query.offset);
625
+ if (result === null) {
203
626
  return {
204
- protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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
- 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)
633
+ ...result,
634
+ kind: "completion"
226
635
  };
227
636
  }
228
637
  case "hover": {
229
- const environment = this.getSourceEnvironment(query.filePath);
230
- if (environment === null) {
638
+ const result = this.semanticService.getHover(query.filePath, query.offset);
639
+ if (result === null) {
231
640
  return {
232
- protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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
- 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)
647
+ ...result,
648
+ kind: "hover"
250
649
  };
251
650
  }
252
651
  case "diagnostics": {
253
- const snapshot = this.getFileSnapshot(query.filePath);
652
+ const result = this.semanticService.getDiagnostics(query.filePath);
254
653
  return {
255
- protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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, import_analysis2.isFormSpecSemanticQuery)(query)) {
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: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
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
- getSourceEnvironment(filePath) {
305
- const program = this.options.getProgram();
306
- if (program === void 0) {
307
- return null;
701
+ logQueryDuration(query, durationMs) {
702
+ const logger = this.options.logger;
703
+ if (logger === void 0) {
704
+ return;
308
705
  }
309
- const sourceFile = program.getSourceFile(filePath);
310
- if (sourceFile === void 0) {
311
- return null;
706
+ const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
707
+ if (durationMs < thresholdMs) {
708
+ return;
312
709
  }
313
- return {
314
- sourceFile,
315
- checker: program.getTypeChecker()
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