@formspec/language-server 0.1.0-alpha.20 → 0.1.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/__tests__/diagnostics.test.d.ts +2 -0
- package/dist/__tests__/diagnostics.test.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/index.cjs +174 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -20
- package/dist/index.js.map +1 -1
- package/dist/language-server.d.ts +84 -1
- package/dist/plugin-client.d.ts +9 -1
- package/dist/plugin-client.d.ts.map +1 -1
- package/dist/providers/completion.d.ts +1 -1
- package/dist/providers/completion.d.ts.map +1 -1
- package/dist/providers/hover.d.ts +1 -1
- package/dist/providers/hover.d.ts.map +1 -1
- package/dist/server.d.ts +6 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +3 -3
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
|
|
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 {
|
|
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 @@
|
|
|
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,19 +33,21 @@ __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
|
|
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
|
|
45
|
-
var
|
|
47
|
+
var import_internal = require("@formspec/analysis/internal");
|
|
46
48
|
var import_node = require("vscode-languageserver/node.js");
|
|
47
49
|
function getCompletionItems(extensions) {
|
|
48
|
-
return (0,
|
|
50
|
+
return (0, import_internal.getConstraintTagDefinitions)(extensions).map((tag) => ({
|
|
49
51
|
label: `@${tag.canonicalName}`,
|
|
50
52
|
kind: import_node.CompletionItemKind.Keyword,
|
|
51
53
|
detail: tag.completionDetail
|
|
@@ -58,45 +60,52 @@ function toCompletionItem(tag) {
|
|
|
58
60
|
detail: tag.completionDetail
|
|
59
61
|
};
|
|
60
62
|
}
|
|
63
|
+
function toTargetCompletionItems(tagName, targetCompletions) {
|
|
64
|
+
return targetCompletions.map((target) => ({
|
|
65
|
+
label: target,
|
|
66
|
+
kind: target === "singular" || target === "plural" ? import_node.CompletionItemKind.EnumMember : import_node.CompletionItemKind.Field,
|
|
67
|
+
detail: `Target for @${tagName}`
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
function filterTagNameCompletionItems(prefix, availableTags) {
|
|
71
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
72
|
+
return availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
73
|
+
}
|
|
61
74
|
function getCompletionItemsAtOffset(documentText, offset, extensions, semanticContext) {
|
|
62
75
|
if (semanticContext !== null && semanticContext !== void 0) {
|
|
63
76
|
if (semanticContext.kind === "target") {
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}));
|
|
77
|
+
return toTargetCompletionItems(
|
|
78
|
+
semanticContext.semantic.tagName,
|
|
79
|
+
semanticContext.semantic.targetCompletions
|
|
80
|
+
);
|
|
69
81
|
}
|
|
70
82
|
if (semanticContext.kind !== "tag-name") {
|
|
71
83
|
return [];
|
|
72
84
|
}
|
|
73
|
-
|
|
74
|
-
return semanticContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix2));
|
|
85
|
+
return filterTagNameCompletionItems(semanticContext.prefix, semanticContext.availableTags);
|
|
75
86
|
}
|
|
76
|
-
const resolvedContext = (0,
|
|
87
|
+
const resolvedContext = (0, import_internal.getSemanticCommentCompletionContextAtOffset)(
|
|
77
88
|
documentText,
|
|
78
89
|
offset,
|
|
79
90
|
extensions ? { extensions } : void 0
|
|
80
91
|
);
|
|
81
92
|
if (resolvedContext.kind === "target") {
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}));
|
|
93
|
+
return toTargetCompletionItems(
|
|
94
|
+
resolvedContext.semantic.tag.normalizedTagName,
|
|
95
|
+
resolvedContext.semantic.targetCompletions
|
|
96
|
+
);
|
|
87
97
|
}
|
|
88
98
|
if (resolvedContext.kind !== "tag-name") {
|
|
89
99
|
return [];
|
|
90
100
|
}
|
|
91
|
-
|
|
92
|
-
return resolvedContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
101
|
+
return filterTagNameCompletionItems(resolvedContext.prefix, resolvedContext.availableTags);
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
// src/providers/hover.ts
|
|
96
|
-
var
|
|
105
|
+
var import_internal2 = require("@formspec/analysis/internal");
|
|
97
106
|
function getHoverForTag(tagName, extensions) {
|
|
98
107
|
const raw = tagName.startsWith("@") ? tagName.slice(1) : tagName;
|
|
99
|
-
const definition = (0,
|
|
108
|
+
const definition = (0, import_internal2.getTagDefinition)((0, import_internal2.normalizeFormSpecTagName)(raw), extensions);
|
|
100
109
|
if (!definition) {
|
|
101
110
|
return null;
|
|
102
111
|
}
|
|
@@ -108,7 +117,7 @@ function getHoverForTag(tagName, extensions) {
|
|
|
108
117
|
};
|
|
109
118
|
}
|
|
110
119
|
function getHoverAtOffset(documentText, offset, extensions, semanticHover) {
|
|
111
|
-
const hoverInfo = semanticHover ?? (0,
|
|
120
|
+
const hoverInfo = semanticHover ?? (0, import_internal2.getCommentHoverInfoAtOffset)(documentText, offset, extensions ? { extensions } : void 0);
|
|
112
121
|
if (hoverInfo === null) {
|
|
113
122
|
return null;
|
|
114
123
|
}
|
|
@@ -125,15 +134,18 @@ function getDefinition() {
|
|
|
125
134
|
return null;
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
// src/diagnostics.ts
|
|
138
|
+
var import_node2 = require("vscode-languageserver/node.js");
|
|
139
|
+
|
|
128
140
|
// src/plugin-client.ts
|
|
129
141
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
130
142
|
var import_node_net = __toESM(require("net"), 1);
|
|
131
143
|
var import_node_path = __toESM(require("path"), 1);
|
|
132
144
|
var import_node_url = require("url");
|
|
133
|
-
var
|
|
145
|
+
var import_protocol = require("@formspec/analysis/protocol");
|
|
134
146
|
var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
|
|
135
147
|
function getManifestPath(workspaceRoot) {
|
|
136
|
-
return (0,
|
|
148
|
+
return (0, import_protocol.getFormSpecManifestPath)(workspaceRoot);
|
|
137
149
|
}
|
|
138
150
|
function normalizeWorkspaceRoot(root) {
|
|
139
151
|
const resolved = import_node_path.default.resolve(root);
|
|
@@ -155,7 +167,7 @@ async function readManifest(workspaceRoot) {
|
|
|
155
167
|
try {
|
|
156
168
|
const manifestText = await import_promises.default.readFile(getManifestPath(workspaceRoot), "utf8");
|
|
157
169
|
const manifest = JSON.parse(manifestText);
|
|
158
|
-
if (!(0,
|
|
170
|
+
if (!(0, import_protocol.isFormSpecAnalysisManifest)(manifest)) {
|
|
159
171
|
return null;
|
|
160
172
|
}
|
|
161
173
|
return manifest;
|
|
@@ -195,7 +207,7 @@ async function sendSemanticQuery(manifest, query, timeoutMs = DEFAULT_PLUGIN_QUE
|
|
|
195
207
|
buffer = buffer.slice(newlineIndex + 1);
|
|
196
208
|
try {
|
|
197
209
|
const response = JSON.parse(payload);
|
|
198
|
-
finish((0,
|
|
210
|
+
finish((0, import_protocol.isFormSpecSemanticResponse)(response) ? response : null);
|
|
199
211
|
} catch {
|
|
200
212
|
finish(null);
|
|
201
213
|
}
|
|
@@ -231,7 +243,7 @@ async function getPluginCompletionContextForDocument(workspaceRoots, filePath, d
|
|
|
231
243
|
workspaceRoots,
|
|
232
244
|
filePath,
|
|
233
245
|
{
|
|
234
|
-
protocolVersion:
|
|
246
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
235
247
|
kind: "completion",
|
|
236
248
|
filePath,
|
|
237
249
|
offset
|
|
@@ -241,14 +253,14 @@ async function getPluginCompletionContextForDocument(workspaceRoots, filePath, d
|
|
|
241
253
|
if (response?.kind !== "completion") {
|
|
242
254
|
return null;
|
|
243
255
|
}
|
|
244
|
-
return response.sourceHash === (0,
|
|
256
|
+
return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.context : null;
|
|
245
257
|
}
|
|
246
258
|
async function getPluginHoverForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
247
259
|
const response = await sendFileQuery(
|
|
248
260
|
workspaceRoots,
|
|
249
261
|
filePath,
|
|
250
262
|
{
|
|
251
|
-
protocolVersion:
|
|
263
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
252
264
|
kind: "hover",
|
|
253
265
|
filePath,
|
|
254
266
|
offset
|
|
@@ -258,13 +270,95 @@ async function getPluginHoverForDocument(workspaceRoots, filePath, documentText,
|
|
|
258
270
|
if (response?.kind !== "hover") {
|
|
259
271
|
return null;
|
|
260
272
|
}
|
|
261
|
-
return response.sourceHash === (0,
|
|
273
|
+
return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.hover : null;
|
|
274
|
+
}
|
|
275
|
+
async function getPluginDiagnosticsForDocument(workspaceRoots, filePath, documentText, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
276
|
+
const response = await sendFileQuery(
|
|
277
|
+
workspaceRoots,
|
|
278
|
+
filePath,
|
|
279
|
+
{
|
|
280
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
281
|
+
kind: "diagnostics",
|
|
282
|
+
filePath
|
|
283
|
+
},
|
|
284
|
+
timeoutMs
|
|
285
|
+
);
|
|
286
|
+
if (response?.kind !== "diagnostics") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.diagnostics : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/diagnostics.ts
|
|
293
|
+
function toLspDiagnostics(document, diagnostics, options = {}) {
|
|
294
|
+
const source = options.source ?? "formspec";
|
|
295
|
+
return diagnostics.map((diagnostic) => {
|
|
296
|
+
const relatedInformation = toRelatedInformation(document, diagnostic.relatedLocations);
|
|
297
|
+
return {
|
|
298
|
+
range: spanToRange(document, diagnostic.range.start, diagnostic.range.end),
|
|
299
|
+
severity: toLspSeverity(diagnostic.severity),
|
|
300
|
+
source,
|
|
301
|
+
code: diagnostic.code,
|
|
302
|
+
message: diagnostic.message,
|
|
303
|
+
...relatedInformation === void 0 ? {} : { relatedInformation },
|
|
304
|
+
data: {
|
|
305
|
+
...diagnostic.data,
|
|
306
|
+
category: diagnostic.category
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function spanToRange(document, start, end) {
|
|
312
|
+
return import_node2.Range.create(document.positionAt(start), document.positionAt(end));
|
|
313
|
+
}
|
|
314
|
+
function toLspSeverity(severity) {
|
|
315
|
+
switch (severity) {
|
|
316
|
+
case "error":
|
|
317
|
+
return import_node2.DiagnosticSeverity.Error;
|
|
318
|
+
case "warning":
|
|
319
|
+
return import_node2.DiagnosticSeverity.Warning;
|
|
320
|
+
case "info":
|
|
321
|
+
return import_node2.DiagnosticSeverity.Information;
|
|
322
|
+
default:
|
|
323
|
+
return import_node2.DiagnosticSeverity.Information;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function toRelatedInformation(document, locations) {
|
|
327
|
+
if (locations.length === 0) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
const currentDocumentFilePath = getDocumentFilePath(document);
|
|
331
|
+
const relatedInformation = locations.filter((location) => location.filePath === currentDocumentFilePath).map(
|
|
332
|
+
(location) => import_node2.DiagnosticRelatedInformation.create(
|
|
333
|
+
import_node2.Location.create(
|
|
334
|
+
document.uri,
|
|
335
|
+
spanToRange(document, location.range.start, location.range.end)
|
|
336
|
+
),
|
|
337
|
+
location.message ?? "Related FormSpec location"
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
return relatedInformation.length === 0 ? void 0 : relatedInformation;
|
|
341
|
+
}
|
|
342
|
+
function getDocumentFilePath(document) {
|
|
343
|
+
return fileUriToPathOrNull(document.uri);
|
|
262
344
|
}
|
|
263
345
|
|
|
264
346
|
// src/server.ts
|
|
347
|
+
var PLUGIN_QUERY_TIMEOUT_ENV_VAR = "FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS";
|
|
265
348
|
function dedupeWorkspaceRoots(workspaceRoots) {
|
|
266
349
|
return [...new Set(workspaceRoots)];
|
|
267
350
|
}
|
|
351
|
+
function resolvePluginQueryTimeoutMs(explicitTimeoutMs) {
|
|
352
|
+
if (explicitTimeoutMs !== void 0) {
|
|
353
|
+
return explicitTimeoutMs;
|
|
354
|
+
}
|
|
355
|
+
const rawValue = process.env[PLUGIN_QUERY_TIMEOUT_ENV_VAR];
|
|
356
|
+
if (rawValue === void 0) {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
360
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
361
|
+
}
|
|
268
362
|
function getWorkspaceRootsFromInitializeParams(params) {
|
|
269
363
|
const workspaceFolders = params.workspaceFolders?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri)).filter((workspaceRoot) => workspaceRoot !== null) ?? [];
|
|
270
364
|
const rootUri = params.rootUri === null || params.rootUri === void 0 ? null : fileUriToPathOrNull(params.rootUri);
|
|
@@ -276,10 +370,34 @@ function getWorkspaceRootsFromInitializeParams(params) {
|
|
|
276
370
|
]);
|
|
277
371
|
}
|
|
278
372
|
function createServer(options = {}) {
|
|
279
|
-
const connection = (0,
|
|
280
|
-
const documents = new
|
|
373
|
+
const connection = (0, import_node3.createConnection)(import_node3.ProposedFeatures.all);
|
|
374
|
+
const documents = new import_node3.TextDocuments(import_vscode_languageserver_textdocument.TextDocument);
|
|
281
375
|
let workspaceRoots = [...options.workspaceRoots ?? []];
|
|
376
|
+
const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);
|
|
377
|
+
const diagnosticsMode = options.diagnosticsMode ?? "off";
|
|
378
|
+
const diagnosticSource = options.diagnosticSource ?? "formspec";
|
|
282
379
|
documents.listen(connection);
|
|
380
|
+
async function publishDiagnosticsForDocument(document) {
|
|
381
|
+
if (diagnosticsMode !== "plugin" || options.usePluginTransport === false) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const filePath = fileUriToPathOrNull(document.uri);
|
|
385
|
+
if (filePath === null) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const diagnostics = await getPluginDiagnosticsForDocument(
|
|
389
|
+
workspaceRoots,
|
|
390
|
+
filePath,
|
|
391
|
+
document.getText(),
|
|
392
|
+
pluginQueryTimeoutMs
|
|
393
|
+
) ?? [];
|
|
394
|
+
void connection.sendDiagnostics({
|
|
395
|
+
uri: document.uri,
|
|
396
|
+
diagnostics: toLspDiagnostics(document, diagnostics, {
|
|
397
|
+
source: diagnosticSource
|
|
398
|
+
})
|
|
399
|
+
});
|
|
400
|
+
}
|
|
283
401
|
connection.onInitialize((params) => {
|
|
284
402
|
workspaceRoots = dedupeWorkspaceRoots([
|
|
285
403
|
...getWorkspaceRootsFromInitializeParams(params),
|
|
@@ -287,7 +405,7 @@ function createServer(options = {}) {
|
|
|
287
405
|
]);
|
|
288
406
|
return {
|
|
289
407
|
capabilities: {
|
|
290
|
-
textDocumentSync:
|
|
408
|
+
textDocumentSync: import_node3.TextDocumentSyncKind.Incremental,
|
|
291
409
|
completionProvider: {
|
|
292
410
|
// Trigger completions inside JSDoc comments for tags and target specifiers
|
|
293
411
|
triggerCharacters: ["@", ":"]
|
|
@@ -314,7 +432,7 @@ function createServer(options = {}) {
|
|
|
314
432
|
filePath,
|
|
315
433
|
documentText,
|
|
316
434
|
offset,
|
|
317
|
-
|
|
435
|
+
pluginQueryTimeoutMs
|
|
318
436
|
);
|
|
319
437
|
return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);
|
|
320
438
|
});
|
|
@@ -331,13 +449,31 @@ function createServer(options = {}) {
|
|
|
331
449
|
filePath,
|
|
332
450
|
documentText,
|
|
333
451
|
offset,
|
|
334
|
-
|
|
452
|
+
pluginQueryTimeoutMs
|
|
335
453
|
);
|
|
336
454
|
return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);
|
|
337
455
|
});
|
|
338
456
|
connection.onDefinition((_params) => {
|
|
339
457
|
return getDefinition();
|
|
340
458
|
});
|
|
459
|
+
documents.onDidOpen(({ document }) => {
|
|
460
|
+
void publishDiagnosticsForDocument(document).catch((error) => {
|
|
461
|
+
connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
documents.onDidChangeContent(({ document }) => {
|
|
465
|
+
void publishDiagnosticsForDocument(document).catch((error) => {
|
|
466
|
+
connection.console.error(`[FormSpec] Failed to publish diagnostics: ${String(error)}`);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
documents.onDidClose(({ document }) => {
|
|
470
|
+
if (diagnosticsMode === "plugin") {
|
|
471
|
+
void connection.sendDiagnostics({
|
|
472
|
+
uri: document.uri,
|
|
473
|
+
diagnostics: []
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
});
|
|
341
477
|
return connection;
|
|
342
478
|
}
|
|
343
479
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -345,6 +481,8 @@ function createServer(options = {}) {
|
|
|
345
481
|
createServer,
|
|
346
482
|
getCompletionItems,
|
|
347
483
|
getDefinition,
|
|
348
|
-
getHoverForTag
|
|
484
|
+
getHoverForTag,
|
|
485
|
+
getPluginDiagnosticsForDocument,
|
|
486
|
+
toLspDiagnostics
|
|
349
487
|
});
|
|
350
488
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\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"]}
|
|
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 getFormSpecManifestPath,\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\";\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","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
|
-
*
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
@@ -11,7 +11,7 @@ import { TextDocument } from "vscode-languageserver-textdocument";
|
|
|
11
11
|
import {
|
|
12
12
|
getConstraintTagDefinitions,
|
|
13
13
|
getSemanticCommentCompletionContextAtOffset
|
|
14
|
-
} from "@formspec/analysis";
|
|
14
|
+
} from "@formspec/analysis/internal";
|
|
15
15
|
import { CompletionItemKind } from "vscode-languageserver/node.js";
|
|
16
16
|
function getCompletionItems(extensions) {
|
|
17
17
|
return getConstraintTagDefinitions(extensions).map((tag) => ({
|
|
@@ -27,20 +27,29 @@ function toCompletionItem(tag) {
|
|
|
27
27
|
detail: tag.completionDetail
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
function toTargetCompletionItems(tagName, targetCompletions) {
|
|
31
|
+
return targetCompletions.map((target) => ({
|
|
32
|
+
label: target,
|
|
33
|
+
kind: target === "singular" || target === "plural" ? CompletionItemKind.EnumMember : CompletionItemKind.Field,
|
|
34
|
+
detail: `Target for @${tagName}`
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
function filterTagNameCompletionItems(prefix, availableTags) {
|
|
38
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
39
|
+
return availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
40
|
+
}
|
|
30
41
|
function getCompletionItemsAtOffset(documentText, offset, extensions, semanticContext) {
|
|
31
42
|
if (semanticContext !== null && semanticContext !== void 0) {
|
|
32
43
|
if (semanticContext.kind === "target") {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}));
|
|
44
|
+
return toTargetCompletionItems(
|
|
45
|
+
semanticContext.semantic.tagName,
|
|
46
|
+
semanticContext.semantic.targetCompletions
|
|
47
|
+
);
|
|
38
48
|
}
|
|
39
49
|
if (semanticContext.kind !== "tag-name") {
|
|
40
50
|
return [];
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
return semanticContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix2));
|
|
52
|
+
return filterTagNameCompletionItems(semanticContext.prefix, semanticContext.availableTags);
|
|
44
53
|
}
|
|
45
54
|
const resolvedContext = getSemanticCommentCompletionContextAtOffset(
|
|
46
55
|
documentText,
|
|
@@ -48,17 +57,15 @@ function getCompletionItemsAtOffset(documentText, offset, extensions, semanticCo
|
|
|
48
57
|
extensions ? { extensions } : void 0
|
|
49
58
|
);
|
|
50
59
|
if (resolvedContext.kind === "target") {
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}));
|
|
60
|
+
return toTargetCompletionItems(
|
|
61
|
+
resolvedContext.semantic.tag.normalizedTagName,
|
|
62
|
+
resolvedContext.semantic.targetCompletions
|
|
63
|
+
);
|
|
56
64
|
}
|
|
57
65
|
if (resolvedContext.kind !== "tag-name") {
|
|
58
66
|
return [];
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
return resolvedContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
68
|
+
return filterTagNameCompletionItems(resolvedContext.prefix, resolvedContext.availableTags);
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
// src/providers/hover.ts
|
|
@@ -66,7 +73,7 @@ import {
|
|
|
66
73
|
getCommentHoverInfoAtOffset,
|
|
67
74
|
getTagDefinition,
|
|
68
75
|
normalizeFormSpecTagName
|
|
69
|
-
} from "@formspec/analysis";
|
|
76
|
+
} from "@formspec/analysis/internal";
|
|
70
77
|
function getHoverForTag(tagName, extensions) {
|
|
71
78
|
const raw = tagName.startsWith("@") ? tagName.slice(1) : tagName;
|
|
72
79
|
const definition = getTagDefinition(normalizeFormSpecTagName(raw), extensions);
|
|
@@ -98,6 +105,14 @@ function getDefinition() {
|
|
|
98
105
|
return null;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
// src/diagnostics.ts
|
|
109
|
+
import {
|
|
110
|
+
DiagnosticRelatedInformation,
|
|
111
|
+
DiagnosticSeverity,
|
|
112
|
+
Location,
|
|
113
|
+
Range
|
|
114
|
+
} from "vscode-languageserver/node.js";
|
|
115
|
+
|
|
101
116
|
// src/plugin-client.ts
|
|
102
117
|
import fs from "fs/promises";
|
|
103
118
|
import net from "net";
|
|
@@ -109,7 +124,7 @@ import {
|
|
|
109
124
|
getFormSpecManifestPath,
|
|
110
125
|
isFormSpecAnalysisManifest,
|
|
111
126
|
isFormSpecSemanticResponse
|
|
112
|
-
} from "@formspec/analysis";
|
|
127
|
+
} from "@formspec/analysis/protocol";
|
|
113
128
|
var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
|
|
114
129
|
function getManifestPath(workspaceRoot) {
|
|
115
130
|
return getFormSpecManifestPath(workspaceRoot);
|
|
@@ -239,11 +254,93 @@ async function getPluginHoverForDocument(workspaceRoots, filePath, documentText,
|
|
|
239
254
|
}
|
|
240
255
|
return response.sourceHash === computeFormSpecTextHash(documentText) ? response.hover : null;
|
|
241
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
|
+
}
|
|
242
327
|
|
|
243
328
|
// src/server.ts
|
|
329
|
+
var PLUGIN_QUERY_TIMEOUT_ENV_VAR = "FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS";
|
|
244
330
|
function dedupeWorkspaceRoots(workspaceRoots) {
|
|
245
331
|
return [...new Set(workspaceRoots)];
|
|
246
332
|
}
|
|
333
|
+
function resolvePluginQueryTimeoutMs(explicitTimeoutMs) {
|
|
334
|
+
if (explicitTimeoutMs !== void 0) {
|
|
335
|
+
return explicitTimeoutMs;
|
|
336
|
+
}
|
|
337
|
+
const rawValue = process.env[PLUGIN_QUERY_TIMEOUT_ENV_VAR];
|
|
338
|
+
if (rawValue === void 0) {
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
342
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
343
|
+
}
|
|
247
344
|
function getWorkspaceRootsFromInitializeParams(params) {
|
|
248
345
|
const workspaceFolders = params.workspaceFolders?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri)).filter((workspaceRoot) => workspaceRoot !== null) ?? [];
|
|
249
346
|
const rootUri = params.rootUri === null || params.rootUri === void 0 ? null : fileUriToPathOrNull(params.rootUri);
|
|
@@ -258,7 +355,31 @@ function createServer(options = {}) {
|
|
|
258
355
|
const connection = createConnection(ProposedFeatures.all);
|
|
259
356
|
const documents = new TextDocuments(TextDocument);
|
|
260
357
|
let workspaceRoots = [...options.workspaceRoots ?? []];
|
|
358
|
+
const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);
|
|
359
|
+
const diagnosticsMode = options.diagnosticsMode ?? "off";
|
|
360
|
+
const diagnosticSource = options.diagnosticSource ?? "formspec";
|
|
261
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
|
+
}
|
|
262
383
|
connection.onInitialize((params) => {
|
|
263
384
|
workspaceRoots = dedupeWorkspaceRoots([
|
|
264
385
|
...getWorkspaceRootsFromInitializeParams(params),
|
|
@@ -293,7 +414,7 @@ function createServer(options = {}) {
|
|
|
293
414
|
filePath,
|
|
294
415
|
documentText,
|
|
295
416
|
offset,
|
|
296
|
-
|
|
417
|
+
pluginQueryTimeoutMs
|
|
297
418
|
);
|
|
298
419
|
return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);
|
|
299
420
|
});
|
|
@@ -310,19 +431,39 @@ function createServer(options = {}) {
|
|
|
310
431
|
filePath,
|
|
311
432
|
documentText,
|
|
312
433
|
offset,
|
|
313
|
-
|
|
434
|
+
pluginQueryTimeoutMs
|
|
314
435
|
);
|
|
315
436
|
return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);
|
|
316
437
|
});
|
|
317
438
|
connection.onDefinition((_params) => {
|
|
318
439
|
return getDefinition();
|
|
319
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
|
+
});
|
|
320
459
|
return connection;
|
|
321
460
|
}
|
|
322
461
|
export {
|
|
323
462
|
createServer,
|
|
324
463
|
getCompletionItems,
|
|
325
464
|
getDefinition,
|
|
326
|
-
getHoverForTag
|
|
465
|
+
getHoverForTag,
|
|
466
|
+
getPluginDiagnosticsForDocument,
|
|
467
|
+
toLspDiagnostics
|
|
327
468
|
};
|
|
328
469
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../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 * 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":["normalizedPrefix"]}
|
|
1
|
+
{"version":3,"sources":["../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 * 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 getFormSpecManifestPath,\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\";\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":[]}
|
|
@@ -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
|
-
*
|
|
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
|
|
@@ -25,9 +26,17 @@
|
|
|
25
26
|
|
|
26
27
|
import { CompletionItem } from 'vscode-languageserver/node.js';
|
|
27
28
|
import { Connection } from 'vscode-languageserver/node.js';
|
|
29
|
+
import { Diagnostic } from 'vscode-languageserver/node.js';
|
|
28
30
|
import type { ExtensionDefinition } from '@formspec/core';
|
|
29
31
|
import type { Hover } from 'vscode-languageserver/node.js';
|
|
30
32
|
import type { Location } from 'vscode-languageserver/node.js';
|
|
33
|
+
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
34
|
+
|
|
35
|
+
/** @public */
|
|
36
|
+
export declare interface CommentSpan {
|
|
37
|
+
readonly start: number;
|
|
38
|
+
readonly end: number;
|
|
39
|
+
}
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* Creates and configures the FormSpec language server connection.
|
|
@@ -54,6 +63,51 @@ export declare interface CreateServerOptions {
|
|
|
54
63
|
readonly usePluginTransport?: boolean;
|
|
55
64
|
/** IPC timeout, in milliseconds, for semantic plugin requests. */
|
|
56
65
|
readonly pluginQueryTimeoutMs?: number;
|
|
66
|
+
/** Optional diagnostics publishing mode for the packaged reference LSP. */
|
|
67
|
+
readonly diagnosticsMode?: "off" | "plugin";
|
|
68
|
+
/** Source label to use when publishing plugin-derived diagnostics. */
|
|
69
|
+
readonly diagnosticSource?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* File-local diagnostic derived from comment parsing or semantic analysis.
|
|
74
|
+
*
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export declare interface FormSpecAnalysisDiagnostic {
|
|
78
|
+
readonly code: string;
|
|
79
|
+
readonly category: FormSpecAnalysisDiagnosticCategory;
|
|
80
|
+
readonly message: string;
|
|
81
|
+
readonly range: CommentSpan;
|
|
82
|
+
readonly severity: "error" | "warning" | "info";
|
|
83
|
+
readonly relatedLocations: readonly FormSpecAnalysisDiagnosticLocation[];
|
|
84
|
+
readonly data: Record<string, FormSpecAnalysisDiagnosticDataValue>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Machine-readable diagnostic category used by FormSpec tooling surfaces.
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
export declare type FormSpecAnalysisDiagnosticCategory = "tag-recognition" | "value-parsing" | "type-compatibility" | "target-resolution" | "constraint-validation" | "infrastructure";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Primitive structured values carried in diagnostic facts for white-label
|
|
96
|
+
* downstream rendering.
|
|
97
|
+
*
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
export declare type FormSpecAnalysisDiagnosticDataValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Additional source location associated with a diagnostic.
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export declare interface FormSpecAnalysisDiagnosticLocation {
|
|
108
|
+
readonly filePath: string;
|
|
109
|
+
readonly range: CommentSpan;
|
|
110
|
+
readonly message?: string;
|
|
57
111
|
}
|
|
58
112
|
|
|
59
113
|
/**
|
|
@@ -80,4 +134,33 @@ export declare function getDefinition(): Location | null;
|
|
|
80
134
|
*/
|
|
81
135
|
export declare function getHoverForTag(tagName: string, extensions?: readonly ExtensionDefinition[]): Hover | null;
|
|
82
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Retrieves canonical FormSpec diagnostics for the current document revision
|
|
139
|
+
* from the plugin transport. Returns `null` when the transport is missing,
|
|
140
|
+
* stale, or invalid.
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
export declare function getPluginDiagnosticsForDocument(workspaceRoots: readonly string[], filePath: string, documentText: string, timeoutMs?: number): Promise<readonly FormSpecAnalysisDiagnostic[] | null>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Converts canonical FormSpec diagnostics into LSP diagnostics.
|
|
148
|
+
*
|
|
149
|
+
* Downstream consumers that want complete white-label control can ignore this
|
|
150
|
+
* helper and render their own messages from `code` + `data`.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
export declare function toLspDiagnostics(document: TextDocument, diagnostics: readonly FormSpecAnalysisDiagnostic[], options?: ToLspDiagnosticsOptions): Diagnostic[];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Options for converting canonical FormSpec diagnostics into LSP diagnostics.
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
*/
|
|
161
|
+
export declare interface ToLspDiagnosticsOptions {
|
|
162
|
+
/** Source label shown by LSP clients. Defaults to `formspec`. */
|
|
163
|
+
readonly source?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
83
166
|
export { }
|
package/dist/plugin-client.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { type FormSpecSerializedCompletionContext, type FormSpecSerializedHoverInfo } from "@formspec/analysis";
|
|
1
|
+
import { type FormSpecAnalysisDiagnostic, type FormSpecSerializedCompletionContext, type FormSpecSerializedHoverInfo } from "@formspec/analysis/protocol";
|
|
2
2
|
export declare function fileUriToPathOrNull(uri: string): string | null;
|
|
3
3
|
export declare function getPluginCompletionContextForDocument(workspaceRoots: readonly string[], filePath: string, documentText: string, offset: number, timeoutMs?: number): Promise<FormSpecSerializedCompletionContext | null>;
|
|
4
4
|
export declare function getPluginHoverForDocument(workspaceRoots: readonly string[], filePath: string, documentText: string, offset: number, timeoutMs?: number): Promise<FormSpecSerializedHoverInfo | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves canonical FormSpec diagnostics for the current document revision
|
|
7
|
+
* from the plugin transport. Returns `null` when the transport is missing,
|
|
8
|
+
* stale, or invalid.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare function getPluginDiagnosticsForDocument(workspaceRoots: readonly string[], filePath: string, documentText: string, timeoutMs?: number): Promise<readonly FormSpecAnalysisDiagnostic[] | null>;
|
|
5
13
|
//# sourceMappingURL=plugin-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-client.d.ts","sourceRoot":"","sources":["../src/plugin-client.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"plugin-client.d.ts","sourceRoot":"","sources":["../src/plugin-client.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,0BAA0B,EAE/B,KAAK,mCAAmC,EACxC,KAAK,2BAA2B,EAGjC,MAAM,6BAA6B,CAAC;AAwGrC,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM9D;AAqBD,wBAAsB,qCAAqC,CACzD,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,SAAkC,GAC1C,OAAO,CAAC,mCAAmC,GAAG,IAAI,CAAC,CAiBrD;AAED,wBAAsB,yBAAyB,CAC7C,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,SAAkC,GAC1C,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAiB7C;AAED;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CACnD,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,SAAS,SAAkC,GAC1C,OAAO,CAAC,SAAS,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAkBvD"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses the shared tag registry from `@formspec/analysis` so completions stay
|
|
5
5
|
* aligned with the same metadata that powers linting and build-time analysis.
|
|
6
6
|
*/
|
|
7
|
-
import { type FormSpecSerializedCompletionContext } from "@formspec/analysis";
|
|
7
|
+
import { type FormSpecSerializedCompletionContext } from "@formspec/analysis/internal";
|
|
8
8
|
import type { ExtensionDefinition } from "@formspec/core";
|
|
9
9
|
import { CompletionItem } from "vscode-languageserver/node.js";
|
|
10
10
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/providers/completion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,mCAAmC,EAKzC,MAAM,
|
|
1
|
+
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/providers/completion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,mCAAmC,EAKzC,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAsB,MAAM,+BAA+B,CAAC;AAEnF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,GAAG,cAAc,EAAE,CAMhG;AAkCD,gBAAgB;AAChB,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,EAC3C,eAAe,CAAC,EAAE,mCAAmC,GAAG,IAAI,GAC3D,cAAc,EAAE,CAkClB"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses the shared registry from `@formspec/analysis` so hover content stays in
|
|
5
5
|
* sync with the tag inventory and overload metadata.
|
|
6
6
|
*/
|
|
7
|
-
import { type FormSpecSerializedHoverInfo } from "@formspec/analysis";
|
|
7
|
+
import { type FormSpecSerializedHoverInfo } from "@formspec/analysis/internal";
|
|
8
8
|
import type { ExtensionDefinition } from "@formspec/core";
|
|
9
9
|
import type { Hover } from "vscode-languageserver/node.js";
|
|
10
10
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/providers/hover.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,2BAA2B,EAIjC,MAAM,
|
|
1
|
+
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/providers/hover.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,2BAA2B,EAIjC,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAE3D;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,GAC1C,KAAK,GAAG,IAAI,CAad;AAED,gBAAgB;AAChB,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,EAC3C,aAAa,CAAC,EAAE,2BAA2B,GAAG,IAAI,GACjD,KAAK,GAAG,IAAI,CAcd"}
|
package/dist/server.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - `textDocument/hover` — Documentation for recognized constraint tags
|
|
7
7
|
* - `textDocument/definition` — Go-to-definition (stub, returns null)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* The packaged language server is a reference implementation built on the same
|
|
10
|
+
* composable helpers that downstream consumers can call directly.
|
|
10
11
|
*/
|
|
11
12
|
import { type Connection } from "vscode-languageserver/node.js";
|
|
12
13
|
import type { ExtensionDefinition } from "@formspec/core";
|
|
@@ -24,6 +25,10 @@ export interface CreateServerOptions {
|
|
|
24
25
|
readonly usePluginTransport?: boolean;
|
|
25
26
|
/** IPC timeout, in milliseconds, for semantic plugin requests. */
|
|
26
27
|
readonly pluginQueryTimeoutMs?: number;
|
|
28
|
+
/** Optional diagnostics publishing mode for the packaged reference LSP. */
|
|
29
|
+
readonly diagnosticsMode?: "off" | "plugin";
|
|
30
|
+
/** Source label to use when publishing plugin-derived diagnostics. */
|
|
31
|
+
readonly diagnosticSource?: string;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Creates and configures the FormSpec language server connection.
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAML,KAAK,UAAU,EAEhB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAsD1D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACrD,yEAAyE;IACzE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5C,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,UAAU,CAoI1E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formspec/language-server",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.22",
|
|
4
4
|
"description": "Language server for FormSpec — completions, hover, and go-to-definition for JSDoc constraint tags",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"vscode-languageserver": "^9.0.1",
|
|
22
22
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
23
|
-
"@formspec/analysis": "0.1.0-alpha.
|
|
24
|
-
"@formspec/core": "0.1.0-alpha.
|
|
23
|
+
"@formspec/analysis": "0.1.0-alpha.22",
|
|
24
|
+
"@formspec/core": "0.1.0-alpha.21"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"vitest": "^3.0.0"
|