@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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/service.ts","../src/workspace.ts"],"sourcesContent":["import type * as tsServer from \"typescript/lib/tsserverlibrary.js\";\nimport { createLanguageServiceProxy, FormSpecPluginService } from \"./service.js\";\n\ninterface ServiceEntry {\n readonly service: FormSpecPluginService;\n referenceCount: number;\n}\n\nconst services = new Map<string, ServiceEntry>();\n\nfunction formatPluginError(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\nfunction getOrCreateService(\n info: tsServer.server.PluginCreateInfo,\n typescriptVersion: string\n): FormSpecPluginService {\n const workspaceRoot = info.project.getCurrentDirectory();\n const existing = services.get(workspaceRoot);\n if (existing !== undefined) {\n existing.referenceCount += 1;\n attachProjectCloseHandler(info, workspaceRoot, existing);\n return existing.service;\n }\n\n const service = new FormSpecPluginService({\n workspaceRoot,\n typescriptVersion,\n getProgram: () => info.languageService.getProgram(),\n logger: info.project.projectService.logger,\n });\n\n const serviceEntry: ServiceEntry = {\n service,\n referenceCount: 1,\n };\n attachProjectCloseHandler(info, workspaceRoot, serviceEntry);\n\n service.start().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n services.delete(workspaceRoot);\n });\n services.set(workspaceRoot, serviceEntry);\n return service;\n}\n\nfunction attachProjectCloseHandler(\n info: tsServer.server.PluginCreateInfo,\n workspaceRoot: string,\n serviceEntry: ServiceEntry\n): void {\n const originalClose = info.project.close.bind(info.project);\n let closed = false;\n\n info.project.close = () => {\n if (closed) {\n originalClose();\n return;\n }\n\n closed = true;\n serviceEntry.referenceCount -= 1;\n if (serviceEntry.referenceCount <= 0) {\n services.delete(workspaceRoot);\n void serviceEntry.service.stop().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n });\n }\n originalClose();\n };\n}\n\n/**\n * Initializes the FormSpec TypeScript language service plugin.\n *\n * @public\n */\nexport function init(modules: {\n readonly typescript: typeof tsServer;\n}): tsServer.server.PluginModule {\n const typescriptVersion = modules.typescript.version;\n return {\n create(info) {\n const service = getOrCreateService(info, typescriptVersion);\n return createLanguageServiceProxy(info.languageService, service);\n },\n };\n}\n","import fs from \"node:fs/promises\";\nimport net from \"node:net\";\nimport * as ts from \"typescript\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n buildFormSpecAnalysisFileSnapshot,\n computeFormSpecTextHash,\n findDeclarationForCommentOffset,\n getSubjectType,\n getCommentHoverInfoAtOffset,\n getSemanticCommentCompletionContextAtOffset,\n isFormSpecSemanticQuery,\n resolveDeclarationPlacement,\n serializeCompletionContext,\n serializeHoverInfo,\n type BuildFormSpecAnalysisFileSnapshotOptions,\n type FormSpecAnalysisFileSnapshot,\n type FormSpecAnalysisManifest,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis\";\nimport {\n createFormSpecAnalysisManifest,\n getFormSpecWorkspaceRuntimePaths,\n type FormSpecWorkspaceRuntimePaths,\n} from \"./workspace.js\";\n\ninterface LoggerLike {\n info(message: string): void;\n}\n\nexport interface FormSpecPluginServiceOptions {\n readonly workspaceRoot: string;\n readonly typescriptVersion: string;\n readonly getProgram: () => ts.Program | undefined;\n readonly logger?: LoggerLike;\n readonly snapshotDebounceMs?: number;\n readonly now?: () => Date;\n}\n\ninterface CachedFileSnapshot {\n readonly sourceHash: string;\n readonly snapshot: FormSpecAnalysisFileSnapshot;\n}\n\nexport class FormSpecPluginService {\n private readonly manifest: FormSpecAnalysisManifest;\n private readonly runtimePaths: FormSpecWorkspaceRuntimePaths;\n private readonly snapshotCache = new Map<string, CachedFileSnapshot>();\n private readonly refreshTimers = new Map<string, NodeJS.Timeout>();\n private server: net.Server | null = null;\n\n public constructor(private readonly options: FormSpecPluginServiceOptions) {\n this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);\n this.manifest = createFormSpecAnalysisManifest(\n options.workspaceRoot,\n options.typescriptVersion,\n Date.now()\n );\n }\n\n public getManifest(): FormSpecAnalysisManifest {\n return this.manifest;\n }\n\n public async start(): Promise<void> {\n if (this.server !== null) {\n return;\n }\n\n await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n\n this.server = net.createServer((socket) => {\n let buffer = \"\";\n socket.setEncoding(\"utf8\");\n socket.on(\"data\", (chunk) => {\n buffer += String(chunk);\n const newlineIndex = buffer.indexOf(\"\\n\");\n if (newlineIndex < 0) {\n return;\n }\n\n const payload = buffer.slice(0, newlineIndex);\n const remaining = buffer.slice(newlineIndex + 1);\n if (remaining.trim().length > 0) {\n this.options.logger?.info(\n `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`\n );\n }\n buffer = remaining;\n // The FormSpec IPC transport is intentionally one-request-per-connection.\n this.respondToSocket(socket, payload);\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const handleError = (error: Error) => {\n reject(error);\n };\n this.server?.once(\"error\", handleError);\n this.server?.listen(this.runtimePaths.endpoint.address, () => {\n this.server?.off(\"error\", handleError);\n resolve();\n });\n });\n\n await this.writeManifest();\n }\n\n public async stop(): Promise<void> {\n for (const timer of this.refreshTimers.values()) {\n clearTimeout(timer);\n }\n this.refreshTimers.clear();\n this.snapshotCache.clear();\n\n const server = this.server;\n this.server = null;\n if (server?.listening === true) {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error === undefined) {\n resolve();\n return;\n }\n reject(error);\n });\n });\n }\n\n await this.cleanupRuntimeArtifacts();\n }\n\n public scheduleSnapshotRefresh(filePath: string): void {\n const existing = this.refreshTimers.get(filePath);\n if (existing !== undefined) {\n clearTimeout(existing);\n }\n\n const timer = setTimeout(() => {\n try {\n this.getFileSnapshot(filePath);\n } catch (error: unknown) {\n this.options.logger?.info(\n `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`\n );\n }\n this.refreshTimers.delete(filePath);\n }, this.options.snapshotDebounceMs ?? 250);\n\n this.refreshTimers.set(filePath, timer);\n }\n\n public handleQuery(query: FormSpecSemanticQuery): FormSpecSemanticResponse {\n switch (query.kind) {\n case \"health\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"health\",\n manifest: this.manifest,\n };\n case \"completion\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const context = getSemanticCommentCompletionContextAtOffset(\n environment.sourceFile.text,\n query.offset,\n {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n }\n );\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"completion\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n context: serializeCompletionContext(context),\n };\n }\n case \"hover\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const hover = getCommentHoverInfoAtOffset(environment.sourceFile.text, query.offset, {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n });\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"hover\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n hover: serializeHoverInfo(hover),\n };\n }\n case \"diagnostics\": {\n const snapshot = this.getFileSnapshot(query.filePath);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"diagnostics\",\n sourceHash: snapshot.sourceHash,\n diagnostics: snapshot.diagnostics,\n };\n }\n case \"file-snapshot\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"file-snapshot\",\n snapshot: this.getFileSnapshot(query.filePath),\n };\n default: {\n throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);\n }\n }\n }\n\n private respondToSocket(socket: net.Socket, payload: string): void {\n try {\n const query = JSON.parse(payload) as unknown;\n if (!isFormSpecSemanticQuery(query)) {\n throw new Error(\"Invalid FormSpec semantic query payload\");\n }\n const response = this.handleQuery(query);\n socket.end(`${JSON.stringify(response)}\\n`);\n } catch (error) {\n socket.end(\n `${JSON.stringify({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } satisfies FormSpecSemanticResponse)}\\n`\n );\n }\n }\n\n private async writeManifest(): Promise<void> {\n const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;\n await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}\\n`, \"utf8\");\n await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);\n }\n\n private async cleanupRuntimeArtifacts(): Promise<void> {\n await fs.rm(this.runtimePaths.manifestPath, { force: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n }\n\n private getSourceEnvironment(filePath: string): {\n readonly sourceFile: ts.SourceFile;\n readonly checker: ts.TypeChecker;\n } | null {\n const program = this.options.getProgram();\n if (program === undefined) {\n return null;\n }\n\n const sourceFile = program.getSourceFile(filePath);\n if (sourceFile === undefined) {\n return null;\n }\n\n return {\n sourceFile,\n checker: program.getTypeChecker(),\n };\n }\n\n private getFileSnapshot(filePath: string): FormSpecAnalysisFileSnapshot {\n const environment = this.getSourceEnvironment(filePath);\n if (environment === null) {\n return {\n filePath,\n sourceHash: \"\",\n generatedAt: this.getNow().toISOString(),\n comments: [],\n diagnostics: [\n {\n code: \"MISSING_SOURCE_FILE\",\n message: `Unable to resolve TypeScript source file for ${filePath}`,\n range: { start: 0, end: 0 },\n severity: \"warning\",\n },\n ],\n };\n }\n\n const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);\n const cached = this.snapshotCache.get(filePath);\n if (cached?.sourceHash === sourceHash) {\n return cached.snapshot;\n }\n\n const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {\n checker: environment.checker,\n } satisfies BuildFormSpecAnalysisFileSnapshotOptions);\n this.snapshotCache.set(filePath, {\n sourceHash,\n snapshot,\n });\n return snapshot;\n }\n\n private getNow(): Date {\n return this.options.now?.() ?? new Date();\n }\n}\n\nexport function createLanguageServiceProxy(\n languageService: ts.LanguageService,\n semanticService: FormSpecPluginService\n): ts.LanguageService {\n const wrapWithSnapshotRefresh = <Args extends readonly unknown[], Result>(\n fn: (fileName: string, ...args: Args) => Result\n ) => {\n return (fileName: string, ...args: Args): Result => {\n semanticService.scheduleSnapshotRefresh(fileName);\n return fn(fileName, ...args);\n };\n };\n\n // The plugin keeps semantic snapshots fresh for the lightweight LSP. The\n // underlying tsserver results still come from the original language service.\n const getSemanticDiagnostics = wrapWithSnapshotRefresh((fileName) =>\n languageService.getSemanticDiagnostics(fileName)\n );\n\n const getCompletionsAtPosition = wrapWithSnapshotRefresh(\n (fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined) =>\n languageService.getCompletionsAtPosition(fileName, position, options)\n );\n\n const getQuickInfoAtPosition = wrapWithSnapshotRefresh((fileName, position: number) =>\n languageService.getQuickInfoAtPosition(fileName, position)\n );\n\n return new Proxy(languageService, {\n get(target, property, receiver) {\n switch (property) {\n case \"getSemanticDiagnostics\":\n return getSemanticDiagnostics;\n case \"getCompletionsAtPosition\":\n return getCompletionsAtPosition;\n case \"getQuickInfoAtPosition\":\n return getQuickInfoAtPosition;\n default:\n return Reflect.get(target, property, receiver) as unknown;\n }\n },\n });\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n getFormSpecManifestPath,\n getFormSpecWorkspaceId,\n getFormSpecWorkspaceRuntimeDirectory,\n type FormSpecAnalysisManifest,\n type FormSpecIpcEndpoint,\n} from \"@formspec/analysis\";\n\nexport interface FormSpecWorkspaceRuntimePaths {\n readonly workspaceRoot: string;\n readonly workspaceId: string;\n readonly runtimeDirectory: string;\n readonly manifestPath: string;\n readonly endpoint: FormSpecIpcEndpoint;\n}\n\nexport function getFormSpecWorkspaceRuntimePaths(\n workspaceRoot: string,\n platform = process.platform,\n userScope = getFormSpecUserScope()\n): FormSpecWorkspaceRuntimePaths {\n const workspaceId = getFormSpecWorkspaceId(workspaceRoot);\n const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);\n const sanitizedUserScope = sanitizeScopeSegment(userScope);\n const endpoint: FormSpecIpcEndpoint =\n platform === \"win32\"\n ? {\n kind: \"windows-pipe\",\n address: `\\\\\\\\.\\\\pipe\\\\formspec-${sanitizedUserScope}-${workspaceId}`,\n }\n : {\n kind: \"unix-socket\",\n address: path.join(os.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`),\n };\n\n return {\n workspaceRoot,\n workspaceId,\n runtimeDirectory,\n manifestPath: getFormSpecManifestPath(workspaceRoot),\n endpoint,\n };\n}\n\nexport function createFormSpecAnalysisManifest(\n workspaceRoot: string,\n typescriptVersion: string,\n generation: number,\n extensionFingerprint = \"builtin\"\n): FormSpecAnalysisManifest {\n const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n analysisSchemaVersion: FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n workspaceRoot,\n workspaceId: paths.workspaceId,\n endpoint: paths.endpoint,\n typescriptVersion,\n extensionFingerprint,\n generation,\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction getFormSpecUserScope(): string {\n try {\n return sanitizeScopeSegment(os.userInfo().username);\n } catch {\n return sanitizeScopeSegment(\n process.env[\"USER\"] ?? process.env[\"USERNAME\"] ?? process.env[\"LOGNAME\"] ?? \"formspec\"\n );\n }\n}\n\nfunction sanitizeScopeSegment(value: string): string {\n const trimmed = value.trim().toLowerCase();\n const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, \"-\").replace(/-+/gu, \"-\");\n return sanitized.length > 0 ? sanitized : \"formspec\";\n}\n"],"mappings":"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","names":["import_analysis","path","os","fs","net"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/service.ts","../src/workspace.ts","../src/constants.ts"],"sourcesContent":["import type * as tsServer from \"typescript/lib/tsserverlibrary.js\";\nimport { createLanguageServiceProxy, FormSpecPluginService } from \"./service.js\";\n\ninterface ServiceEntry {\n readonly service: FormSpecPluginService;\n referenceCount: number;\n}\n\nconst services = new Map<string, ServiceEntry>();\nconst PERF_LOG_ENV_VAR = \"FORMSPEC_PLUGIN_PROFILE\";\nconst PERF_LOG_THRESHOLD_ENV_VAR = \"FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS\";\n\nfunction formatPluginError(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\nfunction readBooleanEnvFlag(name: string): boolean {\n const rawValue = process.env[name];\n return rawValue === \"1\" || rawValue === \"true\";\n}\n\nfunction readNumberEnvFlag(name: string): number | undefined {\n const rawValue = process.env[name];\n if (rawValue === undefined || rawValue.trim() === \"\") {\n return undefined;\n }\n\n const parsed = Number(rawValue);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction getOrCreateService(\n info: tsServer.server.PluginCreateInfo,\n typescriptVersion: string\n): FormSpecPluginService {\n const workspaceRoot = info.project.getCurrentDirectory();\n const existing = services.get(workspaceRoot);\n if (existing !== undefined) {\n existing.referenceCount += 1;\n attachProjectCloseHandler(info, workspaceRoot, existing);\n return existing.service;\n }\n\n const performanceLogThresholdMs = readNumberEnvFlag(PERF_LOG_THRESHOLD_ENV_VAR);\n const service = new FormSpecPluginService({\n workspaceRoot,\n typescriptVersion,\n getProgram: () => info.languageService.getProgram(),\n logger: info.project.projectService.logger,\n enablePerformanceLogging: readBooleanEnvFlag(PERF_LOG_ENV_VAR),\n ...(performanceLogThresholdMs === undefined ? {} : { performanceLogThresholdMs }),\n });\n\n const serviceEntry: ServiceEntry = {\n service,\n referenceCount: 1,\n };\n attachProjectCloseHandler(info, workspaceRoot, serviceEntry);\n\n service.start().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n services.delete(workspaceRoot);\n });\n services.set(workspaceRoot, serviceEntry);\n return service;\n}\n\nfunction attachProjectCloseHandler(\n info: tsServer.server.PluginCreateInfo,\n workspaceRoot: string,\n serviceEntry: ServiceEntry\n): void {\n const originalClose = info.project.close.bind(info.project);\n let closed = false;\n\n info.project.close = () => {\n if (closed) {\n originalClose();\n return;\n }\n\n closed = true;\n serviceEntry.referenceCount -= 1;\n if (serviceEntry.referenceCount <= 0) {\n services.delete(workspaceRoot);\n void serviceEntry.service.stop().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n });\n }\n originalClose();\n };\n}\n\n/**\n * Initializes the FormSpec TypeScript language service plugin.\n *\n * @public\n */\nexport function init(modules: {\n readonly typescript: typeof tsServer;\n}): tsServer.server.PluginModule {\n const typescriptVersion = modules.typescript.version;\n return {\n create(info) {\n const service = getOrCreateService(info, typescriptVersion);\n return createLanguageServiceProxy(info.languageService, service);\n },\n };\n}\n","import fs from \"node:fs/promises\";\nimport net from \"node:net\";\nimport * as ts from \"typescript\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n computeFormSpecTextHash,\n isFormSpecSemanticQuery,\n type FormSpecAnalysisManifest,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis/protocol\";\nimport {\n buildFormSpecAnalysisFileSnapshot,\n createFormSpecPerformanceRecorder,\n findDeclarationForCommentOffset,\n getCommentHoverInfoAtOffset,\n getSemanticCommentCompletionContextAtOffset,\n getFormSpecPerformanceNow,\n getSubjectType,\n optionalMeasure,\n resolveDeclarationPlacement,\n serializeCompletionContext,\n serializeHoverInfo,\n type BuildFormSpecAnalysisFileSnapshotOptions,\n type FormSpecAnalysisFileSnapshot,\n type FormSpecPerformanceEvent,\n type FormSpecPerformanceRecorder,\n} from \"@formspec/analysis/internal\";\nimport {\n createFormSpecAnalysisManifest,\n getFormSpecWorkspaceRuntimePaths,\n type FormSpecWorkspaceRuntimePaths,\n} from \"./workspace.js\";\nimport {\n FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS,\n FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS,\n FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES,\n FORM_SPEC_PLUGIN_PERFORMANCE_EVENT,\n FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS,\n} from \"./constants.js\";\n\ninterface LoggerLike {\n info(message: string): void;\n}\n\nexport interface FormSpecPluginServiceOptions {\n readonly workspaceRoot: string;\n readonly typescriptVersion: string;\n readonly getProgram: () => ts.Program | undefined;\n readonly logger?: LoggerLike;\n /**\n * Enables structured hotspot logging for semantic queries.\n *\n * The tsserver plugin sets this from `FORMSPEC_PLUGIN_PROFILE=1`.\n */\n readonly enablePerformanceLogging?: boolean;\n /**\n * Minimum total query duration in milliseconds before profiling is logged.\n *\n * Defaults to `50` when unset. The tsserver plugin sets this from\n * `FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS`.\n */\n readonly performanceLogThresholdMs?: number;\n readonly snapshotDebounceMs?: number;\n readonly now?: () => Date;\n}\n\ninterface CachedFileSnapshot {\n readonly sourceHash: string;\n readonly snapshot: FormSpecAnalysisFileSnapshot;\n}\n\ninterface SourceEnvironment {\n readonly sourceFile: ts.SourceFile;\n readonly checker: ts.TypeChecker;\n readonly sourceHash: string;\n}\n\ninterface CommentQueryContext extends SourceEnvironment {\n readonly declaration: ts.Node | null;\n readonly placement: ReturnType<typeof resolveDeclarationPlacement>;\n readonly subjectType: ts.Type | undefined;\n}\n\nexport class FormSpecPluginService {\n private readonly manifest: FormSpecAnalysisManifest;\n private readonly runtimePaths: FormSpecWorkspaceRuntimePaths;\n private readonly snapshotCache = new Map<string, CachedFileSnapshot>();\n private readonly refreshTimers = new Map<string, NodeJS.Timeout>();\n private server: net.Server | null = null;\n\n public constructor(private readonly options: FormSpecPluginServiceOptions) {\n this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);\n this.manifest = createFormSpecAnalysisManifest(\n options.workspaceRoot,\n options.typescriptVersion,\n Date.now()\n );\n }\n\n public getManifest(): FormSpecAnalysisManifest {\n return this.manifest;\n }\n\n public async start(): Promise<void> {\n if (this.server !== null) {\n return;\n }\n\n await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n\n this.server = net.createServer((socket) => {\n let buffer = \"\";\n socket.setEncoding(\"utf8\");\n socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {\n this.options.logger?.info(\n `[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`\n );\n socket.destroy();\n });\n socket.on(\"data\", (chunk) => {\n buffer += String(chunk);\n if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {\n socket.end(\n `${JSON.stringify({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`,\n } satisfies FormSpecSemanticResponse)}\\n`\n );\n return;\n }\n const newlineIndex = buffer.indexOf(\"\\n\");\n if (newlineIndex < 0) {\n return;\n }\n\n const payload = buffer.slice(0, newlineIndex);\n const remaining = buffer.slice(newlineIndex + 1);\n if (remaining.trim().length > 0) {\n this.options.logger?.info(\n `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`\n );\n }\n buffer = remaining;\n // The FormSpec IPC transport is intentionally one-request-per-connection.\n this.respondToSocket(socket, payload);\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const handleError = (error: Error) => {\n reject(error);\n };\n this.server?.once(\"error\", handleError);\n this.server?.listen(this.runtimePaths.endpoint.address, () => {\n this.server?.off(\"error\", handleError);\n resolve();\n });\n });\n\n await this.writeManifest();\n }\n\n public async stop(): Promise<void> {\n for (const timer of this.refreshTimers.values()) {\n clearTimeout(timer);\n }\n this.refreshTimers.clear();\n this.snapshotCache.clear();\n\n const server = this.server;\n this.server = null;\n if (server?.listening === true) {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error === undefined) {\n resolve();\n return;\n }\n reject(error);\n });\n });\n }\n\n await this.cleanupRuntimeArtifacts();\n }\n\n public scheduleSnapshotRefresh(filePath: string): void {\n const existing = this.refreshTimers.get(filePath);\n if (existing !== undefined) {\n clearTimeout(existing);\n }\n\n const timer = setTimeout(() => {\n try {\n this.getFileSnapshot(filePath, undefined);\n } catch (error: unknown) {\n this.options.logger?.info(\n `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`\n );\n }\n this.refreshTimers.delete(filePath);\n }, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);\n\n this.refreshTimers.set(filePath, timer);\n }\n\n public handleQuery(query: FormSpecSemanticQuery): FormSpecSemanticResponse {\n const performance =\n this.options.enablePerformanceLogging === true\n ? createFormSpecPerformanceRecorder()\n : undefined;\n const response = optionalMeasure(\n performance,\n FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery,\n {\n kind: query.kind,\n ...(query.kind === \"health\" ? {} : { filePath: query.filePath }),\n },\n () => this.executeQuery(query, performance)\n );\n\n if (performance !== undefined) {\n this.logPerformanceEvents(performance.events);\n }\n\n return response;\n }\n\n private respondToSocket(socket: net.Socket, payload: string): void {\n try {\n const query = JSON.parse(payload) as unknown;\n if (!isFormSpecSemanticQuery(query)) {\n throw new Error(\"Invalid FormSpec semantic query payload\");\n }\n const response = this.handleQuery(query);\n socket.end(`${JSON.stringify(response)}\\n`);\n } catch (error) {\n socket.end(\n `${JSON.stringify({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } satisfies FormSpecSemanticResponse)}\\n`\n );\n }\n }\n\n private async writeManifest(): Promise<void> {\n const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;\n await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}\\n`, \"utf8\");\n await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);\n }\n\n private async cleanupRuntimeArtifacts(): Promise<void> {\n await fs.rm(this.runtimePaths.manifestPath, { force: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n }\n\n private withCommentQueryContext(\n filePath: string,\n offset: number,\n handler: (context: CommentQueryContext) => FormSpecSemanticResponse,\n performance: FormSpecPerformanceRecorder | undefined\n ): FormSpecSemanticResponse {\n return optionalMeasure(\n performance,\n \"plugin.resolveCommentQueryContext\",\n {\n filePath,\n offset,\n },\n () => {\n const environment = this.getSourceEnvironment(filePath, performance);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${filePath}`,\n };\n }\n\n const declaration = optionalMeasure(\n performance,\n \"plugin.findDeclarationForCommentOffset\",\n {\n filePath,\n offset,\n },\n () => findDeclarationForCommentOffset(environment.sourceFile, offset)\n );\n const placement =\n declaration === null\n ? null\n : optionalMeasure(performance, \"plugin.resolveDeclarationPlacement\", undefined, () =>\n resolveDeclarationPlacement(declaration)\n );\n const subjectType =\n declaration === null\n ? undefined\n : optionalMeasure(performance, \"plugin.getSubjectType\", undefined, () =>\n getSubjectType(declaration, environment.checker)\n );\n\n return handler({\n ...environment,\n declaration,\n placement,\n subjectType,\n });\n }\n );\n }\n\n private getFileSnapshot(\n filePath: string,\n performance: FormSpecPerformanceRecorder | undefined\n ): FormSpecAnalysisFileSnapshot {\n const startedAt = getFormSpecPerformanceNow();\n const environment = this.getSourceEnvironment(filePath, performance);\n if (environment === null) {\n const missingSourceSnapshot: FormSpecAnalysisFileSnapshot = {\n filePath,\n sourceHash: \"\",\n generatedAt: this.getNow().toISOString(),\n comments: [],\n diagnostics: [\n {\n code: \"MISSING_SOURCE_FILE\",\n message: `Unable to resolve TypeScript source file for ${filePath}`,\n range: { start: 0, end: 0 },\n severity: \"warning\",\n },\n ],\n };\n performance?.record({\n name: \"plugin.getFileSnapshot\",\n durationMs: getFormSpecPerformanceNow() - startedAt,\n detail: {\n filePath,\n cache: \"missing-source\",\n },\n });\n return missingSourceSnapshot;\n }\n\n const cached = this.snapshotCache.get(filePath);\n if (cached?.sourceHash === environment.sourceHash) {\n performance?.record({\n name: \"plugin.getFileSnapshot\",\n durationMs: getFormSpecPerformanceNow() - startedAt,\n detail: {\n filePath,\n cache: \"hit\",\n },\n });\n return cached.snapshot;\n }\n\n const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {\n checker: environment.checker,\n now: () => this.getNow(),\n ...(performance === undefined ? {} : { performance }),\n } satisfies BuildFormSpecAnalysisFileSnapshotOptions);\n this.snapshotCache.set(filePath, {\n sourceHash: environment.sourceHash,\n snapshot,\n });\n performance?.record({\n name: \"plugin.getFileSnapshot\",\n durationMs: getFormSpecPerformanceNow() - startedAt,\n detail: {\n filePath,\n cache: \"miss\",\n },\n });\n return snapshot;\n }\n\n private getNow(): Date {\n return this.options.now?.() ?? new Date();\n }\n\n private executeQuery(\n query: FormSpecSemanticQuery,\n performance: FormSpecPerformanceRecorder | undefined\n ): FormSpecSemanticResponse {\n switch (query.kind) {\n case \"health\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"health\",\n manifest: this.manifest,\n };\n case \"completion\":\n return this.withCommentQueryContext(\n query.filePath,\n query.offset,\n (context) => ({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"completion\",\n sourceHash: context.sourceHash,\n context: serializeCompletionContext(\n getSemanticCommentCompletionContextAtOffset(context.sourceFile.text, query.offset, {\n checker: context.checker,\n ...(context.placement === null ? {} : { placement: context.placement }),\n ...(context.subjectType === undefined ? {} : { subjectType: context.subjectType }),\n })\n ),\n }),\n performance\n );\n case \"hover\":\n return this.withCommentQueryContext(\n query.filePath,\n query.offset,\n (context) => ({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"hover\",\n sourceHash: context.sourceHash,\n hover: serializeHoverInfo(\n getCommentHoverInfoAtOffset(context.sourceFile.text, query.offset, {\n checker: context.checker,\n ...(context.placement === null ? {} : { placement: context.placement }),\n ...(context.subjectType === undefined ? {} : { subjectType: context.subjectType }),\n })\n ),\n }),\n performance\n );\n case \"diagnostics\": {\n const snapshot = this.getFileSnapshot(query.filePath, performance);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"diagnostics\",\n sourceHash: snapshot.sourceHash,\n diagnostics: snapshot.diagnostics,\n };\n }\n case \"file-snapshot\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"file-snapshot\",\n snapshot: this.getFileSnapshot(query.filePath, performance),\n };\n default: {\n throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);\n }\n }\n }\n\n private getSourceEnvironment(\n filePath: string,\n performance: FormSpecPerformanceRecorder | undefined\n ): SourceEnvironment | null {\n return optionalMeasure(\n performance,\n \"plugin.getSourceEnvironment\",\n {\n filePath,\n },\n () => {\n const program = optionalMeasure(\n performance,\n \"plugin.sourceEnvironment.getProgram\",\n undefined,\n () => this.options.getProgram()\n );\n if (program === undefined) {\n return null;\n }\n\n const sourceFile = optionalMeasure(\n performance,\n \"plugin.sourceEnvironment.getSourceFile\",\n undefined,\n () => program.getSourceFile(filePath)\n );\n if (sourceFile === undefined) {\n return null;\n }\n\n const checker = optionalMeasure(\n performance,\n \"plugin.sourceEnvironment.getTypeChecker\",\n undefined,\n () => program.getTypeChecker()\n );\n const sourceHash = optionalMeasure(\n performance,\n \"plugin.sourceEnvironment.computeTextHash\",\n undefined,\n () => computeFormSpecTextHash(sourceFile.text)\n );\n\n return {\n sourceFile,\n checker,\n sourceHash,\n };\n }\n );\n }\n\n private logPerformanceEvents(events: readonly FormSpecPerformanceEvent[]): void {\n const logger = this.options.logger;\n if (logger === undefined || events.length === 0) {\n return;\n }\n\n let rootEvent: FormSpecPerformanceEvent | undefined;\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const candidate = events[index];\n if (candidate?.name === FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery) {\n rootEvent = candidate;\n break;\n }\n }\n if (rootEvent === undefined) {\n return;\n }\n\n const thresholdMs =\n this.options.performanceLogThresholdMs ??\n FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;\n if (rootEvent.durationMs < thresholdMs) {\n return;\n }\n\n const sortedHotspots = [...events]\n .filter((event) => event.name !== FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery)\n .sort((left, right) => right.durationMs - left.durationMs)\n .slice(0, 8);\n const lines = [\n `[FormSpec][perf] ${rootEvent.name} ${formatPerformanceEvent(rootEvent)}`,\n ...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`),\n ];\n logger.info(lines.join(\"\\n\"));\n }\n}\n\nfunction formatPerformanceEvent(event: FormSpecPerformanceEvent): string {\n const detailEntries = Object.entries(event.detail ?? {})\n .map(([key, value]) => `${key}=${String(value)}`)\n .join(\" \");\n return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === \"\" ? \"\" : ` ${detailEntries}`}`;\n}\n\nexport function createLanguageServiceProxy(\n languageService: ts.LanguageService,\n semanticService: FormSpecPluginService\n): ts.LanguageService {\n const wrapWithSnapshotRefresh = <Args extends readonly unknown[], Result>(\n fn: (fileName: string, ...args: Args) => Result\n ) => {\n return (fileName: string, ...args: Args): Result => {\n semanticService.scheduleSnapshotRefresh(fileName);\n return fn(fileName, ...args);\n };\n };\n\n // The plugin keeps semantic snapshots fresh for the lightweight LSP. The\n // underlying tsserver results still come from the original language service.\n const getSemanticDiagnostics = wrapWithSnapshotRefresh((fileName) =>\n languageService.getSemanticDiagnostics(fileName)\n );\n\n const getCompletionsAtPosition = wrapWithSnapshotRefresh(\n (fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined) =>\n languageService.getCompletionsAtPosition(fileName, position, options)\n );\n\n const getQuickInfoAtPosition = wrapWithSnapshotRefresh((fileName, position: number) =>\n languageService.getQuickInfoAtPosition(fileName, position)\n );\n\n return new Proxy(languageService, {\n get(target, property, receiver) {\n switch (property) {\n case \"getSemanticDiagnostics\":\n return getSemanticDiagnostics;\n case \"getCompletionsAtPosition\":\n return getCompletionsAtPosition;\n case \"getQuickInfoAtPosition\":\n return getQuickInfoAtPosition;\n default:\n return Reflect.get(target, property, receiver) as unknown;\n }\n },\n });\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n getFormSpecManifestPath,\n getFormSpecWorkspaceId,\n getFormSpecWorkspaceRuntimeDirectory,\n type FormSpecAnalysisManifest,\n type FormSpecIpcEndpoint,\n} from \"@formspec/analysis/protocol\";\n\nexport interface FormSpecWorkspaceRuntimePaths {\n readonly workspaceRoot: string;\n readonly workspaceId: string;\n readonly runtimeDirectory: string;\n readonly manifestPath: string;\n readonly endpoint: FormSpecIpcEndpoint;\n}\n\nexport function getFormSpecWorkspaceRuntimePaths(\n workspaceRoot: string,\n platform = process.platform,\n userScope = getFormSpecUserScope()\n): FormSpecWorkspaceRuntimePaths {\n const workspaceId = getFormSpecWorkspaceId(workspaceRoot);\n const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);\n const sanitizedUserScope = sanitizeScopeSegment(userScope);\n const endpoint: FormSpecIpcEndpoint =\n platform === \"win32\"\n ? {\n kind: \"windows-pipe\",\n address: `\\\\\\\\.\\\\pipe\\\\formspec-${sanitizedUserScope}-${workspaceId}`,\n }\n : {\n kind: \"unix-socket\",\n address: path.join(os.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`),\n };\n\n return {\n workspaceRoot,\n workspaceId,\n runtimeDirectory,\n manifestPath: getFormSpecManifestPath(workspaceRoot),\n endpoint,\n };\n}\n\nexport function createFormSpecAnalysisManifest(\n workspaceRoot: string,\n typescriptVersion: string,\n generation: number,\n extensionFingerprint = \"builtin\"\n): FormSpecAnalysisManifest {\n const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n analysisSchemaVersion: FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n workspaceRoot,\n workspaceId: paths.workspaceId,\n endpoint: paths.endpoint,\n typescriptVersion,\n extensionFingerprint,\n generation,\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction getFormSpecUserScope(): string {\n try {\n return sanitizeScopeSegment(os.userInfo().username);\n } catch {\n return sanitizeScopeSegment(\n process.env[\"USER\"] ?? process.env[\"USERNAME\"] ?? process.env[\"LOGNAME\"] ?? \"formspec\"\n );\n }\n}\n\nfunction sanitizeScopeSegment(value: string): string {\n const trimmed = value.trim().toLowerCase();\n const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, \"-\").replace(/-+/gu, \"-\");\n return sanitized.length > 0 ? sanitized : \"formspec\";\n}\n","export const FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;\nexport const FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 30_000;\nexport const FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;\nexport const FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;\n\nexport const FORM_SPEC_PLUGIN_PERFORMANCE_EVENT = {\n handleQuery: \"plugin.handleQuery\",\n} as const;\n"],"mappings":"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","names":["import_protocol","path","os","fs","net"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,mCAAmC,CAAC;AA6EnE;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,QAAQ,CAAC;CACtC,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAQ/B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,mCAAmC,CAAC;AAiGnE;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,QAAQ,CAAC;CACtC,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAQ/B"}
package/dist/index.js CHANGED
@@ -4,17 +4,22 @@ import net from "net";
4
4
  import "typescript";
5
5
  import {
6
6
  FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
7
- buildFormSpecAnalysisFileSnapshot,
8
7
  computeFormSpecTextHash,
8
+ isFormSpecSemanticQuery
9
+ } from "@formspec/analysis/protocol";
10
+ import {
11
+ buildFormSpecAnalysisFileSnapshot,
12
+ createFormSpecPerformanceRecorder,
9
13
  findDeclarationForCommentOffset,
10
- getSubjectType,
11
14
  getCommentHoverInfoAtOffset,
12
15
  getSemanticCommentCompletionContextAtOffset,
13
- isFormSpecSemanticQuery,
16
+ getFormSpecPerformanceNow,
17
+ getSubjectType,
18
+ optionalMeasure,
14
19
  resolveDeclarationPlacement,
15
20
  serializeCompletionContext,
16
21
  serializeHoverInfo
17
- } from "@formspec/analysis";
22
+ } from "@formspec/analysis/internal";
18
23
 
