@formspec/language-server 0.1.0-alpha.17 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -78
- 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 +263 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +265 -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
package/README.md
CHANGED
|
@@ -1,96 +1,31 @@
|
|
|
1
1
|
# @formspec/language-server
|
|
2
2
|
|
|
3
|
-
Language
|
|
3
|
+
Language-server support for FormSpec TSDoc tags.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @formspec/language-server
|
|
9
|
-
# or
|
|
10
8
|
pnpm add @formspec/language-server
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
##
|
|
11
|
+
## Features
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
- completion items for FormSpec tags
|
|
14
|
+
- hover documentation for recognized tags
|
|
15
|
+
- go-to-definition support for known tags
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
// package.json
|
|
19
|
-
{
|
|
20
|
-
"type": "module"
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
// tsconfig.json
|
|
26
|
-
{
|
|
27
|
-
"compilerOptions": {
|
|
28
|
-
"module": "NodeNext",
|
|
29
|
-
"moduleResolution": "NodeNext"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Overview
|
|
35
|
-
|
|
36
|
-
This package provides language server features for FormSpec's JSDoc constraint tags (`@Minimum`, `@Maximum`, `@Pattern`, etc.). It can be integrated into any LSP-compatible editor.
|
|
37
|
-
|
|
38
|
-
### Features
|
|
39
|
-
|
|
40
|
-
- **Completion** — Autocomplete for constraint tag names inside JSDoc comments
|
|
41
|
-
- **Hover** — Documentation on hover for constraint tags
|
|
42
|
-
- **Go to Definition** — Navigate to constraint definitions _(placeholder — not yet implemented)_
|
|
43
|
-
|
|
44
|
-
## API Reference
|
|
45
|
-
|
|
46
|
-
### Functions
|
|
47
|
-
|
|
48
|
-
| Function | Description |
|
|
49
|
-
| ------------------------- | ----------------------------------------------- |
|
|
50
|
-
| `createServer()` | Create a full LSP server connection |
|
|
51
|
-
| `getCompletionItems()` | Get completion items for constraint tags |
|
|
52
|
-
| `getDefinition()` | Get definition location for a constraint tag |
|
|
53
|
-
| `getHoverForTag(tagName)` | Get hover information for a constraint tag name |
|
|
54
|
-
|
|
55
|
-
### `createServer()`
|
|
56
|
-
|
|
57
|
-
Creates a Language Server Protocol connection that handles `initialize`, `textDocument/completion`, `textDocument/hover`, and `textDocument/definition` requests.
|
|
17
|
+
Diagnostics are intentionally handled elsewhere; this package focuses on editor assistance.
|
|
58
18
|
|
|
59
|
-
|
|
60
|
-
import { createServer } from "@formspec/language-server";
|
|
19
|
+
## Usage
|
|
61
20
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### `getCompletionItems()`
|
|
67
|
-
|
|
68
|
-
Returns completion items for all known FormSpec constraint tags.
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
import { getCompletionItems } from "@formspec/language-server";
|
|
72
|
-
|
|
73
|
-
const items = getCompletionItems();
|
|
74
|
-
// [{ label: "@Minimum", kind: CompletionItemKind.Keyword, ... }, ...]
|
|
75
|
-
```
|
|
21
|
+
```ts
|
|
22
|
+
import { createServer, getCompletionItems, getHoverForTag } from "@formspec/language-server";
|
|
76
23
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import { getHoverForTag } from "@formspec/language-server";
|
|
83
|
-
|
|
84
|
-
const hover = getHoverForTag("Minimum");
|
|
85
|
-
// { contents: { kind: "markdown", value: "..." } }
|
|
24
|
+
const server = createServer();
|
|
25
|
+
const completions = getCompletionItems();
|
|
26
|
+
const hover = getHoverForTag("minimum");
|
|
86
27
|
```
|
|
87
28
|
|
|
88
|
-
## Editor Integration
|
|
89
|
-
|
|
90
|
-
### VS Code
|
|
91
|
-
|
|
92
|
-
Use with a VS Code extension that connects to the language server. The server communicates over the standard LSP protocol via `vscode-languageserver/node.js`.
|
|
93
|
-
|
|
94
29
|
## License
|
|
95
30
|
|
|
96
31
|
UNLICENSED
|
|
@@ -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,83 @@ 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_analysis = require("@formspec/analysis");
|
|
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_analysis.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 getCompletionItemsAtOffset(documentText, offset, extensions, semanticContext) {
|
|
62
|
+
if (semanticContext !== null && semanticContext !== void 0) {
|
|
63
|
+
if (semanticContext.kind === "target") {
|
|
64
|
+
return semanticContext.semantic.targetCompletions.map((target) => ({
|
|
65
|
+
label: target,
|
|
66
|
+
kind: target === "singular" || target === "plural" ? import_node.CompletionItemKind.EnumMember : import_node.CompletionItemKind.Field,
|
|
67
|
+
detail: `Target for @${semanticContext.semantic.tagName}`
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
if (semanticContext.kind !== "tag-name") {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const normalizedPrefix2 = semanticContext.prefix.toLowerCase();
|
|
74
|
+
return semanticContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix2));
|
|
75
|
+
}
|
|
76
|
+
const resolvedContext = (0, import_analysis.getSemanticCommentCompletionContextAtOffset)(
|
|
77
|
+
documentText,
|
|
78
|
+
offset,
|
|
79
|
+
extensions ? { extensions } : void 0
|
|
58
80
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
label:
|
|
62
|
-
kind: import_node.CompletionItemKind.
|
|
63
|
-
detail: `
|
|
64
|
-
}))
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
if (resolvedContext.kind === "target") {
|
|
82
|
+
return resolvedContext.semantic.targetCompletions.map((target) => ({
|
|
83
|
+
label: target,
|
|
84
|
+
kind: target === "singular" || target === "plural" ? import_node.CompletionItemKind.EnumMember : import_node.CompletionItemKind.Field,
|
|
85
|
+
detail: `Target for @${resolvedContext.semantic.tag.normalizedTagName}`
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (resolvedContext.kind !== "tag-name") {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const normalizedPrefix = resolvedContext.prefix.toLowerCase();
|
|
92
|
+
return resolvedContext.availableTags.map(toCompletionItem).filter((item) => item.label.slice(1).toLowerCase().startsWith(normalizedPrefix));
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
// 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
|
-
};
|
|
96
|
+
var import_analysis2 = require("@formspec/analysis");
|
|
242
97
|
function getHoverForTag(tagName, extensions) {
|
|
243
98
|
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;
|
|
99
|
+
const definition = (0, import_analysis2.getTagDefinition)((0, import_analysis2.normalizeFormSpecTagName)(raw), extensions);
|
|
100
|
+
if (!definition) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
contents: {
|
|
105
|
+
kind: "markdown",
|
|
106
|
+
value: definition.hoverMarkdown
|
|
254
107
|
}
|
|
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
|
-
};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function getHoverAtOffset(documentText, offset, extensions, semanticHover) {
|
|
111
|
+
const hoverInfo = semanticHover ?? (0, import_analysis2.getCommentHoverInfoAtOffset)(documentText, offset, extensions ? { extensions } : void 0);
|
|
112
|
+
if (hoverInfo === null) {
|
|
113
|
+
return null;
|
|
267
114
|
}
|
|
268
115
|
return {
|
|
269
116
|
contents: {
|
|
270
117
|
kind: "markdown",
|
|
271
|
-
value:
|
|
118
|
+
value: hoverInfo.markdown
|
|
272
119
|
}
|
|
273
120
|
};
|
|
274
121
|
}
|
|
@@ -278,16 +125,172 @@ function getDefinition() {
|
|
|
278
125
|
return null;
|
|
279
126
|
}
|
|
280
127
|
|
|
128
|
+
// src/plugin-client.ts
|
|
129
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
130
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
131
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
132
|
+
var import_node_url = require("url");
|
|
133
|
+
var import_analysis3 = require("@formspec/analysis");
|
|
134
|
+
var DEFAULT_PLUGIN_QUERY_TIMEOUT_MS = 2e3;
|
|
135
|
+
function getManifestPath(workspaceRoot) {
|
|
136
|
+
return (0, import_analysis3.getFormSpecManifestPath)(workspaceRoot);
|
|
137
|
+
}
|
|
138
|
+
function normalizeWorkspaceRoot(root) {
|
|
139
|
+
const resolved = import_node_path.default.resolve(root);
|
|
140
|
+
const parsed = import_node_path.default.parse(resolved);
|
|
141
|
+
let normalized = resolved;
|
|
142
|
+
while (normalized.length > parsed.root.length && normalized.endsWith(import_node_path.default.sep)) {
|
|
143
|
+
normalized = normalized.slice(0, -import_node_path.default.sep.length);
|
|
144
|
+
}
|
|
145
|
+
return normalized;
|
|
146
|
+
}
|
|
147
|
+
function getMatchingWorkspaceRoot(workspaceRoots, filePath) {
|
|
148
|
+
const normalizedFilePath = import_node_path.default.resolve(filePath);
|
|
149
|
+
const normalizedRoots = [...workspaceRoots].map(normalizeWorkspaceRoot).sort((left, right) => right.length - left.length);
|
|
150
|
+
return normalizedRoots.find(
|
|
151
|
+
(workspaceRoot) => normalizedFilePath === workspaceRoot || normalizedFilePath.startsWith(`${workspaceRoot}${import_node_path.default.sep}`)
|
|
152
|
+
) ?? null;
|
|
153
|
+
}
|
|
154
|
+
async function readManifest(workspaceRoot) {
|
|
155
|
+
try {
|
|
156
|
+
const manifestText = await import_promises.default.readFile(getManifestPath(workspaceRoot), "utf8");
|
|
157
|
+
const manifest = JSON.parse(manifestText);
|
|
158
|
+
if (!(0, import_analysis3.isFormSpecAnalysisManifest)(manifest)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return manifest;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function sendSemanticQuery(manifest, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const socket = import_node_net.default.createConnection(manifest.endpoint.address);
|
|
169
|
+
let buffer = "";
|
|
170
|
+
let settled = false;
|
|
171
|
+
const finish = (response) => {
|
|
172
|
+
if (settled) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
settled = true;
|
|
176
|
+
socket.removeAllListeners("data");
|
|
177
|
+
socket.destroy();
|
|
178
|
+
resolve(response);
|
|
179
|
+
};
|
|
180
|
+
socket.setTimeout(timeoutMs, () => {
|
|
181
|
+
finish(null);
|
|
182
|
+
});
|
|
183
|
+
socket.setEncoding("utf8");
|
|
184
|
+
socket.on("connect", () => {
|
|
185
|
+
socket.write(`${JSON.stringify(query)}
|
|
186
|
+
`);
|
|
187
|
+
});
|
|
188
|
+
socket.on("data", (chunk) => {
|
|
189
|
+
buffer += String(chunk);
|
|
190
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
191
|
+
if (newlineIndex < 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const payload = buffer.slice(0, newlineIndex);
|
|
195
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
196
|
+
try {
|
|
197
|
+
const response = JSON.parse(payload);
|
|
198
|
+
finish((0, import_analysis3.isFormSpecSemanticResponse)(response) ? response : null);
|
|
199
|
+
} catch {
|
|
200
|
+
finish(null);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
socket.on("error", () => {
|
|
204
|
+
finish(null);
|
|
205
|
+
});
|
|
206
|
+
socket.on("close", () => {
|
|
207
|
+
finish(null);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function fileUriToPathOrNull(uri) {
|
|
212
|
+
try {
|
|
213
|
+
return (0, import_node_url.fileURLToPath)(uri);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function sendFileQuery(workspaceRoots, filePath, query, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
219
|
+
const workspaceRoot = getMatchingWorkspaceRoot(workspaceRoots, filePath);
|
|
220
|
+
if (workspaceRoot === null) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const manifest = await readManifest(workspaceRoot);
|
|
224
|
+
if (manifest === null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return sendSemanticQuery(manifest, query, timeoutMs);
|
|
228
|
+
}
|
|
229
|
+
async function getPluginCompletionContextForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
230
|
+
const response = await sendFileQuery(
|
|
231
|
+
workspaceRoots,
|
|
232
|
+
filePath,
|
|
233
|
+
{
|
|
234
|
+
protocolVersion: import_analysis3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
235
|
+
kind: "completion",
|
|
236
|
+
filePath,
|
|
237
|
+
offset
|
|
238
|
+
},
|
|
239
|
+
timeoutMs
|
|
240
|
+
);
|
|
241
|
+
if (response?.kind !== "completion") {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return response.sourceHash === (0, import_analysis3.computeFormSpecTextHash)(documentText) ? response.context : null;
|
|
245
|
+
}
|
|
246
|
+
async function getPluginHoverForDocument(workspaceRoots, filePath, documentText, offset, timeoutMs = DEFAULT_PLUGIN_QUERY_TIMEOUT_MS) {
|
|
247
|
+
const response = await sendFileQuery(
|
|
248
|
+
workspaceRoots,
|
|
249
|
+
filePath,
|
|
250
|
+
{
|
|
251
|
+
protocolVersion: import_analysis3.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
252
|
+
kind: "hover",
|
|
253
|
+
filePath,
|
|
254
|
+
offset
|
|
255
|
+
},
|
|
256
|
+
timeoutMs
|
|
257
|
+
);
|
|
258
|
+
if (response?.kind !== "hover") {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return response.sourceHash === (0, import_analysis3.computeFormSpecTextHash)(documentText) ? response.hover : null;
|
|
262
|
+
}
|
|
263
|
+
|
|
281
264
|
// src/server.ts
|
|
265
|
+
function dedupeWorkspaceRoots(workspaceRoots) {
|
|
266
|
+
return [...new Set(workspaceRoots)];
|
|
267
|
+
}
|
|
268
|
+
function getWorkspaceRootsFromInitializeParams(params) {
|
|
269
|
+
const workspaceFolders = params.workspaceFolders?.map((workspaceFolder) => fileUriToPathOrNull(workspaceFolder.uri)).filter((workspaceRoot) => workspaceRoot !== null) ?? [];
|
|
270
|
+
const rootUri = params.rootUri === null || params.rootUri === void 0 ? null : fileUriToPathOrNull(params.rootUri);
|
|
271
|
+
const rootPath = params.rootPath ?? null;
|
|
272
|
+
return dedupeWorkspaceRoots([
|
|
273
|
+
...workspaceFolders,
|
|
274
|
+
...rootUri === null ? [] : [rootUri],
|
|
275
|
+
...rootPath === null ? [] : [rootPath]
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
282
278
|
function createServer(options = {}) {
|
|
283
279
|
const connection = (0, import_node2.createConnection)(import_node2.ProposedFeatures.all);
|
|
284
|
-
|
|
280
|
+
const documents = new import_node2.TextDocuments(import_vscode_languageserver_textdocument.TextDocument);
|
|
281
|
+
let workspaceRoots = [...options.workspaceRoots ?? []];
|
|
282
|
+
documents.listen(connection);
|
|
283
|
+
connection.onInitialize((params) => {
|
|
284
|
+
workspaceRoots = dedupeWorkspaceRoots([
|
|
285
|
+
...getWorkspaceRootsFromInitializeParams(params),
|
|
286
|
+
...workspaceRoots
|
|
287
|
+
]);
|
|
285
288
|
return {
|
|
286
289
|
capabilities: {
|
|
287
290
|
textDocumentSync: import_node2.TextDocumentSyncKind.Incremental,
|
|
288
291
|
completionProvider: {
|
|
289
|
-
// Trigger completions inside JSDoc comments
|
|
290
|
-
triggerCharacters: ["@"]
|
|
292
|
+
// Trigger completions inside JSDoc comments for tags and target specifiers
|
|
293
|
+
triggerCharacters: ["@", ":"]
|
|
291
294
|
},
|
|
292
295
|
hoverProvider: true,
|
|
293
296
|
definitionProvider: true
|
|
@@ -298,11 +301,39 @@ function createServer(options = {}) {
|
|
|
298
301
|
}
|
|
299
302
|
};
|
|
300
303
|
});
|
|
301
|
-
connection.onCompletion(() => {
|
|
302
|
-
|
|
304
|
+
connection.onCompletion(async (params) => {
|
|
305
|
+
const document = documents.get(params.textDocument.uri);
|
|
306
|
+
if (!document) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const offset = document.offsetAt(params.position);
|
|
310
|
+
const documentText = document.getText();
|
|
311
|
+
const filePath = fileUriToPathOrNull(params.textDocument.uri);
|
|
312
|
+
const semanticContext = options.usePluginTransport === false || filePath === null ? null : await getPluginCompletionContextForDocument(
|
|
313
|
+
workspaceRoots,
|
|
314
|
+
filePath,
|
|
315
|
+
documentText,
|
|
316
|
+
offset,
|
|
317
|
+
options.pluginQueryTimeoutMs
|
|
318
|
+
);
|
|
319
|
+
return getCompletionItemsAtOffset(documentText, offset, options.extensions, semanticContext);
|
|
303
320
|
});
|
|
304
|
-
connection.onHover((
|
|
305
|
-
|
|
321
|
+
connection.onHover(async (params) => {
|
|
322
|
+
const document = documents.get(params.textDocument.uri);
|
|
323
|
+
if (!document) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const offset = document.offsetAt(params.position);
|
|
327
|
+
const documentText = document.getText();
|
|
328
|
+
const filePath = fileUriToPathOrNull(params.textDocument.uri);
|
|
329
|
+
const semanticHover = options.usePluginTransport === false || filePath === null ? null : await getPluginHoverForDocument(
|
|
330
|
+
workspaceRoots,
|
|
331
|
+
filePath,
|
|
332
|
+
documentText,
|
|
333
|
+
offset,
|
|
334
|
+
options.pluginQueryTimeoutMs
|
|
335
|
+
);
|
|
336
|
+
return getHoverAtOffset(documentText, offset, options.extensions, semanticHover);
|
|
306
337
|
});
|
|
307
338
|
connection.onDefinition((_params) => {
|
|
308
339
|
return getDefinition();
|