@formspec/language-server 0.1.0-alpha.17 → 0.1.0-alpha.20

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/server.ts","../src/providers/completion.ts","../src/providers/hover.ts","../src/providers/definition.ts"],"sourcesContent":["/**\n * \\@formspec/language-server\n *\n * Language server for FormSpec — provides completions, hover documentation,\n * and go-to-definition for FormSpec JSDoc constraint tags (`@Minimum`,\n * `@Maximum`, `@Pattern`, etc.) in TypeScript files.\n *\n * This package implements the Language Server Protocol (LSP) using the\n * `vscode-languageserver` library. Constraint names are sourced from\n * `BUILTIN_CONSTRAINT_DEFINITIONS` in `@formspec/core`, ensuring the\n * language server stays in sync with the single source of truth.\n *\n * Diagnostics are intentionally omitted per design decision A7.\n *\n * @example\n * ```ts\n * import { createServer } from '@formspec/language-server';\n *\n * const connection = createServer();\n * connection.listen();\n * ```\n *\n * @packageDocumentation\n */\n\nexport { createServer } from \"./server.js\";\nexport type { CreateServerOptions } from \"./server.js\";\nexport { getCompletionItems } from \"./providers/completion.js\";\nexport { getHoverForTag } from \"./providers/hover.js\";\nexport { getDefinition } from \"./providers/definition.js\";\n","/**\n * FormSpec Language Server\n *\n * Sets up an LSP server connection and registers handlers for:\n * - `textDocument/completion` — FormSpec JSDoc constraint tag completions\n * - `textDocument/hover` — Documentation for recognized constraint tags\n * - `textDocument/definition` — Go-to-definition (stub, returns null)\n *\n * Diagnostics are intentionally omitted per design decision A7.\n */\n\nimport {\n createConnection,\n ProposedFeatures,\n TextDocumentSyncKind,\n type Connection,\n type InitializeResult,\n} from \"vscode-languageserver/node.js\";\nimport type { ExtensionDefinition } from \"@formspec/core\";\nimport { getCompletionItems } from \"./providers/completion.js\";\nimport { getHoverForTag } from \"./providers/hover.js\";\nimport { getDefinition } from \"./providers/definition.js\";\n\nexport interface CreateServerOptions {\n /** Optional extension definitions whose custom tags should be surfaced by tooling. */\n readonly extensions?: readonly ExtensionDefinition[];\n}\n\n/**\n * Creates and configures the FormSpec language server connection.\n *\n * Registers LSP capability handlers and returns the connection.\n * Call `connection.listen()` to start accepting messages.\n *\n * @returns The configured LSP connection (not yet listening)\n */\nexport function createServer(options: CreateServerOptions = {}): Connection {\n const connection = createConnection(ProposedFeatures.all);\n\n connection.onInitialize((): InitializeResult => {\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n completionProvider: {\n // Trigger completions inside JSDoc comments when `@` is typed\n triggerCharacters: [\"@\"],\n },\n hoverProvider: true,\n definitionProvider: true,\n },\n serverInfo: {\n name: \"formspec-language-server\",\n version: \"0.1.0\",\n },\n };\n });\n\n connection.onCompletion(() => {\n // Return all FormSpec constraint tag completions.\n // Future phases will add context-aware filtering based on field type and\n // cursor position within JSDoc comment ranges.\n return getCompletionItems(options.extensions);\n });\n\n connection.onHover((_params) => {\n // Extract the word under the cursor and look up hover documentation.\n // This is a stub — precise JSDoc token detection (checking that the\n // cursor is within a JSDoc comment and extracting the tag name) will be\n // added in a future phase.\n //\n // For now we return null to signal no hover is available until the\n // token extraction is implemented.\n return getHoverForTag(\"\", options.extensions);\n });\n\n connection.onDefinition((_params) => {\n // Go-to-definition is not yet implemented.\n return getDefinition();\n });\n\n return connection;\n}\n","/**\n * Completion provider for FormSpec JSDoc constraint tags.\n *\n * Returns completion items for all recognized FormSpec JSDoc constraint tags\n * (e.g., `@minimum`, `@maximum`, `@pattern`), derived from\n * `BUILTIN_CONSTRAINT_DEFINITIONS`. This is a skeleton — context-aware\n * filtering will be added in a future phase.\n */\n\nimport {\n BUILTIN_CONSTRAINT_DEFINITIONS,\n type BuiltinConstraintName,\n type ExtensionDefinition,\n} from \"@formspec/core\";\nimport { CompletionItem, CompletionItemKind } from \"vscode-languageserver/node.js\";\n\n/**\n * Human-readable detail strings for each built-in constraint tag.\n *\n * Keys match the camelCase constraint names (matching keys in `BUILTIN_CONSTRAINT_DEFINITIONS`).\n * Values are shown as the detail string in completion items.\n */\nconst CONSTRAINT_DETAIL: Record<BuiltinConstraintName, string> = {\n minimum: \"Minimum numeric value (inclusive). Example: `@minimum 0`\",\n maximum: \"Maximum numeric value (inclusive). Example: `@maximum 100`\",\n exclusiveMinimum: \"Minimum numeric value (exclusive). Example: `@exclusiveMinimum 0`\",\n exclusiveMaximum: \"Maximum numeric value (exclusive). Example: `@exclusiveMaximum 100`\",\n multipleOf: \"Value must be a multiple of this number. Example: `@multipleOf 0.01`\",\n minLength: \"Minimum string length. Example: `@minLength 1`\",\n maxLength: \"Maximum string length. Example: `@maxLength 255`\",\n minItems: \"Minimum number of array items. Example: `@minItems 1`\",\n maxItems: \"Maximum number of array items. Example: `@maxItems 10`\",\n uniqueItems: \"Require all array items to be distinct. Example: `@uniqueItems`\",\n pattern: \"Regular expression pattern for string validation. Example: `@pattern ^[a-z]+$`\",\n enumOptions: 'Inline JSON array of allowed enum values. Example: `@enumOptions [\"a\",\"b\",\"c\"]`',\n const: 'Require a constant JSON value. Example: `@const \"USD\"`',\n};\n\n/**\n * Returns completion items for all FormSpec JSDoc constraint tags.\n *\n * Items are derived from `BUILTIN_CONSTRAINT_DEFINITIONS`, ensuring this list\n * stays in sync with the single source of truth in `@formspec/core`.\n *\n * Each item uses `CompletionItemKind.Keyword` since these are annotation\n * tags used within JSDoc comments rather than language symbols.\n *\n * @returns An array of LSP completion items for FormSpec constraint tags\n */\nexport function getCompletionItems(extensions?: readonly ExtensionDefinition[]): CompletionItem[] {\n const builtins = (Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS) as BuiltinConstraintName[]).map(\n (name) => ({\n label: `@${name}`,\n kind: CompletionItemKind.Keyword,\n detail: CONSTRAINT_DETAIL[name],\n })\n );\n\n const customItems =\n extensions?.flatMap((extension) =>\n (extension.constraintTags ?? []).map((tag) => ({\n label: `@${tag.tagName}`,\n kind: CompletionItemKind.Keyword,\n detail: `Extension constraint tag from ${extension.extensionId}`,\n }))\n ) ?? [];\n\n return [...builtins, ...customItems];\n}\n","/**\n * Hover provider for FormSpec JSDoc constraint tags.\n *\n * Returns Markdown documentation for a recognized FormSpec JSDoc tag when\n * the cursor is positioned over it. This is a skeleton — precise token\n * detection within JSDoc comment ranges will be added in a future phase.\n */\n\nimport {\n normalizeConstraintTagName,\n isBuiltinConstraintName,\n type BuiltinConstraintName,\n type ExtensionDefinition,\n} from \"@formspec/core\";\nimport type { Hover } from \"vscode-languageserver/node.js\";\n\n/**\n * Markdown documentation for each built-in FormSpec constraint tag.\n *\n * Keys are the canonical constraint names from `BUILTIN_CONSTRAINT_DEFINITIONS`.\n * Values are Markdown strings suitable for LSP hover responses.\n */\nconst CONSTRAINT_HOVER_DOCS: Record<BuiltinConstraintName, string> = {\n minimum: [\n \"**@minimum** `<number>`\",\n \"\",\n \"Sets an inclusive lower bound on a numeric field.\",\n \"\",\n \"Maps to `minimum` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @minimum 0 */\",\n \"amount: number;\",\n \"```\",\n ].join(\"\\n\"),\n\n maximum: [\n \"**@maximum** `<number>`\",\n \"\",\n \"Sets an inclusive upper bound on a numeric field.\",\n \"\",\n \"Maps to `maximum` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @maximum 100 */\",\n \"percentage: number;\",\n \"```\",\n ].join(\"\\n\"),\n\n exclusiveMinimum: [\n \"**@exclusiveMinimum** `<number>`\",\n \"\",\n \"Sets an exclusive lower bound on a numeric field.\",\n \"\",\n \"Maps to `exclusiveMinimum` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @exclusiveMinimum 0 */\",\n \"positiveAmount: number;\",\n \"```\",\n ].join(\"\\n\"),\n\n exclusiveMaximum: [\n \"**@exclusiveMaximum** `<number>`\",\n \"\",\n \"Sets an exclusive upper bound on a numeric field.\",\n \"\",\n \"Maps to `exclusiveMaximum` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @exclusiveMaximum 1 */\",\n \"ratio: number;\",\n \"```\",\n ].join(\"\\n\"),\n\n multipleOf: [\n \"**@multipleOf** `<number>`\",\n \"\",\n \"Requires the numeric value to be a multiple of the given number.\",\n \"\",\n \"Maps to `multipleOf` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @multipleOf 0.01 */\",\n \"price: number;\",\n \"```\",\n ].join(\"\\n\"),\n\n minLength: [\n \"**@minLength** `<number>`\",\n \"\",\n \"Sets a minimum character length on a string field.\",\n \"\",\n \"Maps to `minLength` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @minLength 1 */\",\n \"name: string;\",\n \"```\",\n ].join(\"\\n\"),\n\n maxLength: [\n \"**@maxLength** `<number>`\",\n \"\",\n \"Sets a maximum character length on a string field.\",\n \"\",\n \"Maps to `maxLength` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @maxLength 255 */\",\n \"description: string;\",\n \"```\",\n ].join(\"\\n\"),\n\n minItems: [\n \"**@minItems** `<number>`\",\n \"\",\n \"Sets a minimum number of items in an array field.\",\n \"\",\n \"Maps to `minItems` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @minItems 1 */\",\n \"tags: string[];\",\n \"```\",\n ].join(\"\\n\"),\n\n maxItems: [\n \"**@maxItems** `<number>`\",\n \"\",\n \"Sets a maximum number of items in an array field.\",\n \"\",\n \"Maps to `maxItems` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @maxItems 10 */\",\n \"tags: string[];\",\n \"```\",\n ].join(\"\\n\"),\n\n uniqueItems: [\n \"**@uniqueItems**\",\n \"\",\n \"Requires all items in an array field to be distinct.\",\n \"\",\n \"Maps to `uniqueItems` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @uniqueItems */\",\n \"tags: string[];\",\n \"```\",\n ].join(\"\\n\"),\n\n pattern: [\n \"**@pattern** `<regex>`\",\n \"\",\n \"Sets a regular expression pattern that a string field must match.\",\n \"\",\n \"Maps to `pattern` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n \"/** @pattern ^[a-z0-9]+$ */\",\n \"slug: string;\",\n \"```\",\n ].join(\"\\n\"),\n\n enumOptions: [\n \"**@enumOptions** `<json-array>`\",\n \"\",\n \"Specifies the allowed values for an enum field as an inline JSON array.\",\n \"\",\n \"Maps to `enum` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n '/** @enumOptions [\"draft\",\"sent\",\"archived\"] */',\n \"status: string;\",\n \"```\",\n ].join(\"\\n\"),\n\n const: [\n \"**@const** `<json-literal>`\",\n \"\",\n \"Requires the field value to equal a single constant JSON value.\",\n \"\",\n \"Maps to `const` in JSON Schema.\",\n \"\",\n \"**Example:**\",\n \"```typescript\",\n '/** @const \"USD\" */',\n \"currency: string;\",\n \"```\",\n ].join(\"\\n\"),\n} satisfies Record<BuiltinConstraintName, string>;\n\n/**\n * Returns hover documentation for a FormSpec JSDoc tag name.\n *\n * Accepts both camelCase (`\"minimum\"`) and PascalCase (`\"Minimum\"`) forms.\n * The `@` prefix is stripped before lookup if present.\n * Returns `null` when the tag is not a recognized FormSpec constraint tag.\n *\n * @param tagName - The tag name to look up (e.g., `\"minimum\"`, `\"@pattern\"`)\n * @returns An LSP `Hover` response, or `null` if the tag is not recognized\n */\nexport function getHoverForTag(\n tagName: string,\n extensions?: readonly ExtensionDefinition[]\n): Hover | null {\n // Strip leading `@` prefix if present\n const raw = tagName.startsWith(\"@\") ? tagName.slice(1) : tagName;\n const name = normalizeConstraintTagName(raw);\n\n if (!isBuiltinConstraintName(name)) {\n const registration = extensions\n ?.flatMap((extension) =>\n (extension.constraintTags ?? []).map((tag) => ({\n extensionId: extension.extensionId,\n tag,\n }))\n )\n .find(({ tag }) => tag.tagName === name);\n\n if (registration === undefined) {\n return null;\n }\n\n return {\n contents: {\n kind: \"markdown\",\n value: [\n `**@${registration.tag.tagName}** \\`<value>\\``,\n \"\",\n `Extension-defined constraint tag from \\`${registration.extensionId}\\`.`,\n \"\",\n \"Validated through the registered FormSpec extension surface.\",\n ].join(\"\\n\"),\n },\n };\n }\n\n return {\n contents: {\n kind: \"markdown\",\n value: CONSTRAINT_HOVER_DOCS[name],\n },\n };\n}\n","/**\n * Go-to-definition provider for FormSpec.\n *\n * This is a stub — go-to-definition support (e.g., navigating from a\n * `field.text(\"name\")` call to the form definition that references it) will\n * be implemented in a future phase.\n */\n\nimport type { Location } from \"vscode-languageserver/node.js\";\n\n/**\n * Returns the definition location for a symbol at the given position.\n *\n * Always returns `null` in this stub implementation.\n *\n * @returns `null` — not yet implemented\n */\nexport function getDefinition(): Location | null {\n return null;\n}\n"],"mappings":"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","names":["import_node","import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/providers/completion.ts","../src/providers/hover.ts","../src/providers/definition.ts","../src/plugin-client.ts"],"sourcesContent":["/**\n * \\@formspec/language-server\n *\n * Language server for FormSpec — provides completions, hover documentation,\n * and go-to-definition for FormSpec JSDoc constraint tags (`@Minimum`,\n * `@Maximum`, `@Pattern`, etc.) in TypeScript files.\n *\n * This package implements the Language Server Protocol (LSP) using the\n * `vscode-languageserver` library. Cheap syntax-local behaviors stay in the\n * LSP process, while TypeScript-project-aware semantics are supplied by\n * `@formspec/ts-plugin` over a local manifest + IPC transport.\n *\n * Diagnostics are intentionally omitted per design decision A7.\n *\n * @example\n * ```ts\n * import { createServer } from '@formspec/language-server';\n *\n * const connection = createServer();\n * connection.listen();\n * ```\n *\n * @packageDocumentation\n */\n\nexport { createServer } from \"./server.js\";\nexport type { CreateServerOptions } from \"./server.js\";\nexport { getCompletionItems } from \"./providers/completion.js\";\nexport { getHoverForTag } from \"./providers/hover.js\";\nexport { getDefinition } from \"./providers/definition.js\";\n","/**\n * FormSpec Language Server\n *\n * Sets up an LSP server connection and registers handlers for:\n * - `textDocument/completion` — FormSpec JSDoc constraint tag completions\n * - `textDocument/hover` — Documentation for recognized constraint tags\n * - `textDocument/definition` — Go-to-definition (stub, returns null)\n *\n * Diagnostics are intentionally omitted per design decision A7.\n */\n\nimport {\n createConnection,\n ProposedFeatures,\n TextDocuments,\n TextDocumentSyncKind,\n type Connection,\n type InitializeResult,\n} from \"vscode-languageserver/node.js\";\nimport type { ExtensionDefinition } from \"@formspec/core\";\nimport { TextDocument } from \"vscode-languageserver-textdocument\";\nimport { getCompletionItemsAtOffset } from \"./providers/completion.js\";\nimport { getHoverAtOffset } from \"./providers/hover.js\";\nimport { getDefinition } from \"./providers/definition.js\";\nimport {\n fileUriToPathOrNull,\n getPluginCompletionContextForDocument,\n getPluginHoverForDocument,\n} from \"./plugin-client.js\";\n\nfunction dedupeWorkspaceRoots(workspaceRoots: readonly string[]): string[] {\n return [...new Set(workspaceRoots)];\n}\n\nfunction getWorkspaceRootsFromInitializeParams(params: {\n readonly workspaceFolders?: readonly { readonly uri: string }[] | null;\n readonly rootUri?: string | null;\n readonly rootPath?: string | null;\n}): string[] {\n const workspaceFolders =\n params.workspaceFolders\n ?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri))\n .filter((workspaceRoot): workspaceRoot is string => workspaceRoot !== null) ?? [];\n const rootUri =\n params.rootUri === null || params.rootUri === undefined\n ? null\n : fileUriToPathOrNull(params.rootUri);\n const rootPath = params.rootPath ?? null;\n\n return dedupeWorkspaceRoots([\n ...workspaceFolders,\n ...(rootUri === null ? [] : [rootUri]),\n ...(rootPath === null ? [] : [rootPath]),\n ]);\n}\n\n/**\n * Public configuration for constructing the FormSpec language server.\n *\n * @public\n */\nexport interface CreateServerOptions {\n /** Optional extension definitions whose custom tags should be surfaced by tooling. */\n readonly extensions?: readonly ExtensionDefinition[];\n /** Optional workspace roots to use before initialize() provides them. */\n readonly workspaceRoots?: readonly string[];\n /** Set to false to disable tsserver-plugin semantic enrichment. */\n readonly usePluginTransport?: boolean;\n /** IPC timeout, in milliseconds, for semantic plugin requests. */\n readonly pluginQueryTimeoutMs?: number;\n}\n\n/**\n * Creates and configures the FormSpec language server connection.\n *\n * Registers LSP capability handlers and returns the connection.\n * Call `connection.listen()` to start accepting messages.\n *\n * @returns The configured LSP connection (not yet listening)\n * @public\n */\nexport function createServer(options: CreateServerOptions = {}): Connection {\n const connection = createConnection(ProposedFeatures.all);\n const documents = new TextDocuments(TextDocument);\n let workspaceRoots = [...(options.workspaceRoots ?? [])];\n\n documents.listen(connection);\n\n connection.onInitialize((params): InitializeResult => {\n workspaceRoots = dedupeWorkspaceRoots([\n ...getWorkspaceRootsFromInitializeParams(params),\n ...workspaceRoots,\n ]);\n\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n completionProvider: {\n // Trigger completions inside JSDoc comments for tags and target specifiers\n triggerCharacters: [\"@\", \":\"],\n },\n hoverProvider: true,\n definitionProvider: true,\n },\n serverInfo: {\n name: \"formspec-language-server\",\n version: \"0.1.0\",\n },\n };\n });\n\n connection.onCompletion(async (params) => {\n const document = documents.get(params.textDocument.uri);\n if (!document) {\n return [];\n }\n\n const offset = document.offsetAt(params.position);\n const documentText = document.getText();\n const filePath = fileUriToPathOrNull(params.textDocument.uri);\n const semanticContext =\n options.usePluginTransport === false || filePath === null\n ? null\n : await getPluginCompletionContextForDocument(\n workspaceRoots,\n filePath,\n documentText,\n offset,\n options.pluginQueryTimeoutMs\n );\n\n return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);\n });\n\n connection.onHover(async (params) => {\n const document = documents.get(params.textDocument.uri);\n if (!document) {\n return null;\n }\n\n const offset = document.offsetAt(params.position);\n const documentText = document.getText();\n const filePath = fileUriToPathOrNull(params.textDocument.uri);\n const semanticHover =\n options.usePluginTransport === false || filePath === null\n ? null\n : await getPluginHoverForDocument(\n workspaceRoots,\n filePath,\n documentText,\n offset,\n options.pluginQueryTimeoutMs\n );\n\n return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);\n });\n\n connection.onDefinition((_params) => {\n // Go-to-definition is not yet implemented.\n return getDefinition();\n });\n\n return connection;\n}\n","/**\n * Completion provider for FormSpec JSDoc constraint tags.\n *\n * Uses the shared tag registry from `@formspec/analysis` so completions stay\n * aligned with the same metadata that powers linting and build-time analysis.\n */\n\nimport {\n type FormSpecSerializedCompletionContext,\n type FormSpecSerializedTagDefinition,\n getConstraintTagDefinitions,\n getSemanticCommentCompletionContextAtOffset,\n type TagDefinition,\n} from \"@formspec/analysis\";\nimport type { ExtensionDefinition } from \"@formspec/core\";\nimport { CompletionItem, CompletionItemKind } from \"vscode-languageserver/node.js\";\n\n/**\n * Returns the full set of tag-name completions currently known to FormSpec.\n *\n * @public\n */\nexport function getCompletionItems(extensions?: readonly ExtensionDefinition[]): CompletionItem[] {\n return getConstraintTagDefinitions(extensions).map((tag) => ({\n label: `@${tag.canonicalName}`,\n kind: CompletionItemKind.Keyword,\n detail: tag.completionDetail,\n }));\n}\n\nfunction toCompletionItem(tag: TagDefinition | FormSpecSerializedTagDefinition): CompletionItem {\n return {\n label: `@${tag.canonicalName}`,\n kind: CompletionItemKind.Keyword,\n detail: tag.completionDetail,\n };\n}\n\n/** @internal */\nexport function getCompletionItemsAtOffset(\n documentText: string,\n offset: number,\n extensions?: readonly ExtensionDefinition[],\n semanticContext?: FormSpecSerializedCompletionContext | null\n): CompletionItem[] {\n if (semanticContext !== null && semanticContext !== undefined) {\n if (semanticContext.kind === \"target\") {\n return semanticContext.semantic.targetCompletions.map((target: string) => ({\n label: target,\n kind:\n target === \"singular\" || target === \"plural\"\n ? CompletionItemKind.EnumMember\n : CompletionItemKind.Field,\n detail: `Target for @${semanticContext.semantic.tagName}`,\n }));\n }\n\n if (semanticContext.kind !== \"tag-name\") {\n return [];\n }\n\n const normalizedPrefix = semanticContext.prefix.toLowerCase();\n return semanticContext.availableTags\n .map(toCompletionItem)\n .filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));\n }\n\n const resolvedContext = getSemanticCommentCompletionContextAtOffset(\n documentText,\n offset,\n extensions ? { extensions } : undefined\n );\n\n if (resolvedContext.kind === \"target\") {\n return resolvedContext.semantic.targetCompletions.map((target: string) => ({\n label: target,\n kind:\n target === \"singular\" || target === \"plural\"\n ? CompletionItemKind.EnumMember\n : CompletionItemKind.Field,\n detail: `Target for @${resolvedContext.semantic.tag.normalizedTagName}`,\n }));\n }\n\n if (resolvedContext.kind !== \"tag-name\") {\n return [];\n }\n\n const normalizedPrefix = resolvedContext.prefix.toLowerCase();\n return resolvedContext.availableTags\n .map(toCompletionItem)\n .filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));\n}\n","/**\n * Hover provider for FormSpec JSDoc tags.\n *\n * Uses the shared registry from `@formspec/analysis` so hover content stays in\n * sync with the tag inventory and overload metadata.\n */\n\nimport {\n type FormSpecSerializedHoverInfo,\n getCommentHoverInfoAtOffset,\n getTagDefinition,\n normalizeFormSpecTagName,\n} from \"@formspec/analysis\";\nimport type { ExtensionDefinition } from \"@formspec/core\";\nimport type { Hover } from \"vscode-languageserver/node.js\";\n\n/**\n * Returns hover content for a single FormSpec tag name.\n *\n * @public\n */\nexport function getHoverForTag(\n tagName: string,\n extensions?: readonly ExtensionDefinition[]\n): Hover | null {\n const raw = tagName.startsWith(\"@\") ? tagName.slice(1) : tagName;\n const definition = getTagDefinition(normalizeFormSpecTagName(raw), extensions);\n if (!definition) {\n return null;\n }\n\n return {\n contents: {\n kind: \"markdown\",\n value: definition.hoverMarkdown,\n },\n };\n}\n\n/** @internal */\nexport function getHoverAtOffset(\n documentText: string,\n offset: number,\n extensions?: readonly ExtensionDefinition[],\n semanticHover?: FormSpecSerializedHoverInfo | null\n): Hover | null {\n const hoverInfo =\n semanticHover ??\n getCommentHoverInfoAtOffset(documentText, offset, extensions ? { extensions } : undefined);\n if (hoverInfo === null) {\n return null;\n }\n\n return {\n contents: {\n kind: \"markdown\",\n value: hoverInfo.markdown,\n },\n };\n}\n","/**\n * Go-to-definition provider for FormSpec.\n *\n * This is a stub — go-to-definition support (e.g., navigating from a\n * `field.text(\"name\")` call to the form definition that references it) will\n * be implemented in a future phase.\n */\n\nimport type { Location } from \"vscode-languageserver/node.js\";\n\n/**\n * Returns the definition location for a symbol at the given position.\n *\n * Always returns `null` in this stub implementation.\n *\n * @returns `null` — not yet implemented\n * @public\n */\nexport function getDefinition(): Location | null {\n return null;\n}\n","import fs from \"node:fs/promises\";\nimport net from \"node:net\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n computeFormSpecTextHash,\n getFormSpecManifestPath,\n isFormSpecAnalysisManifest,\n isFormSpecSemanticResponse,\n type FormSpecAnalysisManifest,\n type FormSpecSerializedCompletionContext,\n type FormSpecSerializedHoverInfo,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis\";\n\nconst DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2_000;\n\nfunction getManifestPath(workspaceRoot: string): string {\n return getFormSpecManifestPath(workspaceRoot);\n}\n\nfunction normalizeWorkspaceRoot(root: string): string {\n const resolved = path.resolve(root);\n const parsed = path.parse(resolved);\n let normalized = resolved;\n\n while (normalized.length > parsed.root.length && normalized.endsWith(path.sep)) {\n normalized = normalized.slice(0, -path.sep.length);\n }\n\n return normalized;\n}\n\nfunction getMatchingWorkspaceRoot(\n workspaceRoots: readonly string[],\n filePath: string\n): string | null {\n const normalizedFilePath = path.resolve(filePath);\n const normalizedRoots = [...workspaceRoots]\n .map(normalizeWorkspaceRoot)\n .sort((left, right) => right.length - left.length);\n return (\n normalizedRoots.find(\n (workspaceRoot) =>\n normalizedFilePath === workspaceRoot ||\n normalizedFilePath.startsWith(`${workspaceRoot}${path.sep}`)\n ) ?? null\n );\n}\n\nasync function readManifest(workspaceRoot: string): Promise<FormSpecAnalysisManifest | null> {\n try {\n const manifestText = await fs.readFile(getManifestPath(workspaceRoot), \"utf8\");\n const manifest = JSON.parse(manifestText) as unknown;\n if (!isFormSpecAnalysisManifest(manifest)) {\n return null;\n }\n\n return manifest;\n } catch {\n return null;\n }\n}\n\nasync function sendSemanticQuery(\n manifest: FormSpecAnalysisManifest,\n query: FormSpecSemanticQuery,\n timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS\n): Promise<FormSpecSemanticResponse | null> {\n return new Promise((resolve) => {\n const socket = net.createConnection(manifest.endpoint.address);\n let buffer = \"\";\n let settled = false;\n\n const finish = (response: FormSpecSemanticResponse | null): void => {\n if (settled) {\n return;\n }\n settled = true;\n socket.removeAllListeners(\"data\");\n socket.destroy();\n resolve(response);\n };\n\n socket.setTimeout(timeoutMs, () => {\n finish(null);\n });\n\n socket.setEncoding(\"utf8\");\n socket.on(\"connect\", () => {\n socket.write(`${JSON.stringify(query)}\\n`);\n });\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 buffer = buffer.slice(newlineIndex + 1);\n try {\n const response = JSON.parse(payload) as unknown;\n finish(isFormSpecSemanticResponse(response) ? response : null);\n } catch {\n finish(null);\n }\n });\n socket.on(\"error\", () => {\n finish(null);\n });\n socket.on(\"close\", () => {\n finish(null);\n });\n });\n}\n\nexport function fileUriToPathOrNull(uri: string): string | null {\n try {\n return fileURLToPath(uri);\n } catch {\n return null;\n }\n}\n\nasync function sendFileQuery(\n workspaceRoots: readonly string[],\n filePath: string,\n query: FormSpecSemanticQuery,\n timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS\n): Promise<FormSpecSemanticResponse | null> {\n const workspaceRoot = getMatchingWorkspaceRoot(workspaceRoots, filePath);\n if (workspaceRoot === null) {\n return null;\n }\n\n const manifest = await readManifest(workspaceRoot);\n if (manifest === null) {\n return null;\n }\n\n return sendSemanticQuery(manifest, query, timeoutMs);\n}\n\nexport async function getPluginCompletionContextForDocument(\n workspaceRoots: readonly string[],\n filePath: string,\n documentText: string,\n offset: number,\n timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS\n): Promise<FormSpecSerializedCompletionContext | null> {\n const response = await sendFileQuery(\n workspaceRoots,\n filePath,\n {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"completion\",\n filePath,\n offset,\n },\n timeoutMs\n );\n if (response?.kind !== \"completion\") {\n return null;\n }\n\n return response.sourceHash === computeFormSpecTextHash(documentText) ? response.context : null;\n}\n\nexport async function getPluginHoverForDocument(\n workspaceRoots: readonly string[],\n filePath: string,\n documentText: string,\n offset: number,\n timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS\n): Promise<FormSpecSerializedHoverInfo | null> {\n const response = await sendFileQuery(\n workspaceRoots,\n filePath,\n {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"hover\",\n filePath,\n offset,\n },\n timeoutMs\n );\n if (response?.kind !== \"hover\") {\n return null;\n }\n\n return response.sourceHash === computeFormSpecTextHash(documentText) ? response.hover : null;\n}\n"],"mappings":"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","names":["import_node","normalizedPrefix","import_analysis","import_analysis","path","fs","net"]}
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@
6
6
  * `@Maximum`, `@Pattern`, etc.) in TypeScript files.
