@formspec/language-server 0.1.0-alpha.21 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Language-server support for FormSpec TSDoc tags.
4
4
 
5
+ The packaged server is a reference implementation built on top of the
6
+ composable completion, hover, and diagnostics helpers exported by this package.
7
+ Downstream tools can reuse those helpers directly and own the final publishing
8
+ and presentation behavior themselves.
9
+
5
10
  ## Install
6
11
 
7
12
  ```bash
@@ -13,19 +18,45 @@ pnpm add @formspec/language-server
13
18
  - completion items for FormSpec tags
14
19
  - hover documentation for recognized tags
15
20
  - go-to-definition support for known tags
21
+ - optional plugin-backed diagnostics publishing
16
22
 
17
- Diagnostics are intentionally handled elsewhere; this package focuses on editor assistance.
23
+ Diagnostics are off by default. When enabled, the packaged server consumes
24
+ canonical FormSpec diagnostics from `@formspec/ts-plugin` and converts them to
25
+ LSP diagnostics using the same exported helpers that downstream consumers can
26
+ call directly.
18
27
 
19
28
  ## Usage
20
29
 
21
30
  ```ts
22
- import { createServer, getCompletionItems, getHoverForTag } from "@formspec/language-server";
31
+ import {
32
+ createServer,
33
+ getCompletionItems,
34
+ getHoverForTag,
35
+ getPluginDiagnosticsForDocument,
36
+ toLspDiagnostics,
37
+ } from "@formspec/language-server";
23
38
 
24
39
  const server = createServer();
25
40
  const completions = getCompletionItems();
26
41
  const hover = getHoverForTag("minimum");
