@formspec/language-server 0.1.0-alpha.19 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/plugin-client.test.d.ts +2 -0
- package/dist/__tests__/plugin-client.test.d.ts.map +1 -0
- package/dist/index.cjs +283 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +285 -235
- package/dist/index.js.map +1 -1
- package/dist/language-server.d.ts +21 -19
- package/dist/plugin-client.d.ts +5 -0
- package/dist/plugin-client.d.ts.map +1 -0
- package/dist/providers/completion.d.ts +8 -13
- package/dist/providers/completion.d.ts.map +1 -1
- package/dist/providers/definition.d.ts +1 -0
- package/dist/providers/definition.d.ts.map +1 -1
- package/dist/providers/hover.d.ts +9 -12
- package/dist/providers/hover.d.ts.map +1 -1
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/plugin-client.test.ts"],"names":[],"mappings":""}
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -29,246 +39,90 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
39
|
|
|
30
40
|
// src/server.ts
|
|
31
41
|
var import_node2 = require("vscode-languageserver/node.js");
|
|
42
|
+
var import_vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
|
|
32
43
|
|
|
33
44
|
// src/providers/completion.ts
|
|
34
|
-
var
|
|
45
|
+
var import_internal = require("@formspec/analysis/internal");
|
|
35
46
|
var import_node = require("vscode-languageserver/node.js");
|
|
36
|
-
var CONSTRAINT_DETAIL = {
|
|
37
|
-
minimum: "Minimum numeric value (inclusive). Example: `@minimum 0`",
|
|
38
|
-
maximum: "Maximum numeric value (inclusive). Example: `@maximum 100`",
|
|
39
|
-
exclusiveMinimum: "Minimum numeric value (exclusive). Example: `@exclusiveMinimum 0`",
|
|
40
|
-
exclusiveMaximum: "Maximum numeric value (exclusive). Example: `@exclusiveMaximum 100`",
|
|
41
|
-
multipleOf: "Value must be a multiple of this number. Example: `@multipleOf 0.01`",
|
|
42
|
-
minLength: "Minimum string length. Example: `@minLength 1`",
|
|
43
|
-
maxLength: "Maximum string length. Example: `@maxLength 255`",
|
|
44
|
-
minItems: "Minimum number of array items. Example: `@minItems 1`",
|
|
45
|
-
maxItems: "Maximum number of array items. Example: `@maxItems 10`",
|
|
46
|
-
uniqueItems: "Require all array items to be distinct. Example: `@uniqueItems`",
|
|
47
|
-
pattern: "Regular expression pattern for string validation. Example: `@pattern ^[a-z]+$`",
|
|
48
|
-
enumOptions: 'Inline JSON array of allowed enum values. Example: `@enumOptions ["a","b","c"]`',
|
|
49
|
-
const: 'Require a constant JSON value. Example: `@const "USD"`'
|
|
50
|
-
};
|
|
51
47
|
function getCompletionItems(extensions) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
return (0, import_internal.getConstraintTagDefinitions)(extensions).map((tag) => ({
|
|
49
|
+
label: `@${tag.canonicalName}`,
|
|
50
|
+
kind: import_node.CompletionItemKind.Keyword,
|
|
51
|
+
detail: tag.completionDetail
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function toCompletionItem(tag) {
|
|
55
|
+
return {
|
|
56
|
+
label: `@${tag.canonicalName}`,
|
|
57
|
+
kind: import_node.CompletionItemKind.Keyword,
|
|
58
|
+
detail: tag.completionDetail
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function toTargetCompletionItems(tagName, targetCompletions) {
|
|
62
|
+
return targetCompletions.map((target) => ({
|
|
63
|
+
label: target,
|
|
64
|
+
kind: target === "singular" || target === "plural" ? import_node.CompletionItemKind.EnumMember : import_node.CompletionItemKind.Field,
|
|
65
|
+
detail: `Target for @${tagName}`
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function filterTagNameCompletionItems(prefix, availableTags) {
|
|
69
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
70
|
+
return availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
71
|
+
}
|
|
72
|
+
function getCompletionItemsAtOffset(documentText, offset, extensions, semanticContext) {
|
|
73
|
+
if (semanticContext !== null && semanticContext !== void 0) {
|
|
74
|
+
if (semanticContext.kind === "target") {
|
|
75
|
+
return toTargetCompletionItems(
|
|
76
|
+
semanticContext.semantic.tagName,
|
|
77
|
+
semanticContext.semantic.targetCompletions
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (semanticContext.kind !== "tag-name") {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return filterTagNameCompletionItems(semanticContext.prefix, semanticContext.availableTags);
|
|
84
|
+
}
|
|
85
|
+
const resolvedContext = (0, import_internal.getSemanticCommentCompletionContextAtOffset)(
|
|
86
|
+
documentText,
|
|
87
|
+
offset,
|
|
88
|
+
extensions ? { extensions } : void 0
|
|
58
89
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
90
|
+
if (resolvedContext.kind === "target") {
|
|
91
|
+
return toTargetCompletionItems(
|
|
92
|
+
resolvedContext.semantic.tag.normalizedTagName,
|
|
93
|
+
resolvedContext.semantic.targetCompletions
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (resolvedContext.kind !== "tag-name") {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return filterTagNameCompletionItems(resolvedContext.prefix, resolvedContext.availableTags);
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
// src/providers/hover.ts
|
|
70
|
-
var
|
|
71
|
-
var CONSTRAINT_HOVER_DOCS = {
|
|
72
|
-
minimum: [
|
|
73
|
-
"**@minimum** `<number>`",
|
|
74
|
-
"",
|
|
75
|
-
"Sets an inclusive lower bound on a numeric field.",
|
|
76
|
-
"",
|
|
77
|
-
"Maps to `minimum` in JSON Schema.",
|
|
78
|
-
"",
|
|
79
|
-
"**Example:**",
|
|
80
|
-
"```typescript",
|
|
81
|
-
"/** @minimum 0 */",
|
|
82
|
-
"amount: number;",
|
|
83
|
-
"```"
|
|
84
|
-
].join("\n"),
|
|
85
|
-
maximum: [
|
|
86
|
-
"**@maximum** `<number>`",
|
|
87
|
-
"",
|
|
88
|
-
"Sets an inclusive upper bound on a numeric field.",
|
|
89
|
-
"",
|
|
90
|
-
"Maps to `maximum` in JSON Schema.",
|
|
91
|
-
"",
|
|
92
|
-
"**Example:**",
|
|
93
|
-
"```typescript",
|
|
94
|
-
"/** @maximum 100 */",
|
|
95
|
-
"percentage: number;",
|
|
96
|
-
"```"
|
|
97
|
-
].join("\n"),
|
|
98
|
-
exclusiveMinimum: [
|
|
99
|
-
"**@exclusiveMinimum** `<number>`",
|
|
100
|
-
"",
|
|
101
|
-
"Sets an exclusive lower bound on a numeric field.",
|
|
102
|
-
"",
|
|
103
|
-
"Maps to `exclusiveMinimum` in JSON Schema.",
|
|
104
|
-
"",
|
|
105
|
-
"**Example:**",
|
|
106
|
-
"```typescript",
|
|
107
|
-
"/** @exclusiveMinimum 0 */",
|
|
108
|
-
"positiveAmount: number;",
|
|
109
|
-
"```"
|
|
110
|
-
].join("\n"),
|
|
111
|
-
exclusiveMaximum: [
|
|
112
|
-
"**@exclusiveMaximum** `<number>`",
|
|
113
|
-
"",
|
|
114
|
-
"Sets an exclusive upper bound on a numeric field.",
|
|
115
|
-
"",
|
|
116
|
-
"Maps to `exclusiveMaximum` in JSON Schema.",
|
|
117
|
-
"",
|
|
118
|
-
"**Example:**",
|
|
119
|
-
"```typescript",
|
|
120
|
-
"/** @exclusiveMaximum 1 */",
|
|
121
|
-
"ratio: number;",
|
|
122
|
-
"```"
|
|
123
|
-
].join("\n"),
|
|
124
|
-
multipleOf: [
|
|
125
|
-
"**@multipleOf** `<number>`",
|
|
126
|
-
"",
|
|
127
|
-
"Requires the numeric value to be a multiple of the given number.",
|
|
128
|
-
"",
|
|
129
|
-
"Maps to `multipleOf` in JSON Schema.",
|
|
130
|
-
"",
|
|
131
|
-
"**Example:**",
|
|
132
|
-
"```typescript",
|
|
133
|
-
"/** @multipleOf 0.01 */",
|
|
134
|
-
"price: number;",
|
|
135
|
-
"```"
|
|
136
|
-
].join("\n"),
|
|
137
|
-
minLength: [
|
|
138
|
-
"**@minLength** `<number>`",
|
|
139
|
-
"",
|
|
140
|
-
"Sets a minimum character length on a string field.",
|
|
141
|
-
"",
|
|
142
|
-
"Maps to `minLength` in JSON Schema.",
|
|
143
|
-
"",
|
|
144
|
-
"**Example:**",
|
|
145
|
-
"```typescript",
|
|
146
|
-
"/** @minLength 1 */",
|
|
147
|
-
"name: string;",
|
|
148
|
-
"```"
|
|
149
|
-
].join("\n"),
|
|
150
|
-
maxLength: [
|
|
151
|
-
"**@maxLength** `<number>`",
|
|
152
|
-
"",
|
|
153
|
-
"Sets a maximum character length on a string field.",
|
|
154
|
-
"",
|
|
155
|
-
"Maps to `maxLength` in JSON Schema.",
|
|
156
|
-
"",
|
|
157
|
-
"**Example:**",
|
|
158
|
-
"```typescript",
|
|
159
|
-
"/** @maxLength 255 */",
|
|
160
|
-
"description: string;",
|
|
161
|
-
"```"
|
|
162
|
-
].join("\n"),
|
|
163
|
-
minItems: [
|
|
164
|
-
"**@minItems** `<number>`",
|
|
165
|
-
"",
|
|
166
|
-
"Sets a minimum number of items in an array field.",
|
|
167
|
-
"",
|
|
168
|
-
"Maps to `minItems` in JSON Schema.",
|
|
169
|
-
"",
|
|
170
|
-
"**Example:**",
|
|
171
|
-
"```typescript",
|
|
172
|
-
"/** @minItems 1 */",
|
|
173
|
-
"tags: string[];",
|
|
174
|
-
"```"
|
|
175
|
-
].join("\n"),
|
|
176
|
-
maxItems: [
|
|
177
|
-
"**@maxItems** `<number>`",
|
|
178
|
-
"",
|
|
179
|
-
"Sets a maximum number of items in an array field.",
|
|
180
|
-
"",
|
|
181
|
-
"Maps to `maxItems` in JSON Schema.",
|
|
182
|
-
"",
|
|
183
|
-
"**Example:**",
|
|
184
|
-
"```typescript",
|
|
185
|
-
"/** @maxItems 10 */",
|
|
186
|
-
"tags: string[];",
|
|
187
|
-
"```"
|
|
188
|
-
].join("\n"),
|
|
189
|
-
uniqueItems: [
|
|
190
|
-
"**@uniqueItems**",
|
|
191
|
-
"",
|
|
192
|
-
"Requires all items in an array field to be distinct.",
|
|
193
|
-
"",
|
|
194
|
-
"Maps to `uniqueItems` in JSON Schema.",
|
|
195
|
-
"",
|
|
196
|
-
"**Example:**",
|
|
197
|
-
"```typescript",
|
|
198
|
-
"/** @uniqueItems */",
|
|
199
|
-
"tags: string[];",
|
|
200
|
-
"```"
|
|
201
|
-
].join("\n"),
|
|
202
|
-
pattern: [
|
|
203
|
-
"**@pattern** `<regex>`",
|
|
204
|
-
"",
|
|
205
|
-
"Sets a regular expression pattern that a string field must match.",
|
|
206
|
-
"",
|
|
207
|
-
"Maps to `pattern` in JSON Schema.",
|
|
208
|
-
"",
|
|
209
|
-
"**Example:**",
|
|
210
|
-
"```typescript",
|
|
211
|
-
"/** @pattern ^[a-z0-9]+$ */",
|
|
212
|
-
"slug: string;",
|
|
213
|
-
"```"
|
|
214
|
-
].join("\n"),
|
|
215
|
-
enumOptions: [
|
|
216
|
-
"**@enumOptions** `<json-array>`",
|
|
217
|
-
"",
|
|
218
|
-
"Specifies the allowed values for an enum field as an inline JSON array.",
|
|
219
|
-
"",
|
|
220
|
-
"Maps to `enum` in JSON Schema.",
|
|
221
|
-
"",
|
|
222
|
-
"**Example:**",
|
|
223
|
-
"```typescript",
|
|
224
|
-
'/** @enumOptions ["draft","sent","archived"] */',
|
|
225
|
-
"status: string;",
|
|
226
|
-
"```"
|
|
227
|
-
].join("\n"),
|
|
228
|
-
const: [
|
|
229
|
-
"**@const** `<json-literal>`",
|
|
230
|
-
"",
|
|
231
|
-
"Requires the field value to equal a single constant JSON value.",
|
|
232
|
-
"",
|
|
233
|
-
"Maps to `const` in JSON Schema.",
|
|
234
|
-
"",
|
|
235
|
-
"**Example:**",
|
|
236
|
-
"```typescript",
|
|
237
|
-
'/** @const "USD" */',
|
|
238
|
-
"currency: string;",
|
|
239
|
-
"```"
|
|
240
|
-
].join("\n")
|
|
241
|
-
};
|
|
103
|
+
var import_internal2 = require("@formspec/analysis/internal");
|
|
242
104
|
function getHoverForTag(tagName, extensions) {
|
|
243
105
|
const raw = tagName.startsWith("@") ? tagName.slice(1) : tagName;
|
|
244
|
-
const
|
|
245
|
-
if (!
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (registration === void 0) {
|
|
253
|
-
return null;
|
|
106
|
+
const definition = (0, import_internal2.getTagDefinition)((0, import_internal2.normalizeFormSpecTagName)(raw), extensions);
|
|
107
|
+
if (!definition) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
contents: {
|
|
112
|
+
kind: "markdown",
|
|
113
|
+
value: definition.hoverMarkdown
|
|
254
114
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`Extension-defined constraint tag from \`${registration.extensionId}\`.`,
|
|
262
|
-
"",
|
|
263
|
-
"Validated through the registered FormSpec extension surface."
|
|
264
|
-
].join("\n")
|
|
265
|
-
}
|
|
266
|
-
};
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getHoverAtOffset(documentText, offset, extensions, semanticHover) {
|
|
118
|
+
const hoverInfo = semanticHover ?? (0, import_internal2.getCommentHoverInfoAtOffset)(documentText, offset, extensions ? { extensions } : void 0);
|
|
119
|
+
if (hoverInfo === null) {
|
|
120
|
+
return null;
|
|
267
121
|
}
|
|
268
122
|
return {
|
|
269
123
|
contents: {
|
|
270
124
|
kind: "markdown",
|
|
271
|
-
value:
|
|
125
|
+
value: hoverInfo.markdown
|
|
272
126
|
}
|
|
273
127
|
};
|
|
274
128
|
}
|
|
@@ -278,16 +132,185 @@ function getDefinition() {
|
|
|
278
132
|
return null;
|
|
279
133
|
}
|
|
280
134
|
|
|
135
|
+
// src/plugin-client.ts
|
|
136
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
137
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
138
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
139
|
+
var import_node_url = require("url");
|
|
140
|
+
var import_protocol = require("@formspec/analysis/protocol");
|
|
141
|
+
var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
|
|
142
|
+
function getManifestPath(workspaceRoot) {
|
|
143
|
+
return (0, import_protocol.getFormSpecManifestPath)(workspaceRoot);
|
|
144
|
+
}
|
|
145
|
+
function normalizeWorkspaceRoot(root) {
|
|
146
|
+
const resolved = import_node_path.default.resolve(root);
|
|
147
|
+
const parsed = import_node_path.default.parse(resolved);
|
|
148
|
+
let normalized = resolved;
|
|
149
|
+
while (normalized.length > parsed.root.length && normalized.endsWith(import_node_path.default.sep)) {
|
|
150
|
+
normalized = normalized.slice(0, -import_node_path.default.sep.length);
|
|
151
|
+
}
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
function getMatchingWorkspaceRoot(workspaceRoots, filePath) {
|
|
155
|
+
const normalizedFilePath = import_node_path.default.resolve(filePath);
|
|
156
|
+
const normalizedRoots = [...workspaceRoots].map(normalizeWorkspaceRoot).sort((left, right) => right.length - left.length);
|
|
157
|
+
return normalizedRoots.find(
|
|
158
|
+
(workspaceRoot) => normalizedFilePath === workspaceRoot || normalizedFilePath.startsWith(`${workspaceRoot}${import_node_path.default.sep}`)
|
|
159
|
+
) ?? null;
|
|
160
|
+
}
|
|
161
|
+
async function readManifest(workspaceRoot) {
|
|
162
|
+
try {
|
|
163
|
+
const manifestText = await import_promises.default.readFile(getManifestPath(workspaceRoot), "utf8");
|
|
164
|
+
const manifest = JSON.parse(manifestText);
|
|
165
|
+
if (!(0, import_protocol.isFormSpecAnalysisManifest)(manifest)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return manifest;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function sendSemanticQuery(manifest, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const socket = import_node_net.default.createConnection(manifest.endpoint.address);
|
|
176
|
+
let buffer = "";
|
|
177
|
+
let settled = false;
|
|
178
|
+
const finish = (response) => {
|
|
179
|
+
if (settled) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
settled = true;
|
|
183
|
+
socket.removeAllListeners("data");
|
|
184
|
+
socket.destroy();
|
|
185
|
+
resolve(response);
|
|
186
|
+
};
|
|
187
|
+
socket.setTimeout(timeoutMs, () => {
|
|
188
|
+
finish(null);
|
|
189
|
+
});
|
|
190
|
+
socket.setEncoding("utf8");
|
|
191
|
+
socket.on("connect", () => {
|
|
192
|
+
socket.write(`${JSON.stringify(query)}
|
|
193
|
+
`);
|
|
194
|
+
});
|
|
195
|
+
socket.on("data", (chunk) => {
|
|
196
|
+
buffer += String(chunk);
|
|
197
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
198
|
+
if (newlineIndex < 0) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const payload = buffer.slice(0, newlineIndex);
|
|
202
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
203
|
+
try {
|
|
204
|
+
const response = JSON.parse(payload);
|
|
205
|
+
finish((0, import_protocol.isFormSpecSemanticResponse)(response) ? response : null);
|
|
206
|
+
} catch {
|
|
207
|
+
finish(null);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
socket.on("error", () => {
|
|
211
|
+
finish(null);
|
|
212
|
+
});
|
|
213
|
+
socket.on("close", () => {
|
|
214
|
+
finish(null);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function fileUriToPathOrNull(uri) {
|
|
219
|
+
try {
|
|
220
|
+
return (0, import_node_url.fileURLToPath)(uri);
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function sendFileQuery(workspaceRoots, filePath, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
226
|
+
const workspaceRoot = getMatchingWorkspaceRoot(workspaceRoots, filePath);
|
|
227
|
+
if (workspaceRoot === null) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const manifest = await readManifest(workspaceRoot);
|
|
231
|
+
if (manifest === null) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return sendSemanticQuery(manifest, query, timeoutMs);
|
|
235
|
+
}
|
|
236
|
+
async function getPluginCompletionContextForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
237
|
+
const response = await sendFileQuery(
|
|
238
|
+
workspaceRoots,
|
|
239
|
+
filePath,
|
|
240
|
+
{
|
|
241
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
242
|
+
kind: "completion",
|
|
243
|
+
filePath,
|
|
244
|
+
offset
|
|
245
|
+
},
|
|
246
|
+
timeoutMs
|
|
247
|
+
);
|
|
248
|
+
if (response?.kind !== "completion") {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.context : null;
|
|
252
|
+
}
|
|
253
|
+
async function getPluginHoverForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
254
|
+
const response = await sendFileQuery(
|
|
255
|
+
workspaceRoots,
|
|
256
|
+
filePath,
|
|
257
|
+
{
|
|
258
|
+
protocolVersion: import_protocol.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
259
|
+
kind: "hover",
|
|
260
|
+
filePath,
|
|
261
|
+
offset
|
|
262
|
+
},
|
|
263
|
+
timeoutMs
|
|
264
|
+
);
|
|
265
|
+
if (response?.kind !== "hover") {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return response.sourceHash === (0, import_protocol.computeFormSpecTextHash)(documentText) ? response.hover : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
281
271
|
// src/server.ts
|
|
272
|
+
var PLUGIN_QUERY_TIMEOUT_ENV_VAR = "FORMSPEC_PLUGIN_QUERY_TIMEOUT_MS";
|
|
273
|
+
function dedupeWorkspaceRoots(workspaceRoots) {
|
|
274
|
+
return [...new Set(workspaceRoots)];
|
|
275
|
+
}
|
|
276
|
+
function resolvePluginQueryTimeoutMs(explicitTimeoutMs) {
|
|
277
|
+
if (explicitTimeoutMs !== void 0) {
|
|
278
|
+
return explicitTimeoutMs;
|
|
279
|
+
}
|
|
280
|
+
const rawValue = process.env[PLUGIN_QUERY_TIMEOUT_ENV_VAR];
|
|
281
|
+
if (rawValue === void 0) {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
285
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
286
|
+
}
|
|
287
|
+
function getWorkspaceRootsFromInitializeParams(params) {
|
|
288
|
+
const workspaceFolders = params.workspaceFolders?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri)).filter((workspaceRoot) => workspaceRoot !== null) ?? [];
|
|
289
|
+
const rootUri = params.rootUri === null || params.rootUri === void 0 ? null : fileUriToPathOrNull(params.rootUri);
|
|
290
|
+
const rootPath = params.rootPath ?? null;
|
|
291
|
+
return dedupeWorkspaceRoots([
|
|
292
|
+
...workspaceFolders,
|
|
293
|
+
...rootUri === null ? [] : [rootUri],
|
|
294
|
+
...rootPath === null ? [] : [rootPath]
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
282
297
|
function createServer(options = {}) {
|
|
283
298
|
const connection = (0, import_node2.createConnection)(import_node2.ProposedFeatures.all);
|
|
284
|
-
|
|
299
|
+
const documents = new import_node2.TextDocuments(import_vscode_languageserver_textdocument.TextDocument);
|
|
300
|
+
let workspaceRoots = [...options.workspaceRoots ?? []];
|
|
301
|
+
const pluginQueryTimeoutMs = resolvePluginQueryTimeoutMs(options.pluginQueryTimeoutMs);
|
|
302
|
+
documents.listen(connection);
|
|
303
|
+
connection.onInitialize((params) => {
|
|
304
|
+
workspaceRoots = dedupeWorkspaceRoots([
|
|
305
|
+
...getWorkspaceRootsFromInitializeParams(params),
|
|
306
|
+
...workspaceRoots
|
|
307
|
+
]);
|
|
285
308
|
return {
|
|
286
309
|
capabilities: {
|
|
287
310
|
textDocumentSync: import_node2.TextDocumentSyncKind.Incremental,
|
|
288
311
|
completionProvider: {
|
|
289
|
-
// Trigger completions inside JSDoc comments
|
|
290
|
-
triggerCharacters: ["@"]
|
|
312
|
+
// Trigger completions inside JSDoc comments for tags and target specifiers
|
|
313
|
+
triggerCharacters: ["@", ":"]
|
|
291
314
|
},
|
|
292
315
|
hoverProvider: true,
|
|
293
316
|
definitionProvider: true
|
|
@@ -298,11 +321,39 @@ function createServer(options = {}) {
|
|
|
298
321
|
}
|
|
299
322
|
};
|
|
300
323
|
});
|
|
301
|
-
connection.onCompletion(() => {
|
|
302
|
-
|
|
324
|
+
connection.onCompletion(async (params) => {
|
|
325
|
+
const document = documents.get(params.textDocument.uri);
|
|
326
|
+
if (!document) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const offset = document.offsetAt(params.position);
|
|
330
|
+
const documentText = document.getText();
|
|
331
|
+
const filePath = fileUriToPathOrNull(params.textDocument.uri);
|
|
332
|
+
const semanticContext = options.usePluginTransport === false || filePath === null ? null : await getPluginCompletionContextForDocument(
|
|
333
|
+
workspaceRoots,
|
|
334
|
+
filePath,
|
|
335
|
+
documentText,
|
|
336
|
+
offset,
|
|
337
|
+
pluginQueryTimeoutMs
|
|
338
|
+
);
|
|
339
|
+
return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);
|
|
303
340
|
});
|
|
304
|
-
connection.onHover((
|
|
305
|
-
|
|
341
|
+
connection.onHover(async (params) => {
|
|
342
|
+
const document = documents.get(params.textDocument.uri);
|
|
343
|
+
if (!document) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const offset = document.offsetAt(params.position);
|
|
347
|
+
const documentText = document.getText();
|
|
348
|
+
const filePath = fileUriToPathOrNull(params.textDocument.uri);
|
|
349
|
+
const semanticHover = options.usePluginTransport === false || filePath === null ? null : await getPluginHoverForDocument(
|
|
350
|
+
workspaceRoots,
|
|
351
|
+
filePath,
|
|
352
|
+
documentText,
|
|
353
|
+
offset,
|
|
354
|
+
pluginQueryTimeoutMs
|
|
355
|
+
);
|
|
356
|
+
return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);
|
|
306
357
|
});
|
|
307
358
|
connection.onDefinition((_params) => {
|
|
308
359
|
return getDefinition();
|