7
7
  *
8
8
  * This package implements the Language Server Protocol (LSP) using the
9
- * `vscode-languageserver` library. Constraint names are sourced from
10
- * `BUILTIN_CONSTRAINT_DEFINITIONS` in `@formspec/core`, ensuring the
11
- * language server stays in sync with the single source of truth.
9
+ * `vscode-languageserver` library. Cheap syntax-local behaviors stay in the
10
+ * LSP process, while TypeScript-project-aware semantics are supplied by
11
+ * `@formspec/ts-plugin` over a local manifest + IPC transport.
12
12
  *
13
13
  * Diagnostics are intentionally omitted per design decision A7.
14
14
  *
package/dist/index.js CHANGED
@@ -2,253 +2,93 @@
2
2
  import {
3
3
  createConnection,
4
4
  ProposedFeatures,
5
+ TextDocuments,
5
6
  TextDocumentSyncKind
6
7
  } from "vscode-languageserver/node.js";
8
+ import { TextDocument } from "vscode-languageserver-textdocument";
7
9
 
8
10
  // src/providers/completion.ts
9
11
  import {
10
- BUILTIN_CONSTRAINT_DEFINITIONS
11
- } from "@formspec/core";
12
+ getConstraintTagDefinitions,
13
+ getSemanticCommentCompletionContextAtOffset
14
+ } from "@formspec/analysis";
12
15
  import { CompletionItemKind } from "vscode-languageserver/node.js";