19
24
  // src/workspace.ts
20
25
  import os from "os";
@@ -25,7 +30,7 @@ import {
25
30
  getFormSpecManifestPath,
26
31
  getFormSpecWorkspaceId,
27
32
  getFormSpecWorkspaceRuntimeDirectory
28
- } from "@formspec/analysis";
33
+ } from "@formspec/analysis/protocol";
29
34
  function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
30
35
  const workspaceId = getFormSpecWorkspaceId(workspaceRoot);
31
36
  const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);
@@ -74,6 +79,15 @@ function sanitizeScopeSegment(value) {
74
79
  return sanitized.length > 0 ? sanitized : "formspec";
75
80
  }
76
81
 
82
+ // src/constants.ts
83
+ var FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;
84
+ var FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 3e4;
85
+ var FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
86
+ var FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
87
+ var FORM_SPEC_PLUGIN_PERFORMANCE_EVENT = {
88
+ handleQuery: "plugin.handleQuery"
89
+ };
90
+
77
91
  // src/service.ts
78
92
  var FormSpecPluginService = class {
79
93
  constructor(options) {
@@ -104,8 +118,25 @@ var FormSpecPluginService = class {
104
118
  this.server = net.createServer((socket) => {
105
119
  let buffer = "";
106
120
  socket.setEncoding("utf8");
121
+ socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
122
+ this.options.logger?.info(
123
+ `[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
124
+ );
125
+ socket.destroy();
126
+ });
107
127
  socket.on("data", (chunk) => {
108
128
  buffer += String(chunk);
129
+ if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
130
+ socket.end(
131
+ `${JSON.stringify({
132
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
133
+ kind: "error",
134
+ error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
135
+ })}
136
+ `
137
+ );
138
+ return;
139
+ }
109
140
  const newlineIndex = buffer.indexOf("\n");
110
141
  if (newlineIndex < 0) {
111
142
  return;
@@ -161,95 +192,31 @@ var FormSpecPluginService = class {
161
192
  }
162
193
  const timer = setTimeout(() => {
163
194
  try {
164
- this.getFileSnapshot(filePath);
195
+ this.getFileSnapshot(filePath, void 0);
165
196
  } catch (error) {
166
197
  this.options.logger?.info(
167
198
  `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
168
199
  );
169
200
  }
170
201
  this.refreshTimers.delete(filePath);
171
- }, this.options.snapshotDebounceMs ?? 250);
202
+ }, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);
172
203
  this.refreshTimers.set(filePath, timer);
