@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 CHANGED
@@ -1,96 +1,31 @@
1
1
  # @formspec/language-server
2
2
 
3
- Language server protocol (LSP) features for FormSpec, providing editor intelligence for JSDoc constraint tags.
3
+ Language-server support for FormSpec TSDoc tags.
4
4
 
5
- ## Installation
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
- ## Requirements
11
+ ## Features
14
12
 
15
- This package is ESM-only and requires:
13
+ - completion items for FormSpec tags
14
+ - hover documentation for recognized tags
15
+ - go-to-definition support for known tags
16
16
 
17
- ```json
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
- ```typescript
60
- import { createServer } from "@formspec/language-server";
19
+ ## Usage
61
20
 
62
- const connection = createServer();
63
- connection.listen();
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
- ### `getHoverForTag(tagName)`
78
-
79
- Returns hover documentation for a given tag name, or `null` if the tag is not recognized.
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin-client.test.d.ts.map
@@ -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 import_core = require("@formspec/core");
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
- const builtins = Object.keys(import_core.BUILTIN_CONSTRAINT_DEFINITIONS).map(
53
- (name) => ({
54
- label: `@${name}`,
55
- kind: import_node.CompletionItemKind.Keyword,
56
- detail: CONSTRAINT_DETAIL[name]
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
- const customItems = extensions?.flatMap(
60
- (extension) => (extension.constraintTags ?? []).map((tag) => ({
61
- label: `@${tag.tagName}`,
62
- kind: import_node.CompletionItemKind.Keyword,
63
- detail: `Extension constraint tag from ${extension.extensionId}`
64
- }))
65
- ) ?? [];
66
- return [...builtins, ...customItems];
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 import_core2 = require("@formspec/core");
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 name = (0, import_core2.normalizeConstraintTagName)(raw);
245
- if (!(0, import_core2.isBuiltinConstraintName)(name)) {
246
- const registration = extensions?.flatMap(
247
- (extension) => (extension.constraintTags ?? []).map((tag) => ({
248
- extensionId: extension.extensionId,
249
- tag
250
- }))
251
- ).find(({ tag }) => tag.tagName === name);
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
- return {
256
- contents: {
257
- kind: "markdown",
258
- value: [
259
- `**@${registration.tag.tagName}** \`<value>\``,
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: CONSTRAINT_HOVER_DOCS[name]
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
- connection.onInitialize(() => {
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 when `@` is typed
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
- return getCompletionItems(options.extensions);
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((_params) => {
305
- return getHoverForTag("", options.extensions);
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();