27
42
  ```
28
43
 
44
+ To enable packaged diagnostics publishing:
45
+
46
+ ```ts
47
+ const server = createServer({
48
+ diagnosticsMode: "plugin",
49
+ diagnosticSource: "formspec",
50
+ });
51
+ ```
52
+
53
+ For full white-label control, bypass `createServer()` and use:
54
+
55
+ - `getPluginDiagnosticsForDocument(...)`
56
+ - `toLspDiagnostics(...)`
57
+
58
+ or map canonical FormSpec diagnostics to your own editor/UI model directly.
59
+
29
60
  ## License
30
61
 
31
62
  UNLICENSED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=diagnostics.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/diagnostics.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import type { FormSpecAnalysisDiagnostic } from "@formspec/analysis/protocol";
2
+ import { type Diagnostic } from "vscode-languageserver/node.js";
3
+ import type { TextDocument } from "vscode-languageserver-textdocument";
4
+ export { getPluginDiagnosticsForDocument } from "./plugin-client.js";
5
+ /**
6
+ * Options for converting canonical FormSpec diagnostics into LSP diagnostics.
7
+ *
8
+ * @public
9
+ */
10
+ export interface ToLspDiagnosticsOptions {
11
+ /** Source label shown by LSP clients. Defaults to `formspec`. */
12
+ readonly source?: string;
13
+ }
14
+ /**
15
+ * Converts canonical FormSpec diagnostics into LSP diagnostics.
16
+ *
17
+ * Downstream consumers that want complete white-label control can ignore this
18
+ * helper and render their own messages from `code` + `data`.
19
+ *
20
+ * @public
21
+ */
22
+ export declare function toLspDiagnostics(document: TextDocument, diagnostics: readonly FormSpecAnalysisDiagnostic[], options?: ToLspDiagnosticsOptions): Diagnostic[];
23
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAEvE,OAAO,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,YAAY,EACtB,WAAW,EAAE,SAAS,0BAA0B,EAAE,EAClD,OAAO,GAAE,uBAA4B,GACpC,UAAU,EAAE,CAiBd"}
package/dist/index.cjs CHANGED
@@ -33,12 +33,14 @@ __export(index_exports, {
33
33
  createServer: () => createServer,
34
34
  getCompletionItems: () => getCompletionItems,
35
35
  getDefinition: () => getDefinition,
36
- getHoverForTag: () => getHoverForTag
36
+ getHoverForTag: () => getHoverForTag,
37
+ getPluginDiagnosticsForDocument: () => getPluginDiagnosticsForDocument,
38
+ toLspDiagnostics: () => toLspDiagnostics
37
39
  });
38
40
  module.exports = __toCommonJS(index_exports);
39
41
 
40
42
  // src/server.ts
41
- var import_node2 = require("vscode-languageserver/node.js");
43
+ var import_node3 = require("vscode-languageserver/node.js");
42
44
  var import_vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
43
45
 
44
46
  // src/providers/completion.ts
@@ -132,15 +134,19 @@ function getDefinition() {
132
134
  return null;
133
135
  }
134
136
 
137
+ // src/diagnostics.ts
138
+ var import_node2 = require("vscode-languageserver/node.js");
139
+
135
140
  // src/plugin-client.ts
136
141
  var import_promises = __toESM(require("fs/promises"), 1);
137
142
  var import_node_net = __toESM(require("net"), 1);
138
143
  var import_node_path = __toESM(require("path"), 1);
139
144
  var import_node_url = require("url");
140
145
  var import_protocol = require("@formspec/analysis/protocol");
146
+ var import_internal3 = require("@formspec/analysis/internal");
141
147
  var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
142
148
  function getManifestPath(workspaceRoot) {
143
- return (0, import_protocol.getFormSpecManifestPath)(workspaceRoot);
149
+ return (0, import_internal3.getFormSpecManifestPath)(workspaceRoot);
144
150
  }
145
151
  function normalizeWorkspaceRoot(root) {
146
152
  const resolved = import_node_path.default.resolve(root);
@@ -267,6 +273,76 @@ async function getPluginHoverForDocument(workspaceRoots, filePath, documentText,
267
273
  }
268
274
  return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.hover : null;
269
275
  }
276
+ async function getPluginDiagnosticsForDocument(workspaceRoots, filePath, documentText, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
277
+ const response = await sendFileQuery(
278
+ workspaceRoots,
279
+ filePath,
280
+ {
281
+ protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
282
+ kind: "diagnostics",
283
+ filePath
284
+ },
285
+ timeoutMs
286
+ );
287
+ if (response?.kind !== "diagnostics") {
288
+ return null;
289
+ }
290
+ return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.diagnostics : null;
291
+ }
292
+
293
+ // src/diagnostics.ts
294
+ function toLspDiagnostics(document, diagnostics, options = {}) {
295
+ const source = options.source ?? "formspec";
296
+ return diagnostics.map((diagnostic) => {
297
+ const relatedInformation = toRelatedInformation(document, diagnostic.relatedLocations);
298
+ return {
299
+ range: spanToRange(document, diagnostic.range.start, diagnostic.range.end),
300
+ severity: toLspSeverity(diagnostic.severity),
301
+ source,
302
+ code: diagnostic.code,
303
+ message: diagnostic.message,
304
+ ...relatedInformation === void 0 ? {} : { relatedInformation },
305
+ data: {
306
+ ...diagnostic.data,
307
+ category: diagnostic.category
308
+ }
309
+ };
310
+ });
311
+ }
312
+ function spanToRange(document, start, end) {
313
+ return import_node2.Range.create(document.positionAt(start), document.positionAt(end));
314
+ }
315
+ function toLspSeverity(severity) {
316
+ switch (severity) {
317
+ case "error":
318
+ return import_node2.DiagnosticSeverity.Error;
319
+ case "warning":
320
+ return import_node2.DiagnosticSeverity.Warning;
321
+ case "info":
322
+ return import_node2.DiagnosticSeverity.Information;
323
+ default:
324
+ return import_node2.DiagnosticSeverity.Information;
325
+ }
326
+ }
327
+ function toRelatedInformation(document, locations) {
328
+ if (locations.length === 0) {
329
+ return void 0;
330
+ }
331
+ const currentDocumentFilePath = getDocumentFilePath(document);
332
+ const relatedInformation = locations.filter((location) => location.filePath === currentDocumentFilePath).map(
333
+ (location) => import_node2.DiagnosticRelatedInformation.create(
334
+ import_node2.Location.create(
335
+ document.uri,
336
+ spanToRange(document, location.range.start, location.range.end)
337
+ ),
338
+ location.message ?? "Related FormSpec location"
339
+ )
340
+ );
341
+ return relatedInformation.length === 0 ? void 0 : relatedInformation;
342
+ }
343
+ function getDocumentFilePath(document) {
344
+ return fileUriToPathOrNull(document.uri);
345
+ }
270
346
 
271
347
  // src/server.ts
272
348
  var PLUGIN_QUERY_TIMEOUT_ENV_VAR = "FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS";
@@ -295,11 +371,34 @@ function getWorkspaceRootsFromInitializeParams(params) {
295
371
  ]);
296
372
  }
297
373
  function createServer(options = {}) {
298
- const connection = (0, import_node2.createConnection)(import_node2.ProposedFeatures.all);
299
- const documents = new import_node2.TextDocuments(import_vscode_languageserver_textdocument.TextDocument);
374
+ const connection = (0, import_node3.createConnection)(import_node3.ProposedFeatures.all);
375
+ const documents = new import_node3.TextDocuments(import_vscode_languageserver_textdocument.TextDocument);
300
376
  let workspaceRoots = [...options.workspaceRoots ?? []];
301
377
  const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);
378
+ const diagnosticsMode = options.diagnosticsMode ?? "off";
379
+ const diagnosticSource = options.diagnosticSource ?? "formspec";
302
380
  documents.listen(connection);
381
+ async function publishDiagnosticsForDocument(document) {
382
+ if (diagnosticsMode !== "plugin" || options.usePluginTransport === false) {
383
+ return;
384
+ }
385
+ const filePath = fileUriToPathOrNull(document.uri);
386
+ if (filePath === null) {
387
+ return;
388
+ }
389
+ const diagnostics = await getPluginDiagnosticsForDocument(
390
+ workspaceRoots,
391
+ filePath,
392
+ document.getText(),
393
+ pluginQueryTimeoutMs
394
+ ) ?? [];
395
+ void connection.sendDiagnostics({
396
+ uri: document.uri,
397
+ diagnostics: toLspDiagnostics(document, diagnostics, {
398
+ source: diagnosticSource
399
+ })
400
+ });
401
+ }
303
402
  connection.onInitialize((params) => {
304
403
  workspaceRoots = dedupeWorkspaceRoots([
305
404
  ...getWorkspaceRootsFromInitializeParams(params),
@@ -307,7 +406,7 @@ function createServer(options = {}) {
307
406
  ]);
308
407
  return {
309
408
  capabilities: {
310
- textDocumentSync: import_node2.TextDocumentSyncKind.Incremental,
409
+ textDocumentSync: import_node3.TextDocumentSyncKind.Incremental,
311
410
  completionProvider: {
312
411
  // Trigger completions inside JSDoc comments for tags and target specifiers
313
412
  triggerCharacters: ["@", ":"]
@@ -358,6 +457,24 @@ function createServer(options = {}) {
358
457
  connection.onDefinition((_params) => {
359
458
  return getDefinition();
360
459
  });
460
+ documents.onDidOpen(({ document }) => {
461
+ void publishDiagnosticsForDocument(document).catch((error) => {
462
+ connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
463
+ });
464
+ });
465
+ documents.onDidChangeContent(({ document }) => {
466
+ void publishDiagnosticsForDocument(document).catch((error) => {
467
+ connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
468
+ });
469
+ });
470
+ documents.onDidClose(({ document }) => {
471
+ if (diagnosticsMode === "plugin") {
472
+ void connection.sendDiagnostics({
473
+ uri: document.uri,
474
+ diagnostics: []
475
+ });
476
+ }
477
+ });
361
478
  return connection;
362
479
  }
363
480
  // Annotate the CommonJS export names for ESM import in node:
@@ -365,6 +482,8 @@ function createServer(options = {}) {
365
482
  createServer,
366
483
  getCompletionItems,
367
484
  getDefinition,
368
- getHoverForTag
485
+ getHoverForTag,
486
+ getPluginDiagnosticsForDocument,
487
+ toLspDiagnostics
369
488
  });
370
489
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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\nconst PLUGIN_QUERY_TIMEOUT_ENV_VAR = \"FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS\";\n\nfunction dedupeWorkspaceRoots(workspaceRoots: readonly string[]): string[] {\n return [...new Set(workspaceRoots)];\n}\n\nfunction resolvePluginQueryTimeoutMs(explicitTimeoutMs: number | undefined): number | undefined {\n if (explicitTimeoutMs !== undefined) {\n return explicitTimeoutMs;\n }\n\n const rawValue = process.env[PLUGIN_QUERY_TIMEOUT_ENV_VAR];\n if (rawValue === undefined) {\n return undefined;\n }\n\n const parsed = Number.parseInt(rawValue, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\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 const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);\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 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 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/internal\";\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\nfunction toTargetCompletionItems(\n tagName: string,\n targetCompletions: readonly string[]\n): CompletionItem[] {\n return targetCompletions.map((target: string) => ({\n label: target,\n kind:\n target === \"singular\" || target === \"plural\"\n ? CompletionItemKind.EnumMember\n : CompletionItemKind.Field,\n detail: `Target for @${tagName}`,\n }));\n}\n\nfunction filterTagNameCompletionItems(\n prefix: string,\n availableTags: readonly (TagDefinition | FormSpecSerializedTagDefinition)[]\n): CompletionItem[] {\n const normalizedPrefix = prefix.toLowerCase();\n return availableTags\n .map(toCompletionItem)\n .filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));\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 toTargetCompletionItems(\n semanticContext.semantic.tagName,\n semanticContext.semantic.targetCompletions\n );\n }\n\n if (semanticContext.kind !== \"tag-name\") {\n return [];\n }\n\n return filterTagNameCompletionItems(semanticContext.prefix, semanticContext.availableTags);\n }\n\n const resolvedContext = getSemanticCommentCompletionContextAtOffset(\n documentText,\n offset,\n extensions ? { extensions } : undefined\n );\n\n if (resolvedContext.kind === \"target\") {\n return toTargetCompletionItems(\n resolvedContext.semantic.tag.normalizedTagName,\n resolvedContext.semantic.targetCompletions\n );\n }\n\n if (resolvedContext.kind !== \"tag-name\") {\n return [];\n }\n\n return filterTagNameCompletionItems(resolvedContext.prefix, resolvedContext.availableTags);\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/internal\";\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/protocol\";\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","import_internal","path","fs","net"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/providers/completion.ts","../src/providers/hover.ts","../src/providers/definition.ts","../src/diagnostics.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 * The packaged server acts as a reference implementation over the composable\n * completion, hover, and diagnostics helpers exported from this package.\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 type {\n CommentSpan,\n FormSpecAnalysisDiagnostic,\n FormSpecAnalysisDiagnosticCategory,\n FormSpecAnalysisDiagnosticDataValue,\n FormSpecAnalysisDiagnosticLocation,\n} from \"@formspec/analysis/protocol\";\nexport {\n getPluginDiagnosticsForDocument,\n toLspDiagnostics,\n type ToLspDiagnosticsOptions,\n} from \"./diagnostics.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 * The packaged language server is a reference implementation built on the same\n * composable helpers that downstream consumers can call directly.\n */\n\nimport {\n createConnection,\n Diagnostic,\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 { getPluginDiagnosticsForDocument, toLspDiagnostics } from \"./diagnostics.js\";\nimport {\n fileUriToPathOrNull,\n getPluginCompletionContextForDocument,\n getPluginHoverForDocument,\n} from \"./plugin-client.js\";\n\nconst PLUGIN_QUERY_TIMEOUT_ENV_VAR = \"FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS\";\n\nfunction dedupeWorkspaceRoots(workspaceRoots: readonly string[]): string[] {\n return [...new Set(workspaceRoots)];\n}\n\nfunction resolvePluginQueryTimeoutMs(explicitTimeoutMs: number | undefined): number | undefined {\n if (explicitTimeoutMs !== undefined) {\n return explicitTimeoutMs;\n }\n\n const rawValue = process.env[PLUGIN_QUERY_TIMEOUT_ENV_VAR];\n if (rawValue === undefined) {\n return undefined;\n }\n\n const parsed = Number.parseInt(rawValue, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\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 /** Optional diagnostics publishing mode for the packaged reference LSP. */\n readonly diagnosticsMode?: \"off\" | \"plugin\";\n /** Source label to use when publishing plugin-derived diagnostics. */\n readonly diagnosticSource?: string;\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 const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);\n const diagnosticsMode = options.diagnosticsMode ?? \"off\";\n const diagnosticSource = options.diagnosticSource ?? \"formspec\";\n\n documents.listen(connection);\n\n async function publishDiagnosticsForDocument(document: TextDocument): Promise<void> {\n if (diagnosticsMode !== \"plugin\" || options.usePluginTransport === false) {\n return;\n }\n\n const filePath = fileUriToPathOrNull(document.uri);\n if (filePath === null) {\n return;\n }\n\n const diagnostics =\n (await getPluginDiagnosticsForDocument(\n workspaceRoots,\n filePath,\n document.getText(),\n pluginQueryTimeoutMs\n )) ?? [];\n\n void connection.sendDiagnostics({\n uri: document.uri,\n diagnostics: toLspDiagnostics(document, diagnostics, {\n source: diagnosticSource,\n }),\n });\n }\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 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 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 documents.onDidOpen(({ document }) => {\n void publishDiagnosticsForDocument(document).catch((error: unknown) => {\n connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);\n });\n });\n\n documents.onDidChangeContent(({ document }) => {\n void publishDiagnosticsForDocument(document).catch((error: unknown) => {\n connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);\n });\n });\n\n documents.onDidClose(({ document }) => {\n if (diagnosticsMode === \"plugin\") {\n void connection.sendDiagnostics({\n uri: document.uri,\n diagnostics: [] satisfies Diagnostic[],\n });\n }\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/internal\";\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\nfunction toTargetCompletionItems(\n tagName: string,\n targetCompletions: readonly string[]\n): CompletionItem[] {\n return targetCompletions.map((target: string) => ({\n label: target,\n kind:\n target === \"singular\" || target === \"plural\"\n ? CompletionItemKind.EnumMember\n : CompletionItemKind.Field,\n detail: `Target for @${tagName}`,\n }));\n}\n\nfunction filterTagNameCompletionItems(\n prefix: string,\n availableTags: readonly (TagDefinition | FormSpecSerializedTagDefinition)[]\n): CompletionItem[] {\n const normalizedPrefix = prefix.toLowerCase();\n return availableTags\n .map(toCompletionItem)\n .filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));\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 toTargetCompletionItems(\n semanticContext.semantic.tagName,\n semanticContext.semantic.targetCompletions\n );\n }\n\n if (semanticContext.kind !== \"tag-name\") {\n return [];\n }\n\n return filterTagNameCompletionItems(semanticContext.prefix, semanticContext.availableTags);\n }\n\n const resolvedContext = getSemanticCommentCompletionContextAtOffset(\n documentText,\n offset,\n extensions ? { extensions } : undefined\n );\n\n if (resolvedContext.kind === \"target\") {\n return toTargetCompletionItems(\n resolvedContext.semantic.tag.normalizedTagName,\n resolvedContext.semantic.targetCompletions\n );\n }\n\n if (resolvedContext.kind !== \"tag-name\") {\n return [];\n }\n\n return filterTagNameCompletionItems(resolvedContext.prefix, resolvedContext.availableTags);\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/internal\";\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 type {\n FormSpecAnalysisDiagnostic,\n FormSpecAnalysisDiagnosticLocation,\n} from \"@formspec/analysis/protocol\";\nimport {\n DiagnosticRelatedInformation,\n DiagnosticSeverity,\n Location,\n Range,\n type Diagnostic,\n} from \"vscode-languageserver/node.js\";\nimport type { TextDocument } from \"vscode-languageserver-textdocument\";\nimport { fileUriToPathOrNull } from \"./plugin-client.js\";\nexport { getPluginDiagnosticsForDocument } from \"./plugin-client.js\";\n\n/**\n * Options for converting canonical FormSpec diagnostics into LSP diagnostics.\n *\n * @public\n */\nexport interface ToLspDiagnosticsOptions {\n /** Source label shown by LSP clients. Defaults to `formspec`. */\n readonly source?: string;\n}\n\n/**\n * Converts canonical FormSpec diagnostics into LSP diagnostics.\n *\n * Downstream consumers that want complete white-label control can ignore this\n * helper and render their own messages from `code` + `data`.\n *\n * @public\n */\nexport function toLspDiagnostics(\n document: TextDocument,\n diagnostics: readonly FormSpecAnalysisDiagnostic[],\n options: ToLspDiagnosticsOptions = {}\n): Diagnostic[] {\n const source = options.source ?? \"formspec\";\n return diagnostics.map((diagnostic) => {\n const relatedInformation = toRelatedInformation(document, diagnostic.relatedLocations);\n return {\n range: spanToRange(document, diagnostic.range.start, diagnostic.range.end),\n severity: toLspSeverity(diagnostic.severity),\n source,\n code: diagnostic.code,\n message: diagnostic.message,\n ...(relatedInformation === undefined ? {} : { relatedInformation }),\n data: {\n ...diagnostic.data,\n category: diagnostic.category,\n },\n };\n });\n}\n\nfunction spanToRange(document: TextDocument, start: number, end: number): Range {\n return Range.create(document.positionAt(start), document.positionAt(end));\n}\n\nfunction toLspSeverity(severity: FormSpecAnalysisDiagnostic[\"severity\"]): DiagnosticSeverity {\n switch (severity) {\n case \"error\":\n return DiagnosticSeverity.Error;\n case \"warning\":\n return DiagnosticSeverity.Warning;\n case \"info\":\n return DiagnosticSeverity.Information;\n default:\n return DiagnosticSeverity.Information;\n }\n}\n\nfunction toRelatedInformation(\n document: TextDocument,\n locations: readonly FormSpecAnalysisDiagnosticLocation[]\n): DiagnosticRelatedInformation[] | undefined {\n if (locations.length === 0) {\n return undefined;\n }\n\n const currentDocumentFilePath = getDocumentFilePath(document);\n const relatedInformation = locations\n .filter((location) => location.filePath === currentDocumentFilePath)\n .map((location) =>\n DiagnosticRelatedInformation.create(\n Location.create(\n document.uri,\n spanToRange(document, location.range.start, location.range.end)\n ),\n location.message ?? \"Related FormSpec location\"\n )\n );\n\n return relatedInformation.length === 0 ? undefined : relatedInformation;\n}\n\nfunction getDocumentFilePath(document: TextDocument): string | null {\n return fileUriToPathOrNull(document.uri);\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 isFormSpecAnalysisManifest,\n isFormSpecSemanticResponse,\n type FormSpecAnalysisDiagnostic,\n type FormSpecAnalysisManifest,\n type FormSpecSerializedCompletionContext,\n type FormSpecSerializedHoverInfo,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis/protocol\";\nimport { getFormSpecManifestPath } from \"@formspec/analysis/internal\";\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\n/**\n * Retrieves canonical FormSpec diagnostics for the current document revision\n * from the plugin transport. Returns `null` when the transport is missing,\n * stale, or invalid.\n *\n * @public\n */\nexport async function getPluginDiagnosticsForDocument(\n workspaceRoots: readonly string[],\n filePath: string,\n documentText: string,\n timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS\n): Promise<readonly FormSpecAnalysisDiagnostic[] | null> {\n const response = await sendFileQuery(\n workspaceRoots,\n filePath,\n {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"diagnostics\",\n filePath,\n },\n timeoutMs\n );\n if (response?.kind !== \"diagnostics\") {\n return null;\n }\n\n return response.sourceHash === computeFormSpecTextHash(documentText)\n ? response.diagnostics\n : null;\n}\n"],"mappings":"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","names":["import_node","import_internal","import_node","import_internal","path","fs","net"]}
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * LSP process, while TypeScript-project-aware semantics are supplied by
11
11
  * `@formspec/ts-plugin` over a local manifest + IPC transport.
12
12
  *
13
- * Diagnostics are intentionally omitted per design decision A7.
13
+ * The packaged server acts as a reference implementation over the composable
14
+ * completion, hover, and diagnostics helpers exported from this package.
14
15
  *
15
16
  * @example
16
17
  * ```ts