13
- var CONSTRAINT_DETAIL = {
14
- minimum: "Minimum numeric value (inclusive). Example: `@minimum 0`",
15
- maximum: "Maximum numeric value (inclusive). Example: `@maximum 100`",
16
- exclusiveMinimum: "Minimum numeric value (exclusive). Example: `@exclusiveMinimum 0`",
17
- exclusiveMaximum: "Maximum numeric value (exclusive). Example: `@exclusiveMaximum 100`",
18
- multipleOf: "Value must be a multiple of this number. Example: `@multipleOf 0.01`",
19
- minLength: "Minimum string length. Example: `@minLength 1`",
20
- maxLength: "Maximum string length. Example: `@maxLength 255`",
21
- minItems: "Minimum number of array items. Example: `@minItems 1`",
22
- maxItems: "Maximum number of array items. Example: `@maxItems 10`",
23
- uniqueItems: "Require all array items to be distinct. Example: `@uniqueItems`",
24
- pattern: "Regular expression pattern for string validation. Example: `@pattern ^[a-z]+$`",
25
- enumOptions: 'Inline JSON array of allowed enum values. Example: `@enumOptions ["a","b","c"]`',
26
- const: 'Require a constant JSON value. Example: `@const "USD"`'
27
- };
28
16
  function getCompletionItems(extensions) {
29
- const builtins = Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS).map(
30
- (name) => ({
31
- label: `@${name}`,
32
- kind: CompletionItemKind.Keyword,
33
- detail: CONSTRAINT_DETAIL[name]
34
- })
17
+ return getConstraintTagDefinitions(extensions).map((tag) => ({
18
+ label: `@${tag.canonicalName}`,
19
+ kind: CompletionItemKind.Keyword,
20
+ detail: tag.completionDetail
21
+ }));
22
+ }
23
+ function toCompletionItem(tag) {
24
+ return {
25
+ label: `@${tag.canonicalName}`,
26
+ kind: CompletionItemKind.Keyword,
27
+ detail: tag.completionDetail
28
+ };
29
+ }
30
+ function getCompletionItemsAtOffset(documentText, offset, extensions, semanticContext) {
31
+ if (semanticContext !== null && semanticContext !== void 0) {
32
+ if (semanticContext.kind === "target") {
33
+ return semanticContext.semantic.targetCompletions.map((target) => ({
34
+ label: target,
35
+ kind: target === "singular" || target === "plural" ? CompletionItemKind.EnumMember : CompletionItemKind.Field,
36
+ detail: `Target for @${semanticContext.semantic.tagName}`
37
+ }));
38
+ }
39
+ if (semanticContext.kind !== "tag-name") {
40
+ return [];
41
+ }
42
+ const normalizedPrefix2 = semanticContext.prefix.toLowerCase();
43
+ return semanticContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix2));
44
+ }
45
+ const resolvedContext = getSemanticCommentCompletionContextAtOffset(
46
+ documentText,
47
+ offset,
48
+ extensions ? { extensions } : void 0
35
49
  );
