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