@@ -24,6 +25,8 @@
24
25
  */
25
26
  export { createServer } from "./server.js";
26
27
  export type { CreateServerOptions } from "./server.js";
28
+ export type { CommentSpan, FormSpecAnalysisDiagnostic, FormSpecAnalysisDiagnosticCategory, FormSpecAnalysisDiagnosticDataValue, FormSpecAnalysisDiagnosticLocation, } from "@formspec/analysis/protocol";
29
+ export { getPluginDiagnosticsForDocument, toLspDiagnostics, type ToLspDiagnosticsOptions, } from "./diagnostics.js";
27
30
  export { getCompletionItems } from "./providers/completion.js";
28
31
  export { getHoverForTag } from "./providers/hover.js";
29
32
  export { getDefinition } from "./providers/definition.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EACV,WAAW,EACX,0BAA0B,EAC1B,kCAAkC,EAClC,mCAAmC,EACnC,kCAAkC,GACnC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,+BAA+B,EAC/B,gBAAgB,EAChB,KAAK,uBAAuB,GAC7B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -105,6 +105,14 @@ function getDefinition() {
105
105
  return null;
106
106
  }
107
107
 
108
+ // src/diagnostics.ts
109
+ import {
110
+ DiagnosticRelatedInformation,
111
+ DiagnosticSeverity,
112
+ Location,
113
+ Range
114
+ } from "vscode-languageserver/node.js";
115
+
108
116
  // src/plugin-client.ts