36
- const customItems = extensions?.flatMap(
37
- (extension) => (extension.constraintTags ?? []).map((tag) => ({
38
- label: `@${tag.tagName}`,
39
- kind: CompletionItemKind.Keyword,
40
- detail: `Extension constraint tag from ${extension.extensionId}`
41
- }))
42
- ) ?? [];
43
- return [...builtins, ...customItems];
50
+ if (resolvedContext.kind === "target") {
51
+ return resolvedContext.semantic.targetCompletions.map((target) => ({
52
+ label: target,
53
+ kind: target === "singular" || target === "plural" ? CompletionItemKind.EnumMember : CompletionItemKind.Field,
54
+ detail: `Target for @${resolvedContext.semantic.tag.normalizedTagName}`
55
+ }));
56
+ }
57
+ if (resolvedContext.kind !== "tag-name") {
58
+ return [];
59
+ }
60
+ const normalizedPrefix = resolvedContext.prefix.toLowerCase();
61
+ return resolvedContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
44
62
  }
45
63
 
46
64
  // src/providers/hover.ts
47
65
  import {
48
- normalizeConstraintTagName,
49
- isBuiltinConstraintName
50
- } from "@formspec/core";
51
- var CONSTRAINT_HOVER_DOCS = {
52
- minimum: [
53
- "**@minimum** `<number>`",
54
- "",
55
- "Sets an inclusive lower bound on a numeric field.",
56
- "",
57
- "Maps to `minimum` in JSON Schema.",
58
- "",
59
- "**Example:**",
60
- "```typescript",
61
- "/** @minimum 0 */",
62
- "amount: number;",
63
- "```"
64
- ].join("\n"),
65
- maximum: [
66
- "**@maximum** `<number>`",
67
- "",
68
- "Sets an inclusive upper bound on a numeric field.",
69
- "",
70
- "Maps to `maximum` in JSON Schema.",
71
- "",
72
- "**Example:**",
73
- "```typescript",
74
- "/** @maximum 100 */",
75
- "percentage: number;",
76
- "```"
77
- ].join("\n"),
78
- exclusiveMinimum: [
79
- "**@exclusiveMinimum** `<number>`",
80
- "",
81
- "Sets an exclusive lower bound on a numeric field.",
82
- "",
83
- "Maps to `exclusiveMinimum` in JSON Schema.",
84
- "",
85
- "**Example:**",
86
- "```typescript",
87
- "/** @exclusiveMinimum 0 */",
88
- "positiveAmount: number;",
89
- "```"
90
- ].join("\n"),
91
- exclusiveMaximum: [
92
- "**@exclusiveMaximum** `<number>`",
93
- "",
94
- "Sets an exclusive upper bound on a numeric field.",
95
- "",
96
- "Maps to `exclusiveMaximum` in JSON Schema.",
97
- "",
98
- "**Example:**",
99
- "```typescript",
100
- "/** @exclusiveMaximum 1 */",
101
- "ratio: number;",
102
- "```"
103
- ].join("\n"),
104
- multipleOf: [
105
- "**@multipleOf** `<number>`",
106
- "",
107
- "Requires the numeric value to be a multiple of the given number.",
108
- "",
109
- "Maps to `multipleOf` in JSON Schema.",
110
- "",
111
- "**Example:**",
112
- "```typescript",
113
- "/** @multipleOf 0.01 */",
114
- "price: number;",
115
- "```"
116
- ].join("\n"),
117
- minLength: [
118
- "**@minLength** `<number>`",
119
- "",
120
- "Sets a minimum character length on a string field.",
121
- "",
122
- "Maps to `minLength` in JSON Schema.",
123
- "",
124
- "**Example:**",
125
- "```typescript",
126
- "/** @minLength 1 */",
127
- "name: string;",
128
- "```"
129
- ].join("\n"),
130
- maxLength: [
131
- "**@maxLength** `<number>`",
132
- "",
133
- "Sets a maximum character length on a string field.",
134
- "",
135
- "Maps to `maxLength` in JSON Schema.",
136
- "",
137
- "**Example:**",
138
- "```typescript",
139
- "/** @maxLength 255 */",
140
- "description: string;",
141
- "```"
142
- ].join("\n"),
143
- minItems: [
144
- "**@minItems** `<number>`",
145
- "",
146
- "Sets a minimum number of items in an array field.",
147
- "",
148
- "Maps to `minItems` in JSON Schema.",
149
- "",
150
- "**Example:**",
151
- "```typescript",
152
- "/** @minItems 1 */",
153
- "tags: string[];",
154
- "```"
155
- ].join("\n"),
156
- maxItems: [
157
- "**@maxItems** `<number>`",
158
- "",
159
- "Sets a maximum number of items in an array field.",
160
- "",
161
- "Maps to `maxItems` in JSON Schema.",
162
- "",
163
- "**Example:**",
164
- "```typescript",
165
- "/** @maxItems 10 */",
166
- "tags: string[];",
167
- "```"
168
- ].join("\n"),
169
- uniqueItems: [
170
- "**@uniqueItems**",
171
- "",
172
- "Requires all items in an array field to be distinct.",
173
- "",
174
- "Maps to `uniqueItems` in JSON Schema.",
175
- "",
176
- "**Example:**",
177
- "```typescript",
178
- "/** @uniqueItems */",
179
- "tags: string[];",
180
- "```"
181
- ].join("\n"),
182
- pattern: [
183
- "**@pattern** `<regex>`",
184
- "",
185
- "Sets a regular expression pattern that a string field must match.",
186
- "",
187
- "Maps to `pattern` in JSON Schema.",
188
- "",
189
- "**Example:**",
190
- "```typescript",
191
- "/** @pattern ^[a-z0-9]+$ */",
192
- "slug: string;",
193
- "```"
194
- ].join("\n"),
195
- enumOptions: [
196
- "**@enumOptions** `<json-array>`",
197
- "",
198
- "Specifies the allowed values for an enum field as an inline JSON array.",
199
- "",
200
- "Maps to `enum` in JSON Schema.",
201
- "",
202
- "**Example:**",
203
- "```typescript",
204
- '/** @enumOptions ["draft","sent","archived"] */',
205
- "status: string;",
206
- "```"
207
- ].join("\n"),
208
- const: [
209
- "**@const** `<json-literal>`",
210
- "",
211
- "Requires the field value to equal a single constant JSON value.",
212
- "",
213
- "Maps to `const` in JSON Schema.",
214
- "",
215
- "**Example:**",
216
- "```typescript",
217
- '/** @const "USD" */',
218
- "currency: string;",
219
- "```"
220
- ].join("\n")
221
- };
66
+ getCommentHoverInfoAtOffset,
67
+ getTagDefinition,
68
+ normalizeFormSpecTagName
69
+ } from "@formspec/analysis";
222
70
  function getHoverForTag(tagName, extensions) {
223
71
  const raw = tagName.startsWith("@") ? tagName.slice(1) : tagName;
224
- const name = normalizeConstraintTagName(raw);
225
- if (!isBuiltinConstraintName(name)) {
226
- const registration = extensions?.flatMap(
227
- (extension) => (extension.constraintTags ?? []).map((tag) => ({
228
- extensionId: extension.extensionId,
229
- tag
230
- }))
231
- ).find(({ tag }) => tag.tagName === name);
232
- if (registration === void 0) {
233
- return null;
72
+ const definition = getTagDefinition(normalizeFormSpecTagName(raw), extensions);
73
+ if (!definition) {
74
+ return null;
75
+ }
76
+ return {
77
+ contents: {
78
+ kind: "markdown",
79
+ value: definition.hoverMarkdown
234
80
  }
235
- return {
236
- contents: {
237
- kind: "markdown",
238
- value: [
239
- `**@${registration.tag.tagName}** \`<value>\``,
240
- "",
241
- `Extension-defined constraint tag from \`${registration.extensionId}\`.`,
242
- "",
243
- "Validated through the registered FormSpec extension surface."
244
- ].join("\n")
245
- }
246
- };
81
+ };
82
+ }
83
+ function getHoverAtOffset(documentText, offset, extensions, semanticHover) {
84
+ const hoverInfo = semanticHover ?? getCommentHoverInfoAtOffset(documentText, offset, extensions ? { extensions } : void 0);
85
+ if (hoverInfo === null) {
86
+ return null;
247
87
  }
248
88
  return {
249
89
  contents: {
250
90
  kind: "markdown",
251
- value: CONSTRAINT_HOVER_DOCS[name]
91
+ value: hoverInfo.markdown
252
92
  }
253
93
  };
254
94
  }
@@ -258,16 +98,178 @@ function getDefinition() {
258
98
  return null;
259
99
  }
260
100
 
101
+ // src/plugin-client.ts
102
+ import fs from "fs/promises";
103
+ import net from "net";
104
+ import path from "path";
105
+ import { fileURLToPath } from "url";
106
+ import {
107
+ FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
108
+ computeFormSpecTextHash,
109
+ getFormSpecManifestPath,
110
+ isFormSpecAnalysisManifest,
111
+ isFormSpecSemanticResponse
112
+ } from "@formspec/analysis";
113
+ var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
114
+ function getManifestPath(workspaceRoot) {
115
+ return getFormSpecManifestPath(workspaceRoot);
116
+ }
117
+ function normalizeWorkspaceRoot(root) {
118
+ const resolved = path.resolve(root);
119
+ const parsed = path.parse(resolved);
120
+ let normalized = resolved;
121
+ while (normalized.length > parsed.root.length && normalized.endsWith(path.sep)) {
122
+ normalized = normalized.slice(0, -path.sep.length);
123
+ }
124
+ return normalized;
125
+ }
126
+ function getMatchingWorkspaceRoot(workspaceRoots, filePath) {
127
+ const normalizedFilePath = path.resolve(filePath);
128
+ const normalizedRoots = [...workspaceRoots].map(normalizeWorkspaceRoot).sort((left, right) => right.length - left.length);
129
+ return normalizedRoots.find(
130
+ (workspaceRoot) => normalizedFilePath === workspaceRoot || normalizedFilePath.startsWith(`${workspaceRoot}${path.sep}`)
131
+ ) ?? null;
132
+ }
133
+ async function readManifest(workspaceRoot) {
134
+ try {
135
+ const manifestText = await fs.readFile(getManifestPath(workspaceRoot), "utf8");
136
+ const manifest = JSON.parse(manifestText);
137
+ if (!isFormSpecAnalysisManifest(manifest)) {
138
+ return null;
139
+ }
140
+ return manifest;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ async function sendSemanticQuery(manifest, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
146
+ return new Promise((resolve) => {
147
+ const socket = net.createConnection(manifest.endpoint.address);
148
+ let buffer = "";
149
+ let settled = false;
150
+ const finish = (response) => {
151
+ if (settled) {
152
+ return;
153
+ }
154
+ settled = true;
155
+ socket.removeAllListeners("data");
156
+ socket.destroy();
157
+ resolve(response);
158
+ };
159
+ socket.setTimeout(timeoutMs, () => {
160
+ finish(null);
161
+ });
162
+ socket.setEncoding("utf8");
163
+ socket.on("connect", () => {
164
+ socket.write(`${JSON.stringify(query)}
165
+ `);
166
+ });
167
+ socket.on("data", (chunk) => {
168
+ buffer += String(chunk);
169
+ const newlineIndex = buffer.indexOf("\n");
170
+ if (newlineIndex < 0) {
171
+ return;
172
+ }
173
+ const payload = buffer.slice(0, newlineIndex);
174
+ buffer = buffer.slice(newlineIndex + 1);
175
+ try {
176
+ const response = JSON.parse(payload);
177
+ finish(isFormSpecSemanticResponse(response) ? response : null);
178
+ } catch {
179
+ finish(null);
180
+ }
181
+ });
182
+ socket.on("error", () => {
183
+ finish(null);
184
+ });
185
+ socket.on("close", () => {
186
+ finish(null);
187
+ });
188
+ });
189
+ }
190
+ function fileUriToPathOrNull(uri) {
191
+ try {
192
+ return fileURLToPath(uri);
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+ async function sendFileQuery(workspaceRoots, filePath, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
198
+ const workspaceRoot = getMatchingWorkspaceRoot(workspaceRoots, filePath);
199
+ if (workspaceRoot === null) {
200
+ return null;
201
+ }
202
+ const manifest = await readManifest(workspaceRoot);
203
+ if (manifest === null) {
204
+ return null;
205
+ }
206
+ return sendSemanticQuery(manifest, query, timeoutMs);
207
+ }
208
+ async function getPluginCompletionContextForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
209
+ const response = await sendFileQuery(
210
+ workspaceRoots,
211
+ filePath,
212
+ {
213
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
214
+ kind: "completion",
215
+ filePath,
216
+ offset
217
+ },
218
+ timeoutMs
219
+ );
220
+ if (response?.kind !== "completion") {
221
+ return null;
222
+ }
223
+ return response.sourceHash === computeFormSpecTextHash(documentText) ? response.context : null;
224
+ }
225
+ async function getPluginHoverForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
226
+ const response = await sendFileQuery(
227
+ workspaceRoots,
228
+ filePath,
229
+ {
230
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
231
+ kind: "hover",
232
+ filePath,
233
+ offset
234
+ },
235
+ timeoutMs
236
+ );
237
+ if (response?.kind !== "hover") {
238
+ return null;
239
+ }
240
+ return response.sourceHash === computeFormSpecTextHash(documentText) ? response.hover : null;
241
+ }
242
+
261
243
  // src/server.ts
244
+ function dedupeWorkspaceRoots(workspaceRoots) {
245
+ return [...new Set(workspaceRoots)];
246
+ }
247
+ function getWorkspaceRootsFromInitializeParams(params) {
248
+ const workspaceFolders = params.workspaceFolders?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri)).filter((workspaceRoot) => workspaceRoot !== null) ?? [];
249
+ const rootUri = params.rootUri === null || params.rootUri === void 0 ? null : fileUriToPathOrNull(params.rootUri);
250
+ const rootPath = params.rootPath ?? null;
251
+ return dedupeWorkspaceRoots([
252
+ ...workspaceFolders,
253
+ ...rootUri === null ? [] : [rootUri],
254
+ ...rootPath === null ? [] : [rootPath]
255
+ ]);
256
+ }
262
257
  function createServer(options = {}) {
263
258
  const connection = createConnection(ProposedFeatures.all);
264
- connection.onInitialize(() => {
259
+ const documents = new TextDocuments(TextDocument);
260
+ let workspaceRoots = [...options.workspaceRoots ?? []];
261
+ documents.listen(connection);
262
+ connection.onInitialize((params) => {
263
+ workspaceRoots = dedupeWorkspaceRoots([
264
+ ...getWorkspaceRootsFromInitializeParams(params),
265
+ ...workspaceRoots
266
+ ]);
265
267
  return {
266
268
  capabilities: {
267
269
  textDocumentSync: TextDocumentSyncKind.Incremental,
268
270
  completionProvider: {
269
- // Trigger completions inside JSDoc comments when `@` is typed
270
- triggerCharacters: ["@"]
271
+ // Trigger completions inside JSDoc comments for tags and target specifiers
272
+ triggerCharacters: ["@", ":"]
271
273
  },
272
274
  hoverProvider: true,
273
275
  definitionProvider: true
@@ -278,11 +280,39 @@ function createServer(options = {}) {
278
280
  }
279
281
  };
280
282
  });
281
- connection.onCompletion(() => {
282
- return getCompletionItems(options.extensions);
283
+ connection.onCompletion(async (params) => {
284
+ const document = documents.get(params.textDocument.uri);
285
+ if (!document) {
286
+ return [];
287
+ }
288
+ const offset = document.offsetAt(params.position);
289
+ const documentText = document.getText();
290
+ const filePath = fileUriToPathOrNull(params.textDocument.uri);
291
+ const semanticContext = options.usePluginTransport === false || filePath === null ? null : await getPluginCompletionContextForDocument(
292
+ workspaceRoots,
293
+ filePath,
294
+ documentText,
295
+ offset,
296
+ options.pluginQueryTimeoutMs
297
+ );
298
+ return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);
283
299
  });
284
- connection.onHover((_params) => {
285
- return getHoverForTag("", options.extensions);
300
+ connection.onHover(async (params) => {
301
+ const document = documents.get(params.textDocument.uri);
302
+ if (!document) {
303
+ return null;
304
+ }
305
+ const offset = document.offsetAt(params.position);
306
+ const documentText = document.getText();
307
+ const filePath = fileUriToPathOrNull(params.textDocument.uri);
308
+ const semanticHover = options.usePluginTransport === false || filePath === null ? null : await getPluginHoverForDocument(
309
+ workspaceRoots,
310
+ filePath,
311
+ documentText,
312
+ offset,
313
+ options.pluginQueryTimeoutMs
314
+ );
315
+ return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);
286
316
  });
287
317
  connection.onDefinition((_params) => {
288
318
  return getDefinition();