173
204
  }
174
205
  handleQuery(query) {
175
- switch (query.kind) {
176
- case "health":
177
- return {
178
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
179
- kind: "health",
180
- manifest: this.manifest
181
- };
182
- case "completion": {
183
- const environment = this.getSourceEnvironment(query.filePath);
184
- if (environment === null) {
185
- return {
186
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
187
- kind: "error",
188
- error: `Unable to resolve TypeScript source file for ${query.filePath}`
189
- };
190
- }
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
- return {
204
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
205
- kind: "completion",
206
- sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
207
- context: serializeCompletionContext(context)
208
- };
209
- }
210
- case "hover": {
211
- const environment = this.getSourceEnvironment(query.filePath);
212
- if (environment === null) {
213
- return {
214
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
215
- kind: "error",
216
- error: `Unable to resolve TypeScript source file for ${query.filePath}`
217
- };
218
- }
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
- return {
228
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
229
- kind: "hover",
230
- sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
231
- hover: serializeHoverInfo(hover)
232
- };
233
- }
234
- case "diagnostics": {
235
- const snapshot = this.getFileSnapshot(query.filePath);
236
- return {
237
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
238
- kind: "diagnostics",
239
- sourceHash: snapshot.sourceHash,
240
- diagnostics: snapshot.diagnostics
241
- };
242
- }
243
- case "file-snapshot":
244
- return {
245
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
246
- kind: "file-snapshot",
247
- snapshot: this.getFileSnapshot(query.filePath)
248
- };
249
- default: {
250
- throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
251
- }
206
+ const performance = this.options.enablePerformanceLogging === true ? createFormSpecPerformanceRecorder() : void 0;
207
+ const response = optionalMeasure(
208
+ performance,
209
+ FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery,
210
+ {
211
+ kind: query.kind,
212
+ ...query.kind === "health" ? {} : { filePath: query.filePath }
213
+ },
214
+ () => this.executeQuery(query, performance)
215
+ );
216
+ if (performance !== void 0) {
217
+ this.logPerformanceEvents(performance.events);
252
218
  }
219
+ return response;
253
220
  }
254
221
  respondToSocket(socket, payload) {
255
222
  try {
@@ -283,24 +250,58 @@ var FormSpecPluginService = class {
283
250
  await fs.rm(this.runtimePaths.endpoint.address, { force: true });
284
251
  }
285
252
  }
286
- getSourceEnvironment(filePath) {
287
- const program = this.options.getProgram();
288
- if (program === void 0) {
289
- return null;
290
- }
291
- const sourceFile = program.getSourceFile(filePath);
292
- if (sourceFile === void 0) {
293
- return null;
294
- }
295
- return {
296
- sourceFile,
297
- checker: program.getTypeChecker()
298
- };
253
+ withCommentQueryContext(filePath, offset, handler, performance) {
254
+ return optionalMeasure(
255
+ performance,
256
+ "plugin.resolveCommentQueryContext",
257
+ {
258
+ filePath,
259
+ offset
260
+ },
261
+ () => {
262
+ const environment = this.getSourceEnvironment(filePath, performance);
263
+ if (environment === null) {
264
+ return {
265
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
266
+ kind: "error",
267
+ error: `Unable to resolve TypeScript source file for ${filePath}`
268
+ };
269
+ }
270
+ const declaration = optionalMeasure(
271
+ performance,
272
+ "plugin.findDeclarationForCommentOffset",
273
+ {
274
+ filePath,
275
+ offset
276
+ },
277
+ () => findDeclarationForCommentOffset(environment.sourceFile, offset)
278
+ );
279
+ const placement = declaration === null ? null : optionalMeasure(
280
+ performance,
281
+ "plugin.resolveDeclarationPlacement",
282
+ void 0,
283
+ () => resolveDeclarationPlacement(declaration)
284
+ );
285
+ const subjectType = declaration === null ? void 0 : optionalMeasure(
286
+ performance,
287
+ "plugin.getSubjectType",
288
+ void 0,
289
+ () => getSubjectType(declaration, environment.checker)
290
+ );
291
+ return handler({
292
+ ...environment,
293
+ declaration,
294
+ placement,
295
+ subjectType
296
+ });
297
+ }
298
+ );
299
299
  }
300
- getFileSnapshot(filePath) {
301
- const environment = this.getSourceEnvironment(filePath);
300
+ getFileSnapshot(filePath, performance) {
301
+ const startedAt = getFormSpecPerformanceNow();
302
+ const environment = this.getSourceEnvironment(filePath, performance);
302
303
  if (environment === null) {
303
- return {
304
+ const missingSourceSnapshot = {
304
305
  filePath,
305
306
  sourceHash: "",
306
307
  generatedAt: this.getNow().toISOString(),
@@ -314,25 +315,192 @@ var FormSpecPluginService = class {
314
315
  }
315
316
  ]
316
317
  };
318
+ performance?.record({
319
+ name: "plugin.getFileSnapshot",
320
+ durationMs: getFormSpecPerformanceNow() - startedAt,
321
+ detail: {
322
+ filePath,
323
+ cache: "missing-source"
324
+ }
325
+ });
326
+ return missingSourceSnapshot;
317
327
  }
318
- const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);
319
328
  const cached = this.snapshotCache.get(filePath);
320
- if (cached?.sourceHash === sourceHash) {
329
+ if (cached?.sourceHash === environment.sourceHash) {
330
+ performance?.record({
331
+ name: "plugin.getFileSnapshot",
332
+ durationMs: getFormSpecPerformanceNow() - startedAt,
333
+ detail: {
334
+ filePath,
335
+ cache: "hit"
336
+ }
337
+ });
321
338
  return cached.snapshot;
322
339
  }
323
340
  const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {
324
- checker: environment.checker
341
+ checker: environment.checker,
342
+ now: () => this.getNow(),
343
+ ...performance === void 0 ? {} : { performance }
325
344
  });
326
345
  this.snapshotCache.set(filePath, {
327
- sourceHash,
346
+ sourceHash: environment.sourceHash,
328
347
  snapshot
329
348
  });
349
+ performance?.record({
350
+ name: "plugin.getFileSnapshot",
351
+ durationMs: getFormSpecPerformanceNow() - startedAt,
352
+ detail: {
353
+ filePath,
354
+ cache: "miss"
355
+ }
356
+ });
330
357
  return snapshot;
331
358
  }
332
359
  getNow() {
333
360
  return this.options.now?.() ?? /* @__PURE__ */ new Date();
334
361
  }
362
+ executeQuery(query, performance) {
363
+ switch (query.kind) {
364
+ case "health":
365
+ return {
366
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
367
+ kind: "health",
368
+ manifest: this.manifest
369
+ };
370
+ case "completion":
371
+ return this.withCommentQueryContext(
372
+ query.filePath,
373
+ query.offset,
374
+ (context) => ({
375
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
376
+ kind: "completion",
377
+ sourceHash: context.sourceHash,
378
+ context: serializeCompletionContext(
379
+ getSemanticCommentCompletionContextAtOffset(context.sourceFile.text, query.offset, {
380
+ checker: context.checker,
381
+ ...context.placement === null ? {} : { placement: context.placement },
382
+ ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
383
+ })
384
+ )
385
+ }),
386
+ performance
387
+ );
388
+ case "hover":
389
+ return this.withCommentQueryContext(
390
+ query.filePath,
391
+ query.offset,
392
+ (context) => ({
393
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
394
+ kind: "hover",
395
+ sourceHash: context.sourceHash,
396
+ hover: serializeHoverInfo(
397
+ getCommentHoverInfoAtOffset(context.sourceFile.text, query.offset, {
398
+ checker: context.checker,
399
+ ...context.placement === null ? {} : { placement: context.placement },
400
+ ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
401
+ })
402
+ )
403
+ }),
404
+ performance
405
+ );
406
+ case "diagnostics": {
407
+ const snapshot = this.getFileSnapshot(query.filePath, performance);
408
+ return {
409
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
410
+ kind: "diagnostics",
411
+ sourceHash: snapshot.sourceHash,
412
+ diagnostics: snapshot.diagnostics
413
+ };
414
+ }
415
+ case "file-snapshot":
416
+ return {
417
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
418
+ kind: "file-snapshot",
419
+ snapshot: this.getFileSnapshot(query.filePath, performance)
420
+ };
421
+ default: {
422
+ throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
423
+ }
424
+ }
425
+ }
426
+ getSourceEnvironment(filePath, performance) {
427
+ return optionalMeasure(
428
+ performance,
429
+ "plugin.getSourceEnvironment",
430
+ {
431
+ filePath
432
+ },
433
+ () => {
434
+ const program = optionalMeasure(
435
+ performance,
436
+ "plugin.sourceEnvironment.getProgram",
437
+ void 0,
438
+ () => this.options.getProgram()
439
+ );
440
+ if (program === void 0) {
441
+ return null;
442
+ }
443
+ const sourceFile = optionalMeasure(
444
+ performance,
445
+ "plugin.sourceEnvironment.getSourceFile",
446
+ void 0,
447
+ () => program.getSourceFile(filePath)
448
+ );
449
+ if (sourceFile === void 0) {
450
+ return null;
451
+ }
452
+ const checker = optionalMeasure(
453
+ performance,
454
+ "plugin.sourceEnvironment.getTypeChecker",
455
+ void 0,
456
+ () => program.getTypeChecker()
457
+ );
458
+ const sourceHash = optionalMeasure(
459
+ performance,
460
+ "plugin.sourceEnvironment.computeTextHash",
461
+ void 0,
462
+ () => computeFormSpecTextHash(sourceFile.text)
463
+ );
464
+ return {
465
+ sourceFile,
466
+ checker,
467
+ sourceHash
468
+ };
469
+ }
470
+ );
471
+ }
472
+ logPerformanceEvents(events) {
473
+ const logger = this.options.logger;
474
+ if (logger === void 0 || events.length === 0) {
475
+ return;
476
+ }
477
+ let rootEvent;
478
+ for (let index = events.length - 1; index >= 0; index -= 1) {
479
+ const candidate = events[index];
480
+ if (candidate?.name === FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery) {
481
+ rootEvent = candidate;
482
+ break;
483
+ }
484
+ }
485
+ if (rootEvent === void 0) {
486
+ return;
487
+ }
488
+ const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
489
+ if (rootEvent.durationMs < thresholdMs) {
490
+ return;
491
+ }
492
+ const sortedHotspots = [...events].filter((event) => event.name !== FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
493
+ const lines = [
494
+ `[FormSpec][perf] ${rootEvent.name} ${formatPerformanceEvent(rootEvent)}`,
495
+ ...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`)
496
+ ];
497
+ logger.info(lines.join("\n"));
498
+ }
335
499
  };
500
+ function formatPerformanceEvent(event) {
501
+ const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
502
+ return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
503
+ }
336
504
  function createLanguageServiceProxy(languageService, semanticService) {
337
505
  const wrapWithSnapshotRefresh = (fn) => {
338
506
  return (fileName, ...args) => {
@@ -367,9 +535,23 @@ function createLanguageServiceProxy(languageService, semanticService) {
367
535
 
368
536
  // src/index.ts
369
537
  var services = /* @__PURE__ */ new Map();
538
+ var PERF_LOG_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE";
539
+ var PERF_LOG_THRESHOLD_ENV_VAR = "FORMSPEC_PLUGIN_PROFILE_THRESHOLD_MS";
370
540
  function formatPluginError(error) {
371
541
  return error instanceof Error ? error.stack ?? error.message : String(error);
372
542
  }
543
+ function readBooleanEnvFlag(name) {
544
+ const rawValue = process.env[name];
545
+ return rawValue === "1" || rawValue === "true";
546
+ }
547
+ function readNumberEnvFlag(name) {
548
+ const rawValue = process.env[name];
549
+ if (rawValue === void 0 || rawValue.trim() === "") {
550
+ return void 0;
551
+ }
552
+ const parsed = Number(rawValue);
553
+ return Number.isFinite(parsed) ? parsed : void 0;
554
+ }
373
555
  function getOrCreateService(info, typescriptVersion) {
374
556
  const workspaceRoot = info.project.getCurrentDirectory();
375
557
  const existing = services.get(workspaceRoot);
@@ -378,11 +560,14 @@ function getOrCreateService(info, typescriptVersion) {
378
560
  attachProjectCloseHandler(info, workspaceRoot, existing);
379
561
  return existing.service;
380
562
  }
563
+ const performanceLogThresholdMs = readNumberEnvFlag(PERF_LOG_THRESHOLD_ENV_VAR);
381
564
  const service = new FormSpecPluginService({
382
565
  workspaceRoot,
383
566
  typescriptVersion,
384
567
  getProgram: () => info.languageService.getProgram(),
385
- logger: info.project.projectService.logger
568
+ logger: info.project.projectService.logger,
569
+ enablePerformanceLogging: readBooleanEnvFlag(PERF_LOG_ENV_VAR),
570
+ ...performanceLogThresholdMs === void 0 ? {} : { performanceLogThresholdMs }
386
571
  });
387
572
  const serviceEntry = {
388
573
  service,