109
117
  import fs from "fs/promises";
110
118
  import net from "net";
@@ -113,10 +121,10 @@ import { fileURLToPath } from "url";
113
121
  import {
114
122
  FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
115
123
  computeFormSpecTextHash,
116
- getFormSpecManifestPath,
117
124
  isFormSpecAnalysisManifest,
118
125
  isFormSpecSemanticResponse
119
126
  } from "@formspec/analysis/protocol";
127
+ import { getFormSpecManifestPath } from "@formspec/analysis/internal";
120
128
  var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
121
129
  function getManifestPath(workspaceRoot) {
122
130
  return getFormSpecManifestPath(workspaceRoot);
@@ -246,6 +254,76 @@ async function getPluginHoverForDocument(workspaceRoots, filePath, documentText,
246
254
  }
247
255
  return response.sourceHash === computeFormSpecTextHash(documentText) ? response.hover : null;
248
256
  }
257
+ async function getPluginDiagnosticsForDocument(workspaceRoots, filePath, documentText, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
258
+ const response = await sendFileQuery(
259
+ workspaceRoots,
260
+ filePath,
261
+ {
262
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
263
+ kind: "diagnostics",
264
+ filePath
265
+ },
266
+ timeoutMs
267
+ );
268
+ if (response?.kind !== "diagnostics") {
269
+ return null;
270
+ }
271
+ return response.sourceHash === computeFormSpecTextHash(documentText) ? response.diagnostics : null;
272
+ }
273
+
274
+ // src/diagnostics.ts
275
+ function toLspDiagnostics(document, diagnostics, options = {}) {
276
+ const source = options.source ?? "formspec";
277
+ return diagnostics.map((diagnostic) => {
278
+ const relatedInformation = toRelatedInformation(document, diagnostic.relatedLocations);
279
+ return {
280
+ range: spanToRange(document, diagnostic.range.start, diagnostic.range.end),
281
+ severity: toLspSeverity(diagnostic.severity),
282
+ source,
283
+ code: diagnostic.code,
284
+ message: diagnostic.message,
285
+ ...relatedInformation === void 0 ? {} : { relatedInformation },
286
+ data: {
287
+ ...diagnostic.data,
288
+ category: diagnostic.category
289
+ }
290
+ };
291
+ });
292
+ }
293
+ function spanToRange(document, start, end) {
294
+ return Range.create(document.positionAt(start), document.positionAt(end));
295
+ }
296
+ function toLspSeverity(severity) {
297
+ switch (severity) {
298
+ case "error":
299
+ return DiagnosticSeverity.Error;
300
+ case "warning":
301
+ return DiagnosticSeverity.Warning;
302
+ case "info":
303
+ return DiagnosticSeverity.Information;
304
+ default:
305
+ return DiagnosticSeverity.Information;
306
+ }
307
+ }
308
+ function toRelatedInformation(document, locations) {
309
+ if (locations.length === 0) {
310
+ return void 0;
311
+ }
312
+ const currentDocumentFilePath = getDocumentFilePath(document);
313
+ const relatedInformation = locations.filter((location) => location.filePath === currentDocumentFilePath).map(
314
+ (location) => DiagnosticRelatedInformation.create(
315
+ Location.create(
316
+ document.uri,
317
+ spanToRange(document, location.range.start, location.range.end)
318
+ ),
319
+ location.message ?? "Related FormSpec location"
320
+ )
321
+ );
322
+ return relatedInformation.length === 0 ? void 0 : relatedInformation;
323
+ }
324
+ function getDocumentFilePath(document) {
325
+ return fileUriToPathOrNull(document.uri);
326
+ }
249
327
 
250
328
  // src/server.ts
251
329
  var PLUGIN_QUERY_TIMEOUT_ENV_VAR = "FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS";
@@ -278,7 +356,30 @@ function createServer(options = {}) {
278
356
  const documents = new TextDocuments(TextDocument);
279
357
  let workspaceRoots = [...options.workspaceRoots ?? []];
280
358
  const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);
359
+ const diagnosticsMode = options.diagnosticsMode ?? "off";
360
+ const diagnosticSource = options.diagnosticSource ?? "formspec";
281
361
  documents.listen(connection);
362
+ async function publishDiagnosticsForDocument(document) {
363
+ if (diagnosticsMode !== "plugin" || options.usePluginTransport === false) {
364
+ return;
365
+ }
366
+ const filePath = fileUriToPathOrNull(document.uri);
367
+ if (filePath === null) {
368
+ return;
369
+ }
370
+ const diagnostics = await getPluginDiagnosticsForDocument(
371
+ workspaceRoots,
372
+ filePath,
373
+ document.getText(),
374
+ pluginQueryTimeoutMs
375
+ ) ?? [];
376
+ void connection.sendDiagnostics({
377
+ uri: document.uri,
378
+ diagnostics: toLspDiagnostics(document, diagnostics, {
379
+ source: diagnosticSource
380
+ })
381
+ });
382
+ }
282
383
  connection.onInitialize((params) => {
283
384
  workspaceRoots = dedupeWorkspaceRoots([
284
385
  ...getWorkspaceRootsFromInitializeParams(params),
@@ -337,12 +438,32 @@ function createServer(options = {}) {
337
438
  connection.onDefinition((_params) => {
338
439
  return getDefinition();
339
440
  });
441
+ documents.onDidOpen(({ document }) => {
442
+ void publishDiagnosticsForDocument(document).catch((error) => {
443
+ connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
444
+ });
445
+ });
446
+ documents.onDidChangeContent(({ document }) => {
447
+ void publishDiagnosticsForDocument(document).catch((error) => {
448
+ connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
449
+ });
450
+ });
451
+ documents.onDidClose(({ document }) => {
452
+ if (diagnosticsMode === "plugin") {
453
+ void connection.sendDiagnostics({
454
+ uri: document.uri,
455
+ diagnostics: []
456
+ });
457
+ }
458
+ });
340
459
  return connection;
341
460
  }
342
461
  export {
343
462
  createServer,
344
463
  getCompletionItems,
345
464
  getDefinition,
346
- getHoverForTag
465
+ getHoverForTag,
466
+ getPluginDiagnosticsForDocument,
467
+ toLspDiagnostics
347
468
  };
348
469
  //# sourceMappingURL=index.js.map